@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.
@@ -0,0 +1,171 @@
1
+ import { THINKING_LEVELS } from "./types.js";
2
+ export function isThinkingLevel(value) {
3
+ return typeof value === "string" && THINKING_LEVELS.includes(value);
4
+ }
5
+ export function hasModelSelection(selection) {
6
+ return Boolean(selection?.provider || selection?.model || selection?.thinkingLevel);
7
+ }
8
+ export function formatModelRef(model) {
9
+ return `${model.provider}/${model.id}`;
10
+ }
11
+ export function modelMatches(model, other) {
12
+ return model?.provider === other.provider && model.id === other.id;
13
+ }
14
+ export function splitModelThinking(modelRef) {
15
+ const colonIndex = modelRef.lastIndexOf(":");
16
+ if (colonIndex > 0) {
17
+ const suffix = modelRef.slice(colonIndex + 1);
18
+ if (isThinkingLevel(suffix))
19
+ return { modelRef: modelRef.slice(0, colonIndex), thinkingLevel: suffix };
20
+ }
21
+ return { modelRef };
22
+ }
23
+ export function modelRefHasThinkingSuffix(modelRef) {
24
+ return Boolean(modelRef && splitModelThinking(modelRef).thinkingLevel);
25
+ }
26
+ export function mergeModelSelection(base, override) {
27
+ if (!base)
28
+ return override;
29
+ if (!override)
30
+ return base;
31
+ if (!override.model && !override.provider)
32
+ return { ...base, thinkingLevel: override.thinkingLevel ?? base.thinkingLevel };
33
+ return {
34
+ provider: override.provider,
35
+ model: override.model,
36
+ thinkingLevel: override.thinkingLevel ?? (modelRefHasThinkingSuffix(override.model) ? undefined : base.thinkingLevel),
37
+ };
38
+ }
39
+ function selectUniqueModel(matches, requested) {
40
+ if (matches.length === 0)
41
+ return undefined;
42
+ if (matches.length === 1)
43
+ return matches[0];
44
+ const examples = matches.slice(0, 8).map(formatModelRef).join(", ");
45
+ const suffix = matches.length > 8 ? `, ... ${matches.length - 8} more` : "";
46
+ throw new Error(`Model ${JSON.stringify(requested)} is ambiguous. Use provider/model. Matches: ${examples}${suffix}`);
47
+ }
48
+ export function resolveModelRef(modelRegistry, requested, providerInput) {
49
+ const models = modelRegistry.getAll();
50
+ if (models.length === 0)
51
+ throw new Error("No models available. Check your Pi installation or models.json.");
52
+ const providerMap = new Map();
53
+ for (const model of models)
54
+ providerMap.set(model.provider.toLowerCase(), model.provider);
55
+ let provider = providerInput ? providerMap.get(providerInput.toLowerCase()) : undefined;
56
+ if (providerInput && !provider) {
57
+ throw new Error(`Unknown provider ${JSON.stringify(providerInput)}. Use \`pi --list-models\` to see available providers/models.`);
58
+ }
59
+ let pattern = requested;
60
+ if (provider && requested.toLowerCase().startsWith(`${provider.toLowerCase()}/`)) {
61
+ pattern = requested.slice(provider.length + 1);
62
+ }
63
+ if (!provider) {
64
+ const slashIndex = requested.indexOf("/");
65
+ if (slashIndex !== -1) {
66
+ const maybeProvider = requested.slice(0, slashIndex);
67
+ const canonical = providerMap.get(maybeProvider.toLowerCase());
68
+ if (canonical) {
69
+ provider = canonical;
70
+ pattern = requested.slice(slashIndex + 1);
71
+ }
72
+ }
73
+ }
74
+ const candidates = provider ? models.filter((model) => model.provider === provider) : models;
75
+ const normalized = pattern.toLowerCase();
76
+ const exact = selectUniqueModel(candidates.filter((model) => {
77
+ const id = model.id.toLowerCase();
78
+ const canonical = formatModelRef(model).toLowerCase();
79
+ return id === normalized || canonical === requested.toLowerCase();
80
+ }), requested);
81
+ if (exact)
82
+ return exact;
83
+ const fuzzy = selectUniqueModel(candidates.filter((model) => model.id.toLowerCase().includes(normalized) || model.name?.toLowerCase().includes(normalized)), requested);
84
+ if (fuzzy)
85
+ return fuzzy;
86
+ const display = provider ? `${provider}/${pattern}` : requested;
87
+ throw new Error(`Model ${JSON.stringify(display)} not found. Use \`pi --list-models\` to see available models.`);
88
+ }
89
+ export function resolveRequestedModelSelection(modelRegistry, selection) {
90
+ const provider = selection?.provider?.trim();
91
+ const modelInput = selection?.model?.trim();
92
+ if (provider && !modelInput)
93
+ throw new Error("--provider requires --model.");
94
+ let model;
95
+ const requestedThinking = selection?.thinkingLevel;
96
+ if (requestedThinking !== undefined && !isThinkingLevel(String(requestedThinking))) {
97
+ throw new Error(`Invalid --thinking ${JSON.stringify(requestedThinking)}. Expected: ${THINKING_LEVELS.join(", ")}.`);
98
+ }
99
+ let thinkingLevel = requestedThinking;
100
+ let explicitThinking = thinkingLevel !== undefined;
101
+ if (modelInput) {
102
+ try {
103
+ model = resolveModelRef(modelRegistry, modelInput, provider);
104
+ }
105
+ catch (error) {
106
+ const split = splitModelThinking(modelInput);
107
+ if (split.thinkingLevel === undefined)
108
+ throw error;
109
+ const modelRef = split.modelRef.trim();
110
+ if (!modelRef)
111
+ throw new Error("--model requires a non-empty value.");
112
+ model = resolveModelRef(modelRegistry, modelRef, provider);
113
+ thinkingLevel = thinkingLevel ?? split.thinkingLevel;
114
+ explicitThinking = true;
115
+ }
116
+ if (!modelRegistry.hasConfiguredAuth(model)) {
117
+ throw new Error(`No API key/auth configured for ${formatModelRef(model)}.`);
118
+ }
119
+ }
120
+ return {
121
+ model,
122
+ thinkingLevel,
123
+ explicitModel: modelInput !== undefined,
124
+ explicitThinking,
125
+ };
126
+ }
127
+ export function getSessionContextSnapshot(sessionManager) {
128
+ const context = sessionManager.buildSessionContext();
129
+ return {
130
+ messages: context.messages,
131
+ model: context.model,
132
+ thinkingLevel: context.thinkingLevel,
133
+ hasThinkingEntry: sessionManager.getBranch().some((entry) => entry.type === "thinking_level_change"),
134
+ };
135
+ }
136
+ export function restoreModelFromSession(modelRegistry, snapshot) {
137
+ if (!snapshot.model)
138
+ return undefined;
139
+ const model = modelRegistry.find(snapshot.model.provider, snapshot.model.modelId);
140
+ return model && modelRegistry.hasConfiguredAuth(model) ? model : undefined;
141
+ }
142
+ export function resolveSessionModelSelection(modelRegistry, snapshot, selection) {
143
+ const resolved = resolveRequestedModelSelection(modelRegistry, selection);
144
+ if (!resolved.model && !resolved.explicitModel)
145
+ resolved.model = restoreModelFromSession(modelRegistry, snapshot);
146
+ if (!resolved.thinkingLevel && !resolved.explicitThinking && snapshot.hasThinkingEntry && isThinkingLevel(snapshot.thinkingLevel)) {
147
+ resolved.thinkingLevel = snapshot.thinkingLevel;
148
+ }
149
+ return resolved;
150
+ }
151
+ export function persistThinkingLevelIfNeeded(session, thinkingLevel) {
152
+ if (session.thinkingLevel !== thinkingLevel) {
153
+ session.setThinkingLevel(thinkingLevel);
154
+ return;
155
+ }
156
+ if (session.sessionManager.buildSessionContext().thinkingLevel !== thinkingLevel) {
157
+ session.sessionManager.appendThinkingLevelChange(thinkingLevel);
158
+ }
159
+ }
160
+ export async function persistExplicitSelectionIfNeeded(session, snapshot, resolved) {
161
+ const hadMessages = snapshot.messages.length > 0;
162
+ if (hadMessages && resolved.explicitModel && resolved.model) {
163
+ const saved = snapshot.model;
164
+ if (!saved || saved.provider !== resolved.model.provider || saved.modelId !== resolved.model.id) {
165
+ await session.setModel(resolved.model);
166
+ }
167
+ }
168
+ if (hadMessages && resolved.explicitThinking && resolved.thinkingLevel && snapshot.thinkingLevel !== resolved.thinkingLevel) {
169
+ persistThinkingLevelIfNeeded(session, resolved.thinkingLevel);
170
+ }
171
+ }
@@ -0,0 +1,39 @@
1
+ import type { DeliveryMode, ModelSelection } from "./types.js";
2
+ export { sendToLiveSocket } from "./live-socket.js";
3
+ interface RunHeadlessTurnOptions {
4
+ cwd: string;
5
+ sessionFile?: string;
6
+ message: string;
7
+ delivery?: DeliveryMode;
8
+ stream?: boolean;
9
+ modelSelection?: ModelSelection;
10
+ }
11
+ interface RunInteractiveOptions {
12
+ cwd: string;
13
+ sessionFile?: string;
14
+ name?: string;
15
+ initialMessage?: string;
16
+ socketPath?: string;
17
+ modelSelection?: ModelSelection;
18
+ onSessionFile?: (sessionFile: string | undefined, rawSessionId: string) => Promise<void> | void;
19
+ onMaterialized?: () => Promise<void> | void;
20
+ onStatus?: (status: "running" | "idle" | "busy" | "offline" | "error", error?: string) => Promise<void> | void;
21
+ }
22
+ interface CreatePersistentSessionOptions {
23
+ cwd: string;
24
+ name?: string;
25
+ modelSelection?: ModelSelection;
26
+ }
27
+ export declare function validateModelSelection(options: {
28
+ cwd: string;
29
+ modelSelection?: ModelSelection;
30
+ }): Promise<ModelSelection | undefined>;
31
+ export declare function createPersistentSession(options: CreatePersistentSessionOptions): Promise<{
32
+ sessionFile?: string;
33
+ rawSessionId: string;
34
+ }>;
35
+ export declare function runHeadlessTurn(options: RunHeadlessTurnOptions): Promise<{
36
+ sessionFile?: string;
37
+ rawSessionId: string;
38
+ }>;
39
+ export declare function runInteractive(options: RunInteractiveOptions): Promise<void>;
@@ -0,0 +1,182 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, InteractiveMode, SessionManager, } from "@earendil-works/pi-coding-agent";
4
+ import { getSessionContextSnapshot, hasModelSelection, isThinkingLevel, modelMatches, persistExplicitSelectionIfNeeded, persistThinkingLevelIfNeeded, resolveRequestedModelSelection, resolveSessionModelSelection, } from "./model-selection.js";
5
+ import { startSessionSocket } from "./live-socket.js";
6
+ export { sendToLiveSocket } from "./live-socket.js";
7
+ async function createAgentSessionWithModelSelection(options) {
8
+ const snapshot = getSessionContextSnapshot(options.sessionManager);
9
+ const resolved = resolveSessionModelSelection(options.services.modelRegistry, snapshot, options.modelSelection);
10
+ const thinkingLevelForCreate = snapshot.messages.length > 0 && resolved.explicitThinking && snapshot.hasThinkingEntry && isThinkingLevel(snapshot.thinkingLevel)
11
+ ? snapshot.thinkingLevel
12
+ : resolved.thinkingLevel;
13
+ const created = await createAgentSessionFromServices({
14
+ services: options.services,
15
+ sessionManager: options.sessionManager,
16
+ sessionStartEvent: options.sessionStartEvent,
17
+ model: resolved.model,
18
+ thinkingLevel: thinkingLevelForCreate,
19
+ });
20
+ await persistExplicitSelectionIfNeeded(created.session, snapshot, resolved);
21
+ return created;
22
+ }
23
+ async function applyModelSelectionToLiveSession(session, selection) {
24
+ if (!hasModelSelection(selection))
25
+ return;
26
+ const resolved = resolveRequestedModelSelection(session.modelRegistry, selection);
27
+ if (resolved.model && !modelMatches(session.model, resolved.model))
28
+ await session.setModel(resolved.model);
29
+ if (resolved.thinkingLevel)
30
+ persistThinkingLevelIfNeeded(session, resolved.thinkingLevel);
31
+ }
32
+ export async function validateModelSelection(options) {
33
+ if (!hasModelSelection(options.modelSelection))
34
+ return undefined;
35
+ const services = await createAgentSessionServices({ cwd: options.cwd });
36
+ const resolved = resolveRequestedModelSelection(services.modelRegistry, options.modelSelection);
37
+ return {
38
+ provider: resolved.model?.provider,
39
+ model: resolved.model?.id,
40
+ thinkingLevel: resolved.thinkingLevel,
41
+ };
42
+ }
43
+ async function deliverToSession(session, message, delivery = "auto") {
44
+ if (delivery === "auto") {
45
+ if (session.isStreaming)
46
+ await session.followUp(message);
47
+ else
48
+ await session.prompt(message);
49
+ return;
50
+ }
51
+ if (delivery === "steer") {
52
+ if (session.isStreaming)
53
+ await session.steer(message);
54
+ else
55
+ await session.prompt(message);
56
+ return;
57
+ }
58
+ if (delivery === "follow-up") {
59
+ if (session.isStreaming)
60
+ await session.followUp(message);
61
+ else
62
+ await session.prompt(message);
63
+ return;
64
+ }
65
+ if (delivery === "prompt") {
66
+ if (session.isStreaming) {
67
+ throw new Error("Target session is streaming; use --delivery steer, follow-up, or auto.");
68
+ }
69
+ await session.prompt(message);
70
+ return;
71
+ }
72
+ throw new Error(`Unsupported delivery mode: ${delivery}`);
73
+ }
74
+ export async function createPersistentSession(options) {
75
+ const sessionManager = SessionManager.create(options.cwd);
76
+ if (options.name)
77
+ sessionManager.appendSessionInfo(options.name);
78
+ const services = await createAgentSessionServices({ cwd: options.cwd });
79
+ const { session } = await createAgentSessionWithModelSelection({ services, sessionManager, modelSelection: options.modelSelection });
80
+ try {
81
+ return { sessionFile: session.sessionFile, rawSessionId: session.sessionId };
82
+ }
83
+ finally {
84
+ session.dispose();
85
+ }
86
+ }
87
+ async function fileExists(filePath) {
88
+ if (!filePath)
89
+ return false;
90
+ try {
91
+ await fs.access(filePath);
92
+ return true;
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ export async function runHeadlessTurn(options) {
99
+ const sessionManager = options.sessionFile && (await fileExists(options.sessionFile))
100
+ ? SessionManager.open(options.sessionFile)
101
+ : SessionManager.create(options.cwd);
102
+ const services = await createAgentSessionServices({ cwd: options.cwd });
103
+ const { session } = await createAgentSessionWithModelSelection({ services, sessionManager, modelSelection: options.modelSelection });
104
+ try {
105
+ if (options.stream) {
106
+ session.subscribe((event) => {
107
+ if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
108
+ process.stdout.write(event.assistantMessageEvent.delta);
109
+ }
110
+ });
111
+ }
112
+ await deliverToSession(session, options.message, options.delivery ?? "auto");
113
+ await session.agent.waitForIdle();
114
+ if (options.stream)
115
+ process.stdout.write("\n");
116
+ return { sessionFile: session.sessionFile, rawSessionId: session.sessionId };
117
+ }
118
+ finally {
119
+ session.dispose();
120
+ }
121
+ }
122
+ function createRuntimeFactory(modelSelection) {
123
+ return async ({ cwd, sessionManager, sessionStartEvent }) => {
124
+ const services = await createAgentSessionServices({ cwd });
125
+ return {
126
+ ...(await createAgentSessionWithModelSelection({ services, sessionManager, sessionStartEvent, modelSelection })),
127
+ services,
128
+ diagnostics: services.diagnostics,
129
+ };
130
+ };
131
+ }
132
+ export async function runInteractive(options) {
133
+ const sessionManager = options.sessionFile
134
+ ? SessionManager.open(options.sessionFile)
135
+ : SessionManager.create(options.cwd);
136
+ if (options.name)
137
+ sessionManager.appendSessionInfo(options.name);
138
+ const runtime = await createAgentSessionRuntime(createRuntimeFactory(options.modelSelection), {
139
+ cwd: options.cwd,
140
+ agentDir: getAgentDir(),
141
+ sessionManager,
142
+ });
143
+ let server;
144
+ let unsubscribeMaterialized;
145
+ try {
146
+ await options.onSessionFile?.(runtime.session.sessionFile, runtime.session.sessionId);
147
+ await options.onStatus?.("running");
148
+ unsubscribeMaterialized = runtime.session.subscribe((event) => {
149
+ if (event.type === "agent_end" && !event.willRetry)
150
+ void options.onMaterialized?.();
151
+ });
152
+ if (options.socketPath) {
153
+ server = await startSessionSocket({
154
+ socketPath: options.socketPath,
155
+ getSession: () => runtime.session,
156
+ applyModelSelection: applyModelSelectionToLiveSession,
157
+ deliverToSession,
158
+ onStatus: options.onStatus,
159
+ });
160
+ }
161
+ const mode = new InteractiveMode(runtime, {
162
+ migratedProviders: [],
163
+ modelFallbackMessage: undefined,
164
+ initialMessage: options.initialMessage,
165
+ initialImages: [],
166
+ initialMessages: [],
167
+ });
168
+ await mode.run();
169
+ }
170
+ finally {
171
+ unsubscribeMaterialized?.();
172
+ await options.onStatus?.("offline");
173
+ const activeServer = server;
174
+ if (activeServer)
175
+ await new Promise((resolve) => activeServer.close(() => resolve()));
176
+ if (options.socketPath) {
177
+ await fs.rm(options.socketPath, { force: true }).catch(() => undefined);
178
+ await fs.rmdir(path.dirname(options.socketPath)).catch(() => undefined);
179
+ }
180
+ await runtime.dispose();
181
+ }
182
+ }
@@ -0,0 +1,57 @@
1
+ import type { AgentMessageLike, ContentPartLike, SessionData, SessionSummary, ToolInvocation, TranscriptEntry, TranscriptEvent, Turn } from "./types.js";
2
+ interface RegistryHint {
3
+ sessionId?: string;
4
+ sessionFile?: string;
5
+ sessionName?: string;
6
+ workflowId?: string;
7
+ providerId?: string;
8
+ laneId?: string;
9
+ [key: string]: unknown;
10
+ }
11
+ interface RegistryHints {
12
+ bySessionId: Map<string, RegistryHint & {
13
+ _recordPath: string;
14
+ }>;
15
+ bySessionFile: Map<string, RegistryHint & {
16
+ _recordPath: string;
17
+ }>;
18
+ }
19
+ export interface SessionSearchOptions {
20
+ query?: string;
21
+ limit?: number;
22
+ sessionRoot?: string;
23
+ registryRoots?: string[];
24
+ }
25
+ export interface TimeWindow {
26
+ since?: number | null;
27
+ until?: number | null;
28
+ }
29
+ export declare function contentText(part: ContentPartLike, options?: {
30
+ includeThinking?: boolean;
31
+ }): string;
32
+ export declare function messageText(message: AgentMessageLike | undefined, options?: {
33
+ includeThinking?: boolean;
34
+ }): string;
35
+ export declare function parseSessionSummary(filePath: string, hints?: RegistryHints): Promise<SessionSummary | null>;
36
+ export declare function matchesSessionSummary(item: SessionSummary, query?: string): boolean;
37
+ export declare function searchSessions(options?: SessionSearchOptions): Promise<SessionSummary[]>;
38
+ export declare function resolveSessionSpec(spec: string, options?: SessionSearchOptions): Promise<SessionSummary>;
39
+ export declare function loadActiveBranch(sessionFile: string): Promise<{
40
+ header: {
41
+ id: string;
42
+ version?: number;
43
+ cwd?: string;
44
+ timestamp?: string;
45
+ };
46
+ entries: TranscriptEntry[];
47
+ }>;
48
+ export declare function normalizeEvents(entries: TranscriptEntry[]): TranscriptEvent[];
49
+ export declare function summarizeArgs(toolName: string | undefined, args: Record<string, unknown> | undefined): string;
50
+ export declare function buildToolInvocations(events: TranscriptEvent[]): ToolInvocation[];
51
+ export declare function buildTurns(events: TranscriptEvent[]): Turn[];
52
+ export declare function inWindow(timestampMs: number | null | undefined, window?: TimeWindow): boolean;
53
+ export declare function tail<T>(items: T[], count: number): T[];
54
+ export declare function loadSessionDataForSession(session: SessionSummary): Promise<SessionData>;
55
+ export declare function loadSessionData(spec: string, options?: SessionSearchOptions): Promise<SessionData>;
56
+ export declare function renderToolLine(item: ToolInvocation): string;
57
+ export {};