@streichsbaer/pi-mesh 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 +113 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +796 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/live-socket.d.ts +17 -0
- package/dist/live-socket.js +116 -0
- package/dist/lock.d.ts +7 -0
- package/dist/lock.js +82 -0
- package/dist/mesh.d.ts +3 -0
- package/dist/mesh.js +22 -0
- package/dist/model-list.d.ts +48 -0
- package/dist/model-list.js +208 -0
- package/dist/model-selection.d.ts +47 -0
- package/dist/model-selection.js +171 -0
- package/dist/pi-runner.d.ts +39 -0
- package/dist/pi-runner.js +182 -0
- package/dist/pi-session-parser.d.ts +57 -0
- package/dist/pi-session-parser.js +459 -0
- package/dist/registry.d.ts +24 -0
- package/dist/registry.js +139 -0
- package/dist/types.d.ts +142 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +17 -0
- package/dist/utils.js +157 -0
- package/package.json +72 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import type { DeliveryMode, ModelSelection } from "./types.js";
|
|
3
|
+
export interface LiveSocketSession {
|
|
4
|
+
isStreaming: boolean;
|
|
5
|
+
agent: {
|
|
6
|
+
waitForIdle: () => Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export interface StartSessionSocketOptions<TSession extends LiveSocketSession> {
|
|
10
|
+
socketPath: string;
|
|
11
|
+
getSession: () => TSession;
|
|
12
|
+
deliverToSession: (session: TSession, message: string, delivery: DeliveryMode) => Promise<void>;
|
|
13
|
+
applyModelSelection: (session: TSession, selection: ModelSelection | undefined) => Promise<void>;
|
|
14
|
+
onStatus?: (status: "busy" | "idle" | "error", error?: string) => Promise<void> | void;
|
|
15
|
+
}
|
|
16
|
+
export declare function startSessionSocket<TSession extends LiveSocketSession>(options: StartSessionSocketOptions<TSession>): Promise<net.Server>;
|
|
17
|
+
export declare function sendToLiveSocket(socketPath: string, message: string, delivery?: DeliveryMode, modelSelection?: ModelSelection, timeoutMs?: number): Promise<void>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { hasModelSelection } from "./model-selection.js";
|
|
5
|
+
import { ensurePrivateDir } from "./utils.js";
|
|
6
|
+
export async function startSessionSocket(options) {
|
|
7
|
+
await ensurePrivateDir(path.dirname(options.socketPath));
|
|
8
|
+
await fs.rm(options.socketPath, { force: true }).catch(() => undefined);
|
|
9
|
+
let queue = Promise.resolve();
|
|
10
|
+
const enqueue = (task) => {
|
|
11
|
+
queue = queue.then(task, task);
|
|
12
|
+
void queue;
|
|
13
|
+
};
|
|
14
|
+
const server = net.createServer((socket) => {
|
|
15
|
+
let buffer = "";
|
|
16
|
+
socket.setEncoding("utf8");
|
|
17
|
+
socket.on("error", () => undefined); // Keep the live session running if a client disconnects.
|
|
18
|
+
socket.on("data", (chunk) => {
|
|
19
|
+
buffer += chunk;
|
|
20
|
+
while (true) {
|
|
21
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
22
|
+
if (newlineIndex === -1)
|
|
23
|
+
return;
|
|
24
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
25
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
26
|
+
if (!line)
|
|
27
|
+
continue;
|
|
28
|
+
let request;
|
|
29
|
+
try {
|
|
30
|
+
request = JSON.parse(line);
|
|
31
|
+
if (request.type !== "send" || !request.message)
|
|
32
|
+
throw new Error("Expected send request with message");
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
socket.write(`${JSON.stringify({ type: "response", success: false, error: error.message })}\n`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
enqueue(async () => {
|
|
39
|
+
let markedBusy = false;
|
|
40
|
+
try {
|
|
41
|
+
const session = options.getSession();
|
|
42
|
+
if (hasModelSelection(request.modelSelection) && session.isStreaming) {
|
|
43
|
+
throw new Error("Cannot change model or thinking level while target session is busy; wait until idle or send without model options.");
|
|
44
|
+
}
|
|
45
|
+
await options.applyModelSelection(session, request.modelSelection);
|
|
46
|
+
await options.onStatus?.("busy");
|
|
47
|
+
markedBusy = true;
|
|
48
|
+
await options.deliverToSession(session, request.message, request.delivery ?? "auto");
|
|
49
|
+
await session.agent.waitForIdle();
|
|
50
|
+
await options.onStatus?.("idle");
|
|
51
|
+
socket.write(`${JSON.stringify({ id: request.id, type: "response", success: true })}\n`);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (markedBusy)
|
|
55
|
+
await options.onStatus?.("error", error.message);
|
|
56
|
+
socket.write(`${JSON.stringify({ id: request.id, type: "response", success: false, error: error.message })}\n`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
await new Promise((resolve, reject) => {
|
|
63
|
+
server.once("error", reject);
|
|
64
|
+
server.listen(options.socketPath, () => {
|
|
65
|
+
server.off("error", reject);
|
|
66
|
+
resolve();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
return server;
|
|
70
|
+
}
|
|
71
|
+
export async function sendToLiveSocket(socketPath, message, delivery = "auto", modelSelection, timeoutMs = 0) {
|
|
72
|
+
const id = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
73
|
+
const request = { id, type: "send", message, delivery, modelSelection };
|
|
74
|
+
await new Promise((resolve, reject) => {
|
|
75
|
+
const socket = net.createConnection(socketPath);
|
|
76
|
+
let buffer = "";
|
|
77
|
+
let settled = false;
|
|
78
|
+
const timer = timeoutMs > 0
|
|
79
|
+
? setTimeout(() => finish(new Error(`Socket request timed out after ${timeoutMs}ms`)), timeoutMs)
|
|
80
|
+
: undefined;
|
|
81
|
+
const finish = (error) => {
|
|
82
|
+
if (settled)
|
|
83
|
+
return;
|
|
84
|
+
settled = true;
|
|
85
|
+
if (timer)
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
if (error)
|
|
88
|
+
reject(error);
|
|
89
|
+
else
|
|
90
|
+
resolve();
|
|
91
|
+
socket.destroy();
|
|
92
|
+
};
|
|
93
|
+
socket.setEncoding("utf8");
|
|
94
|
+
socket.once("connect", () => socket.write(`${JSON.stringify(request)}\n`));
|
|
95
|
+
socket.once("error", (error) => finish(error));
|
|
96
|
+
socket.once("end", () => finish(new Error("Socket closed before response")));
|
|
97
|
+
socket.once("close", () => finish(new Error("Socket closed before response")));
|
|
98
|
+
socket.on("data", (chunk) => {
|
|
99
|
+
buffer += chunk;
|
|
100
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
101
|
+
if (newlineIndex === -1)
|
|
102
|
+
return;
|
|
103
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
104
|
+
try {
|
|
105
|
+
const response = JSON.parse(line);
|
|
106
|
+
if (response.success)
|
|
107
|
+
finish();
|
|
108
|
+
else
|
|
109
|
+
finish(new Error(response.error || "Socket request failed"));
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
finish(error);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
package/dist/lock.d.ts
ADDED
package/dist/lock.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const DEFAULT_STALE_MS = 20 * 60 * 1000;
|
|
4
|
+
const DEFAULT_WAIT_MS = 60 * 1000;
|
|
5
|
+
const DEFAULT_POLL_MS = 250;
|
|
6
|
+
const DEFAULT_HEARTBEAT_MS = 5_000;
|
|
7
|
+
function sleep(ms) {
|
|
8
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
10
|
+
function isProcessAlive(pid) {
|
|
11
|
+
if (!pid || pid <= 0)
|
|
12
|
+
return false;
|
|
13
|
+
try {
|
|
14
|
+
process.kill(pid, 0);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
return error.code === "EPERM";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function readOwner(lockDir) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(await fs.readFile(path.join(lockDir, "owner.json"), "utf8"));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function touchLock(lockDir) {
|
|
30
|
+
const now = new Date();
|
|
31
|
+
await fs.utimes(lockDir, now, now).catch(() => undefined);
|
|
32
|
+
await fs.utimes(path.join(lockDir, "owner.json"), now, now).catch(() => undefined);
|
|
33
|
+
}
|
|
34
|
+
async function removeIfStale(lockDir, staleMs) {
|
|
35
|
+
const stat = await fs.stat(lockDir).catch(() => null);
|
|
36
|
+
if (!stat)
|
|
37
|
+
return;
|
|
38
|
+
if (Date.now() - stat.mtimeMs <= staleMs)
|
|
39
|
+
return;
|
|
40
|
+
const owner = await readOwner(lockDir);
|
|
41
|
+
if (isProcessAlive(owner?.pid)) {
|
|
42
|
+
await touchLock(lockDir);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
await fs.rm(lockDir, { recursive: true, force: true }).catch(() => undefined);
|
|
46
|
+
}
|
|
47
|
+
export async function withDirectoryLock(lockDir, fn, options = {}) {
|
|
48
|
+
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
49
|
+
const waitMs = options.waitMs ?? DEFAULT_WAIT_MS;
|
|
50
|
+
const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
|
|
51
|
+
const heartbeatMs = Math.max(1_000, Math.min(options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS, Math.max(1_000, staleMs / 3)));
|
|
52
|
+
const started = Date.now();
|
|
53
|
+
let heartbeat;
|
|
54
|
+
await fs.mkdir(path.dirname(lockDir), { recursive: true });
|
|
55
|
+
while (true) {
|
|
56
|
+
await removeIfStale(lockDir, staleMs);
|
|
57
|
+
try {
|
|
58
|
+
await fs.mkdir(lockDir);
|
|
59
|
+
await fs.writeFile(path.join(lockDir, "owner.json"), JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }, null, 2), "utf8");
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
const code = error.code;
|
|
64
|
+
if (code !== "EEXIST")
|
|
65
|
+
throw error;
|
|
66
|
+
if (Date.now() - started > waitMs)
|
|
67
|
+
throw new Error(`Timed out waiting for lock: ${lockDir}`);
|
|
68
|
+
await sleep(pollMs);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
heartbeat = setInterval(() => {
|
|
72
|
+
void touchLock(lockDir);
|
|
73
|
+
}, heartbeatMs);
|
|
74
|
+
try {
|
|
75
|
+
return await fn();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
if (heartbeat)
|
|
79
|
+
clearInterval(heartbeat);
|
|
80
|
+
await fs.rm(lockDir, { recursive: true, force: true }).catch(() => undefined);
|
|
81
|
+
}
|
|
82
|
+
}
|
package/dist/mesh.d.ts
ADDED
package/dist/mesh.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { ensureDir, piAgentDir } from "./utils.js";
|
|
3
|
+
export async function resolveMesh() {
|
|
4
|
+
const baseDir = path.join(piAgentDir(), "pi-mesh");
|
|
5
|
+
const mesh = {
|
|
6
|
+
id: "local",
|
|
7
|
+
baseDir,
|
|
8
|
+
registryFile: path.join(baseDir, "registry.jsonl"),
|
|
9
|
+
inboxDir: path.join(baseDir, "inbox"),
|
|
10
|
+
locksDir: path.join(baseDir, "locks"),
|
|
11
|
+
socketDirFile: path.join(baseDir, "socket-dir"),
|
|
12
|
+
};
|
|
13
|
+
await ensureMesh(mesh);
|
|
14
|
+
return mesh;
|
|
15
|
+
}
|
|
16
|
+
export async function ensureMesh(mesh) {
|
|
17
|
+
await Promise.all([
|
|
18
|
+
ensureDir(mesh.baseDir),
|
|
19
|
+
ensureDir(mesh.inboxDir),
|
|
20
|
+
ensureDir(mesh.locksDir),
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type AgentSessionServices } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type ThinkingLevel } from "./types.js";
|
|
3
|
+
export type ModelListServices = Pick<AgentSessionServices, "cwd" | "diagnostics" | "modelRegistry" | "settingsManager">;
|
|
4
|
+
export interface ListModelsOptions {
|
|
5
|
+
cwd: string;
|
|
6
|
+
search?: string;
|
|
7
|
+
includeAll?: boolean;
|
|
8
|
+
scopedOnly?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ListedModel {
|
|
11
|
+
provider: string;
|
|
12
|
+
model: string;
|
|
13
|
+
ref: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
available: boolean;
|
|
16
|
+
scoped: boolean;
|
|
17
|
+
matchedScopedPatterns: string[];
|
|
18
|
+
default: boolean;
|
|
19
|
+
contextWindow: number;
|
|
20
|
+
maxTokens: number;
|
|
21
|
+
thinking: boolean;
|
|
22
|
+
images: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface ModelListResult {
|
|
25
|
+
ok: true;
|
|
26
|
+
folder: string;
|
|
27
|
+
search?: string;
|
|
28
|
+
includeAll: boolean;
|
|
29
|
+
scopedOnly: boolean;
|
|
30
|
+
enabledModelPatterns: string[];
|
|
31
|
+
defaultModel?: {
|
|
32
|
+
provider?: string;
|
|
33
|
+
model?: string;
|
|
34
|
+
thinkingLevel?: ThinkingLevel;
|
|
35
|
+
};
|
|
36
|
+
scopedResolution: {
|
|
37
|
+
approximate: boolean;
|
|
38
|
+
note?: string;
|
|
39
|
+
};
|
|
40
|
+
diagnostics: Array<{
|
|
41
|
+
type: string;
|
|
42
|
+
message: string;
|
|
43
|
+
}>;
|
|
44
|
+
models: ListedModel[];
|
|
45
|
+
}
|
|
46
|
+
export declare function listModelsFromServices(services: ModelListServices, options: ListModelsOptions): ModelListResult;
|
|
47
|
+
export declare function listModels(options: ListModelsOptions): Promise<ModelListResult>;
|
|
48
|
+
export declare function printModelList(result: ModelListResult): void;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { createAgentSessionServices } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { THINKING_LEVELS } from "./types.js";
|
|
3
|
+
const SCOPED_RESOLUTION_NOTE = "Pi does not currently export stable public helpers for CLI-equivalent model or scoped-model resolution; pi-mesh approximates enabledModels matching against Pi available models.";
|
|
4
|
+
function formatModelRef(model) {
|
|
5
|
+
return `${model.provider}/${model.id}`;
|
|
6
|
+
}
|
|
7
|
+
function modelKey(model) {
|
|
8
|
+
return `${model.provider}\0${model.model}`;
|
|
9
|
+
}
|
|
10
|
+
function isThinkingLevel(value) {
|
|
11
|
+
return typeof value === "string" && THINKING_LEVELS.includes(value);
|
|
12
|
+
}
|
|
13
|
+
function stripThinkingSuffix(pattern) {
|
|
14
|
+
const colonIndex = pattern.lastIndexOf(":");
|
|
15
|
+
if (colonIndex <= 0)
|
|
16
|
+
return pattern;
|
|
17
|
+
return isThinkingLevel(pattern.slice(colonIndex + 1)) ? pattern.slice(0, colonIndex) : pattern;
|
|
18
|
+
}
|
|
19
|
+
function hasGlob(pattern) {
|
|
20
|
+
return /[*?[]/.test(pattern);
|
|
21
|
+
}
|
|
22
|
+
function globToRegExp(pattern) {
|
|
23
|
+
let source = "^";
|
|
24
|
+
for (const char of pattern) {
|
|
25
|
+
if (char === "*")
|
|
26
|
+
source += ".*";
|
|
27
|
+
else if (char === "?")
|
|
28
|
+
source += ".";
|
|
29
|
+
else
|
|
30
|
+
source += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
31
|
+
}
|
|
32
|
+
return new RegExp(`${source}$`, "i");
|
|
33
|
+
}
|
|
34
|
+
function isDatedModelId(id) {
|
|
35
|
+
return /\d{4}[-_]\d{2}[-_]\d{2}|\d{8}/.test(id);
|
|
36
|
+
}
|
|
37
|
+
function chooseBestModel(matches) {
|
|
38
|
+
if (matches.length === 0)
|
|
39
|
+
return undefined;
|
|
40
|
+
const aliases = matches.filter((model) => !isDatedModelId(model.id));
|
|
41
|
+
const candidates = aliases.length ? aliases : matches;
|
|
42
|
+
return [...candidates].sort((a, b) => b.id.localeCompare(a.id))[0];
|
|
43
|
+
}
|
|
44
|
+
function findExactScopedModelReferenceMatch(pattern, models) {
|
|
45
|
+
const lower = pattern.toLowerCase();
|
|
46
|
+
const canonicalMatches = models.filter((model) => formatModelRef(model).toLowerCase() === lower);
|
|
47
|
+
if (canonicalMatches.length === 1)
|
|
48
|
+
return canonicalMatches[0];
|
|
49
|
+
if (canonicalMatches.length > 1)
|
|
50
|
+
return undefined;
|
|
51
|
+
const slashIndex = pattern.indexOf("/");
|
|
52
|
+
if (slashIndex !== -1) {
|
|
53
|
+
const provider = pattern.slice(0, slashIndex).trim().toLowerCase();
|
|
54
|
+
const modelId = pattern.slice(slashIndex + 1).trim().toLowerCase();
|
|
55
|
+
if (provider && modelId) {
|
|
56
|
+
const providerMatches = models.filter((model) => model.provider.toLowerCase() === provider && model.id.toLowerCase() === modelId);
|
|
57
|
+
if (providerMatches.length === 1)
|
|
58
|
+
return providerMatches[0];
|
|
59
|
+
if (providerMatches.length > 1)
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const idMatches = models.filter((model) => model.id.toLowerCase() === lower);
|
|
64
|
+
return idMatches.length === 1 ? idMatches[0] : undefined;
|
|
65
|
+
}
|
|
66
|
+
function resolveScopedPattern(rawPattern, models) {
|
|
67
|
+
const pattern = stripThinkingSuffix(rawPattern.trim());
|
|
68
|
+
if (!pattern)
|
|
69
|
+
return [];
|
|
70
|
+
if (hasGlob(pattern)) {
|
|
71
|
+
const regex = globToRegExp(pattern);
|
|
72
|
+
return models.filter((model) => regex.test(formatModelRef(model)) || regex.test(model.id));
|
|
73
|
+
}
|
|
74
|
+
const exact = findExactScopedModelReferenceMatch(pattern, models);
|
|
75
|
+
if (exact)
|
|
76
|
+
return [exact];
|
|
77
|
+
const lower = pattern.toLowerCase();
|
|
78
|
+
const partial = models.filter((model) => model.id.toLowerCase().includes(lower) || Boolean(model.name?.toLowerCase().includes(lower)));
|
|
79
|
+
return [chooseBestModel(partial)].filter((model) => Boolean(model));
|
|
80
|
+
}
|
|
81
|
+
function matchesSearch(model, search) {
|
|
82
|
+
if (!search)
|
|
83
|
+
return true;
|
|
84
|
+
const lower = search.toLowerCase();
|
|
85
|
+
return [model.provider, model.model, model.ref, model.name]
|
|
86
|
+
.filter(Boolean)
|
|
87
|
+
.some((value) => String(value).toLowerCase().includes(lower));
|
|
88
|
+
}
|
|
89
|
+
export function listModelsFromServices(services, options) {
|
|
90
|
+
const allModels = services.modelRegistry.getAll();
|
|
91
|
+
const availableModels = services.modelRegistry.getAvailable();
|
|
92
|
+
const availableKeys = new Set(availableModels.map((model) => modelKey({ provider: model.provider, model: model.id })));
|
|
93
|
+
const enabledModelPatterns = services.settingsManager.getEnabledModels() ?? [];
|
|
94
|
+
const defaultProvider = services.settingsManager.getDefaultProvider();
|
|
95
|
+
const defaultModel = services.settingsManager.getDefaultModel();
|
|
96
|
+
const defaultThinkingLevel = services.settingsManager.getDefaultThinkingLevel();
|
|
97
|
+
const scopedMatches = new Map();
|
|
98
|
+
for (const pattern of enabledModelPatterns) {
|
|
99
|
+
for (const model of resolveScopedPattern(pattern, availableModels)) {
|
|
100
|
+
const key = modelKey({ provider: model.provider, model: model.id });
|
|
101
|
+
scopedMatches.set(key, [...(scopedMatches.get(key) ?? []), pattern]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const sourceModels = options.includeAll ? allModels : availableModels;
|
|
105
|
+
const rows = sourceModels
|
|
106
|
+
.map((model) => {
|
|
107
|
+
const key = modelKey({ provider: model.provider, model: model.id });
|
|
108
|
+
const matchedScopedPatterns = scopedMatches.get(key) ?? [];
|
|
109
|
+
return {
|
|
110
|
+
provider: model.provider,
|
|
111
|
+
model: model.id,
|
|
112
|
+
ref: formatModelRef(model),
|
|
113
|
+
name: model.name,
|
|
114
|
+
available: availableKeys.has(key),
|
|
115
|
+
scoped: matchedScopedPatterns.length > 0,
|
|
116
|
+
matchedScopedPatterns,
|
|
117
|
+
default: model.provider === defaultProvider && model.id === defaultModel,
|
|
118
|
+
contextWindow: model.contextWindow,
|
|
119
|
+
maxTokens: model.maxTokens,
|
|
120
|
+
thinking: Boolean(model.reasoning),
|
|
121
|
+
images: model.input.includes("image"),
|
|
122
|
+
};
|
|
123
|
+
})
|
|
124
|
+
.filter((model) => !options.scopedOnly || model.scoped)
|
|
125
|
+
.filter((model) => matchesSearch(model, options.search))
|
|
126
|
+
.sort((a, b) => a.provider.localeCompare(b.provider) || a.model.localeCompare(b.model));
|
|
127
|
+
const diagnostics = [...services.diagnostics];
|
|
128
|
+
const loadError = services.modelRegistry.getError();
|
|
129
|
+
if (loadError)
|
|
130
|
+
diagnostics.push({ type: "warning", message: `errors loading models.json: ${loadError}` });
|
|
131
|
+
if (options.scopedOnly && enabledModelPatterns.length === 0) {
|
|
132
|
+
diagnostics.push({ type: "info", message: "No Pi enabledModels patterns are configured for this folder." });
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
ok: true,
|
|
136
|
+
folder: services.cwd,
|
|
137
|
+
search: options.search,
|
|
138
|
+
includeAll: Boolean(options.includeAll),
|
|
139
|
+
scopedOnly: Boolean(options.scopedOnly),
|
|
140
|
+
enabledModelPatterns,
|
|
141
|
+
defaultModel: defaultProvider || defaultModel || defaultThinkingLevel
|
|
142
|
+
? { provider: defaultProvider, model: defaultModel, thinkingLevel: defaultThinkingLevel }
|
|
143
|
+
: undefined,
|
|
144
|
+
scopedResolution: {
|
|
145
|
+
approximate: enabledModelPatterns.length > 0,
|
|
146
|
+
note: enabledModelPatterns.length > 0 ? SCOPED_RESOLUTION_NOTE : undefined,
|
|
147
|
+
},
|
|
148
|
+
diagnostics,
|
|
149
|
+
models: rows,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
export async function listModels(options) {
|
|
153
|
+
return listModelsFromServices(await createAgentSessionServices({ cwd: options.cwd }), options);
|
|
154
|
+
}
|
|
155
|
+
function formatTokenCount(count) {
|
|
156
|
+
if (count >= 1_000_000) {
|
|
157
|
+
const millions = count / 1_000_000;
|
|
158
|
+
return millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M`;
|
|
159
|
+
}
|
|
160
|
+
if (count >= 1_000) {
|
|
161
|
+
const thousands = count / 1_000;
|
|
162
|
+
return thousands % 1 === 0 ? `${thousands}K` : `${thousands.toFixed(1)}K`;
|
|
163
|
+
}
|
|
164
|
+
return String(count);
|
|
165
|
+
}
|
|
166
|
+
function pad(value, width) {
|
|
167
|
+
return value.padEnd(width);
|
|
168
|
+
}
|
|
169
|
+
export function printModelList(result) {
|
|
170
|
+
for (const diagnostic of result.diagnostics) {
|
|
171
|
+
const prefix = diagnostic.type === "error" ? "Error" : diagnostic.type === "warning" ? "Warning" : "Info";
|
|
172
|
+
console.error(`${prefix}: ${diagnostic.message}`);
|
|
173
|
+
}
|
|
174
|
+
if (result.scopedOnly && result.scopedResolution.note)
|
|
175
|
+
console.error(`Note: ${result.scopedResolution.note}`);
|
|
176
|
+
if (!result.models.length) {
|
|
177
|
+
console.log(result.search ? `No models matching ${JSON.stringify(result.search)}` : "No models found");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const rows = result.models.map((model) => ({
|
|
181
|
+
provider: model.provider,
|
|
182
|
+
model: model.model,
|
|
183
|
+
available: model.available ? "yes" : "no",
|
|
184
|
+
scoped: model.scoped ? "yes" : "no",
|
|
185
|
+
default: model.default ? "yes" : "no",
|
|
186
|
+
context: formatTokenCount(model.contextWindow),
|
|
187
|
+
maxOut: formatTokenCount(model.maxTokens),
|
|
188
|
+
thinking: model.thinking ? "yes" : "no",
|
|
189
|
+
images: model.images ? "yes" : "no",
|
|
190
|
+
}));
|
|
191
|
+
const headers = {
|
|
192
|
+
provider: "provider",
|
|
193
|
+
model: "model",
|
|
194
|
+
available: "auth",
|
|
195
|
+
scoped: "scoped",
|
|
196
|
+
default: "default",
|
|
197
|
+
context: "context",
|
|
198
|
+
maxOut: "max-out",
|
|
199
|
+
thinking: "thinking",
|
|
200
|
+
images: "images",
|
|
201
|
+
};
|
|
202
|
+
const order = Object.keys(headers);
|
|
203
|
+
const widths = Object.fromEntries(order.map((key) => [key, Math.max(headers[key].length, ...rows.map((row) => row[key].length))]));
|
|
204
|
+
const formatRow = (row) => order.map((key) => pad(row[key], widths[key])).join(" ");
|
|
205
|
+
console.log(formatRow(headers));
|
|
206
|
+
for (const row of rows)
|
|
207
|
+
console.log(formatRow(row));
|
|
208
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type ModelSelection, type ThinkingLevel } from "./types.js";
|
|
3
|
+
type AgentSessionInstance = Awaited<ReturnType<typeof createAgentSession>>["session"];
|
|
4
|
+
type ModelRegistry = AgentSessionInstance["modelRegistry"];
|
|
5
|
+
type PiModel = ReturnType<ModelRegistry["getAll"]>[number];
|
|
6
|
+
export interface SessionContextSnapshot {
|
|
7
|
+
messages: unknown[];
|
|
8
|
+
model: {
|
|
9
|
+
provider: string;
|
|
10
|
+
modelId: string;
|
|
11
|
+
} | null;
|
|
12
|
+
thinkingLevel: string;
|
|
13
|
+
hasThinkingEntry: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface ResolvedModelSelection {
|
|
16
|
+
model?: PiModel;
|
|
17
|
+
thinkingLevel?: ThinkingLevel;
|
|
18
|
+
explicitModel: boolean;
|
|
19
|
+
explicitThinking: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function isThinkingLevel(value: string | undefined): value is ThinkingLevel;
|
|
22
|
+
export declare function hasModelSelection(selection: ModelSelection | undefined): selection is ModelSelection;
|
|
23
|
+
export declare function formatModelRef(model: {
|
|
24
|
+
provider: string;
|
|
25
|
+
id: string;
|
|
26
|
+
}): string;
|
|
27
|
+
export declare function modelMatches(model: {
|
|
28
|
+
provider: string;
|
|
29
|
+
id: string;
|
|
30
|
+
} | undefined, other: {
|
|
31
|
+
provider: string;
|
|
32
|
+
id: string;
|
|
33
|
+
}): boolean;
|
|
34
|
+
export declare function splitModelThinking(modelRef: string): {
|
|
35
|
+
modelRef: string;
|
|
36
|
+
thinkingLevel?: ThinkingLevel;
|
|
37
|
+
};
|
|
38
|
+
export declare function modelRefHasThinkingSuffix(modelRef: string | undefined): boolean;
|
|
39
|
+
export declare function mergeModelSelection(base: ModelSelection | undefined, override: ModelSelection | undefined): ModelSelection | undefined;
|
|
40
|
+
export declare function resolveModelRef(modelRegistry: ModelRegistry, requested: string, providerInput: string | undefined): PiModel;
|
|
41
|
+
export declare function resolveRequestedModelSelection(modelRegistry: ModelRegistry, selection: ModelSelection | undefined): ResolvedModelSelection;
|
|
42
|
+
export declare function getSessionContextSnapshot(sessionManager: SessionManager): SessionContextSnapshot;
|
|
43
|
+
export declare function restoreModelFromSession(modelRegistry: ModelRegistry, snapshot: SessionContextSnapshot): PiModel | undefined;
|
|
44
|
+
export declare function resolveSessionModelSelection(modelRegistry: ModelRegistry, snapshot: SessionContextSnapshot, selection: ModelSelection | undefined): ResolvedModelSelection;
|
|
45
|
+
export declare function persistThinkingLevelIfNeeded(session: AgentSessionInstance, thinkingLevel: ThinkingLevel): void;
|
|
46
|
+
export declare function persistExplicitSelectionIfNeeded(session: AgentSessionInstance, snapshot: SessionContextSnapshot, resolved: ResolvedModelSelection): Promise<void>;
|
|
47
|
+
export {};
|