localpi 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,225 @@
1
+ import { ensureLlamaServer, getManagedLlamaServerMetadata, getLlamaServerModels, llamaBaseUrl, managedLlamaServerNeedsRestart, stopManagedLlamaServer } from "./llama-server.js";
2
+ import { managedModelSupportsReasoning } from "./catalog.js";
3
+ import { catalogModelFromModelInfo, catalogRuntimeConnection, connectionCatalogModels, modelChoiceList, optionalContextWindow, replaceManagedLoadedModels } from "./runtime-connection.js";
4
+ import { defaultLlamaModelName, findModelAlias, resolveLlamaModel } from "./models.js";
5
+ export async function resolveLlamaRuntime(options) {
6
+ const requested = options.model ?? defaultLlamaModelName();
7
+ const existing = await existingLlamaRuntime(options, requested);
8
+ if (existing !== undefined) {
9
+ return existing;
10
+ }
11
+ const model = await resolveLlamaModelForStart(requested, options);
12
+ const modelForStart = llamaModelForStart(model, options);
13
+ await assertDirectLlamaStartIsSafe(options, modelForStart);
14
+ const runtime = await ensureLlamaServer(options, modelForStart);
15
+ return {
16
+ runtime: runtime.managed ? "llama-server" : "llama-server/external",
17
+ providerId: "llama-server",
18
+ providerName: "llama-server",
19
+ baseUrl: runtime.baseUrl,
20
+ model: runtime.model,
21
+ availableModels: runtime.availableModels,
22
+ catalogModels: connectionCatalogModels("llama-server", "llama-server", "managed-llama-server", runtime.baseUrl, runtime.availableModels, options, runtime.contextWindow),
23
+ warnings: runtime.warnings,
24
+ ...optionalContextWindow(options.contextWindow ?? runtime.contextWindow)
25
+ };
26
+ }
27
+ export async function resolveSelectedLlamaRuntime(options, selected, catalog) {
28
+ const existing = selected.availability === "loaded"
29
+ ? await existingLlamaRuntime(options, selected.modelId)
30
+ : undefined;
31
+ if (existing !== undefined) {
32
+ const selectedModel = managedCatalogModelFromConnection(options, selected, existing);
33
+ return catalogRuntimeConnection(options, selectedModel, {
34
+ models: replaceManagedLoadedModels(catalog.models, selected, existing.catalogModels),
35
+ warnings: [...catalog.warnings, ...existing.warnings]
36
+ });
37
+ }
38
+ return startSelectedLlamaRuntime(options, selected, catalog);
39
+ }
40
+ export async function customPathCatalogModel(options, provider, requested) {
41
+ if (options.runtime !== "auto" ||
42
+ (provider !== undefined && provider !== "llama-server") ||
43
+ !isGgufPathRequest(requested)) {
44
+ return undefined;
45
+ }
46
+ const resolved = await resolveLlamaModelForStart(requested, options);
47
+ return {
48
+ providerId: "llama-server",
49
+ providerName: "llama-server",
50
+ runtime: "managed-llama-server",
51
+ baseUrl: llamaBaseUrl(options),
52
+ modelId: resolved.id,
53
+ aliases: [requested],
54
+ displayName: `llama-server / ${resolved.name}`,
55
+ maxTokens: options.maxTokens,
56
+ reasoning: managedModelSupportsReasoning(resolved.id),
57
+ capabilities: ["text"],
58
+ availability: "startable",
59
+ ...optionalContextWindow(options.contextWindow ?? resolved.contextWindow)
60
+ };
61
+ }
62
+ async function resolveLlamaModelForStart(requested, options) {
63
+ try {
64
+ return await resolveLlamaModel(requested, options.chatTemplate);
65
+ }
66
+ catch (error) {
67
+ const managed = await getManagedLlamaServerMetadata(options);
68
+ if (managed !== undefined && (requested === "auto" || requested === managed.modelId)) {
69
+ return {
70
+ source: "path",
71
+ name: managed.modelId,
72
+ id: managed.modelId,
73
+ modelPath: managed.modelPath,
74
+ contextWindow: managed.contextWindow,
75
+ ...optionalChatTemplate(options.chatTemplate ?? managed.chatTemplate)
76
+ };
77
+ }
78
+ throw error;
79
+ }
80
+ }
81
+ async function existingLlamaRuntime(options, requested) {
82
+ const match = await existingModelMatch(options, requested);
83
+ if (match === undefined) {
84
+ return undefined;
85
+ }
86
+ const managed = await getManagedLlamaServerMetadata(options);
87
+ const reportedContextWindow = reportedRuntimeContext(managed, match);
88
+ if (managed !== undefined &&
89
+ shouldResolveManagedThroughStartPath(managed, options, requested, match.modelId)) {
90
+ return undefined;
91
+ }
92
+ if (shouldRestartManagedForContext(options, managed, reportedContextWindow)) {
93
+ return undefined;
94
+ }
95
+ assertCompatibleRuntimeContext(options, match.modelId, reportedContextWindow);
96
+ return existingRuntimeConnection(options, match, managed, reportedContextWindow);
97
+ }
98
+ function existingRuntimeConnection(options, match, managed, reportedContextWindow) {
99
+ return {
100
+ runtime: runtimeName(managed),
101
+ providerId: "llama-server",
102
+ providerName: "llama-server",
103
+ baseUrl: llamaBaseUrl(options),
104
+ model: match.modelId,
105
+ availableModels: match.models.map((model) => model.id),
106
+ catalogModels: match.models.map((model) => catalogModelFromModelInfo("llama-server", "llama-server", "managed-llama-server", llamaBaseUrl(options), model, options, model.id === match.modelId ? reportedContextWindow : model.contextWindow)),
107
+ warnings: [],
108
+ ...optionalContextWindow(options.contextWindow ?? reportedContextWindow)
109
+ };
110
+ }
111
+ function reportedRuntimeContext(managed, match) {
112
+ return managed?.contextWindow ?? match.info?.contextWindow;
113
+ }
114
+ function runtimeName(managed) {
115
+ return managed === undefined ? "llama-server/external" : "llama-server";
116
+ }
117
+ async function existingModelMatch(options, requested) {
118
+ const models = await getLlamaServerModels(options);
119
+ if (models === undefined) {
120
+ return undefined;
121
+ }
122
+ const modelId = await existingModelId(requested, models);
123
+ if (modelId === undefined) {
124
+ return undefined;
125
+ }
126
+ return {
127
+ models,
128
+ modelId,
129
+ info: models.find((model) => model.id === modelId)
130
+ };
131
+ }
132
+ function shouldRestartManagedForContext(options, managed, reportedContextWindow) {
133
+ return (managed !== undefined &&
134
+ options.contextWindow !== undefined &&
135
+ reportedContextWindow !== undefined &&
136
+ reportedContextWindow !== options.contextWindow);
137
+ }
138
+ function shouldResolveManagedThroughStartPath(managed, options, requested, modelId) {
139
+ return (managedLlamaServerNeedsRestart(options, managed) ||
140
+ (requested !== "auto" && requested !== modelId));
141
+ }
142
+ function assertCompatibleRuntimeContext(options, modelId, reportedContextWindow) {
143
+ if (options.contextWindow !== undefined &&
144
+ reportedContextWindow !== undefined &&
145
+ reportedContextWindow !== options.contextWindow) {
146
+ throw new Error(`server at ${llamaBaseUrl(options)} reports ${modelId} ctx=${String(reportedContextWindow)}, but --ctx ${String(options.contextWindow)} was requested`);
147
+ }
148
+ }
149
+ async function existingModelId(requested, models) {
150
+ if (requested === "auto") {
151
+ return models[0]?.id;
152
+ }
153
+ if (models.some((model) => model.id === requested)) {
154
+ return requested;
155
+ }
156
+ const alias = await findModelAlias(requested);
157
+ return alias !== undefined && models.some((model) => model.id === alias.id)
158
+ ? alias.id
159
+ : undefined;
160
+ }
161
+ function llamaModelForStart(model, options) {
162
+ return {
163
+ id: model.id,
164
+ modelPath: model.modelPath,
165
+ ...optionalContextWindow(options.contextWindow ?? model.contextWindow),
166
+ ...optionalChatTemplate(options.chatTemplate ?? model.chatTemplate)
167
+ };
168
+ }
169
+ async function startSelectedLlamaRuntime(options, selected, catalog) {
170
+ const model = await resolveLlamaModelForStart(selected.aliases[0] ?? selected.modelId, options);
171
+ const modelForStart = llamaModelForStart(model, options);
172
+ await stopStaleManagedLlamaServer(options, modelForStart);
173
+ assertNoLoadedExternalModels(catalog);
174
+ const runtime = await ensureLlamaServer(options, modelForStart);
175
+ const loadedSelected = runtimeSelectedCatalogModel(options, selected, runtime);
176
+ return catalogRuntimeConnection(options, loadedSelected, {
177
+ models: replaceManagedLoadedModels(catalog.models, selected, [loadedSelected]),
178
+ warnings: [...catalog.warnings, ...runtime.warnings]
179
+ });
180
+ }
181
+ async function assertDirectLlamaStartIsSafe(options, model) {
182
+ const { discoverModelCatalog } = await import("./catalog.js");
183
+ const catalog = await discoverModelCatalog({
184
+ ...options,
185
+ runtime: "auto",
186
+ provider: undefined,
187
+ model: "auto"
188
+ });
189
+ try {
190
+ assertNoLoadedExternalModels(catalog);
191
+ }
192
+ catch (error) {
193
+ await stopStaleManagedLlamaServer(options, model);
194
+ throw error;
195
+ }
196
+ }
197
+ async function stopStaleManagedLlamaServer(options, model) {
198
+ const managed = await getManagedLlamaServerMetadata(options);
199
+ if (managed !== undefined && managedLlamaServerNeedsRestart(options, managed, model)) {
200
+ await stopManagedLlamaServer(options);
201
+ }
202
+ }
203
+ function runtimeSelectedCatalogModel(options, selected, runtime) {
204
+ return catalogModelFromModelInfo(selected.providerId, selected.providerName, "managed-llama-server", runtime.baseUrl, runtime.contextWindow === undefined
205
+ ? { id: runtime.model }
206
+ : { id: runtime.model, contextWindow: runtime.contextWindow }, options, runtime.contextWindow);
207
+ }
208
+ function managedCatalogModelFromConnection(options, selected, connection) {
209
+ return catalogModelFromModelInfo(selected.providerId, selected.providerName, "managed-llama-server", connection.baseUrl, connection.contextWindow === undefined
210
+ ? { id: connection.model }
211
+ : { id: connection.model, contextWindow: connection.contextWindow }, options, connection.contextWindow);
212
+ }
213
+ function assertNoLoadedExternalModels(catalog) {
214
+ const external = catalog.models.filter((model) => model.runtime !== "managed-llama-server" && model.availability === "loaded");
215
+ if (external.length === 0) {
216
+ return;
217
+ }
218
+ throw new Error(`external local models are already loaded; choose one or unload them before starting llama-server:\n${modelChoiceList(external)}`);
219
+ }
220
+ function isGgufPathRequest(value) {
221
+ return value.endsWith(".gguf") || value.includes("/") || value.includes("\\");
222
+ }
223
+ function optionalChatTemplate(chatTemplate) {
224
+ return chatTemplate === undefined ? {} : { chatTemplate };
225
+ }
@@ -0,0 +1,169 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { asObject, optionalString } from "../common/json.js";
5
+ export async function listModelAliases(home = os.homedir()) {
6
+ return (await allAliases(home)).map((alias) => ({
7
+ name: alias.name,
8
+ id: alias.id,
9
+ paths: alias.paths,
10
+ ...(alias.contextWindow === undefined ? {} : { contextWindow: alias.contextWindow })
11
+ }));
12
+ }
13
+ export async function findModelAlias(name, home = os.homedir()) {
14
+ return (await allAliases(home)).find((entry) => entry.name === name);
15
+ }
16
+ export async function resolveLlamaModel(requested, chatTemplateOverride, home = os.homedir()) {
17
+ const expanded = expandHome(requested, home);
18
+ if (isGgufPath(expanded)) {
19
+ return customPathModel(expanded, chatTemplateOverride);
20
+ }
21
+ const alias = findRequestedAlias(await allAliases(home), requested);
22
+ if (alias === undefined) {
23
+ throw new Error(`unknown llama-server model alias ${requested}; pass a GGUF path or use --list`);
24
+ }
25
+ const modelPath = await firstExisting(alias.paths.map((candidate) => expandHome(candidate, home)));
26
+ if (modelPath === undefined) {
27
+ throw new Error(`model alias ${requested} has no installed GGUF; checked: ${alias.paths.join(", ")}`);
28
+ }
29
+ return {
30
+ source: "alias",
31
+ name: alias.name,
32
+ id: alias.id,
33
+ modelPath,
34
+ ...(alias.contextWindow === undefined ? {} : { contextWindow: alias.contextWindow }),
35
+ ...optionalChatTemplate(chatTemplateOverride ??
36
+ (await firstExisting((alias.chatTemplates ?? []).map(expandTemplate(home)))))
37
+ };
38
+ }
39
+ function findRequestedAlias(aliases, requested) {
40
+ return (aliases.find((entry) => entry.name === requested) ??
41
+ aliases.find((entry) => entry.id === requested));
42
+ }
43
+ export function defaultLlamaModelName() {
44
+ return "gemma-12b";
45
+ }
46
+ function customPathModel(modelPath, chatTemplate) {
47
+ return {
48
+ source: "path",
49
+ name: path.basename(modelPath, path.extname(modelPath)),
50
+ id: modelIdFromPath(modelPath),
51
+ modelPath,
52
+ ...optionalChatTemplate(chatTemplate)
53
+ };
54
+ }
55
+ async function allAliases(home) {
56
+ return [...(await configuredAliases(home)), ...builtInAliases(home)];
57
+ }
58
+ async function configuredAliases(home) {
59
+ const configPath = process.env["LOCALPI_MODELS_FILE"];
60
+ if (configPath === undefined) {
61
+ return [];
62
+ }
63
+ const raw = await readFile(expandHome(configPath, home), "utf8");
64
+ const root = asObject(JSON.parse(raw), "model alias config");
65
+ if (root["models"] === undefined) {
66
+ return [];
67
+ }
68
+ const models = asObject(root["models"], "model alias config models");
69
+ return Object.entries(models).map(([name, value]) => configuredAlias(name, value, home));
70
+ }
71
+ function configuredAlias(name, value, home) {
72
+ const entry = asObject(value, `model alias ${name}`);
73
+ const id = optionalString(entry["id"]) ?? name;
74
+ const primaryPath = optionalString(entry["path"]);
75
+ const paths = optionalStringArray(entry["paths"]);
76
+ const allPaths = [primaryPath, ...paths].filter((candidate) => candidate !== undefined);
77
+ if (allPaths.length === 0) {
78
+ throw new Error(`model alias ${name} must define path or paths`);
79
+ }
80
+ return {
81
+ name,
82
+ id,
83
+ paths: allPaths.map((candidate) => expandHome(candidate, home)),
84
+ ...optionalContextWindow(optionalPositiveInteger(entry["contextWindow"])),
85
+ chatTemplates: optionalStringArray(entry["chatTemplates"], optionalString(entry["chatTemplate"]))
86
+ };
87
+ }
88
+ function builtInAliases(home) {
89
+ return [
90
+ {
91
+ name: "gemma-12b",
92
+ id: "gemma-4-12b-it",
93
+ paths: [
94
+ "~/.lmstudio/models/lmstudio-community/gemma-4-12B-it-GGUF/gemma-4-12B-it-Q4_K_M.gguf",
95
+ "~/.lmstudio/models/unsloth/gemma-4-12b-it-GGUF/gemma-4-12b-it-Q4_K_M.gguf"
96
+ ],
97
+ contextWindow: 32768,
98
+ chatTemplates: [
99
+ "~/scratch/gemma12b-chat-template-efficiency/templates/source/gemma4-12b-lmstudio.jinja"
100
+ ]
101
+ },
102
+ {
103
+ name: "gemma-e4b",
104
+ id: "gemma-4-e4b-it",
105
+ paths: ["~/.lmstudio/models/ggml-org/gemma-4-E4B-it-GGUF/gemma-4-E4B-it-bf16.gguf"],
106
+ contextWindow: 32768,
107
+ chatTemplates: [
108
+ "~/scratch/gemma12b-chat-template-efficiency/templates/source/gemma4-e4b.jinja"
109
+ ]
110
+ },
111
+ {
112
+ name: "gemma-e2b",
113
+ id: "gemma-4-e2b-it",
114
+ paths: ["~/.lmstudio/models/ggml-org/gemma-4-E2B-it-GGUF/gemma-4-E2B-it-Q8_0.gguf"],
115
+ contextWindow: 32768,
116
+ chatTemplates: [
117
+ "~/scratch/gemma12b-chat-template-efficiency/templates/source/gemma4-e4b.jinja"
118
+ ]
119
+ }
120
+ ].map((alias) => ({
121
+ ...alias,
122
+ paths: alias.paths.map((candidate) => expandHome(candidate, home)),
123
+ chatTemplates: alias.chatTemplates.map((candidate) => expandHome(candidate, home))
124
+ }));
125
+ }
126
+ function optionalStringArray(value, first) {
127
+ const rest = Array.isArray(value) && value.every((entry) => typeof entry === "string")
128
+ ? value
129
+ : [];
130
+ return first === undefined ? rest : [first, ...rest];
131
+ }
132
+ function optionalPositiveInteger(value) {
133
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
134
+ }
135
+ function expandTemplate(home) {
136
+ return (candidate) => expandHome(candidate, home);
137
+ }
138
+ function expandHome(value, home) {
139
+ return value === "~" || value.startsWith("~/") ? path.join(home, value.slice(2)) : value;
140
+ }
141
+ function isGgufPath(value) {
142
+ return value.endsWith(".gguf") || value.includes("/") || value.includes("\\");
143
+ }
144
+ function optionalContextWindow(contextWindow) {
145
+ return contextWindow === undefined ? {} : { contextWindow };
146
+ }
147
+ function optionalChatTemplate(chatTemplate) {
148
+ return chatTemplate === undefined ? {} : { chatTemplate };
149
+ }
150
+ async function firstExisting(candidates) {
151
+ for (const candidate of candidates) {
152
+ if (await exists(candidate)) {
153
+ return candidate;
154
+ }
155
+ }
156
+ return undefined;
157
+ }
158
+ async function exists(candidate) {
159
+ try {
160
+ await access(candidate);
161
+ return true;
162
+ }
163
+ catch {
164
+ return false;
165
+ }
166
+ }
167
+ function modelIdFromPath(modelPath) {
168
+ return path.basename(modelPath, path.extname(modelPath)).replaceAll(/\s+/gu, "-").toLowerCase();
169
+ }
@@ -0,0 +1,240 @@
1
+ import path from "node:path";
2
+ import { normalizeBaseUrl } from "../llm/openai.js";
3
+ export const thinkingLevels = [
4
+ "off",
5
+ "minimal",
6
+ "low",
7
+ "medium",
8
+ "high",
9
+ "xhigh"
10
+ ];
11
+ export function defaultOptions() {
12
+ const home = envString("HOME", ".");
13
+ const stateDir = envString("LOCALPI_STATE_DIR", path.join(home, ".local/state/localpi"));
14
+ return {
15
+ runtime: parseRuntime(envString("LOCALPI_RUNTIME", "auto")),
16
+ baseUrl: envOptionalBaseUrl("LOCALPI_BASE_URL"),
17
+ model: process.env["LOCALPI_MODEL"],
18
+ provider: process.env["LOCALPI_PROVIDER"],
19
+ customProviderId: envString("LOCALPI_PROVIDER_ID", "local-openai"),
20
+ providersFile: process.env["LOCALPI_PROVIDERS_FILE"],
21
+ stateDir,
22
+ sessionDir: defaultSessionDir(stateDir),
23
+ piCommand: envString("LOCALPI_PI_CMD", "npx -y @earendil-works/pi-coding-agent@latest"),
24
+ thinking: parseThinkingLevel(envString("LOCALPI_THINKING", "off")),
25
+ contextWindow: envOptionalPositiveInteger("LOCALPI_CONTEXT_WINDOW"),
26
+ maxTokens: envPositiveInteger("LOCALPI_MAX_TOKENS", "8192"),
27
+ timeoutMs: envPositiveInteger("LOCALPI_TIMEOUT_MS", "3000"),
28
+ serverCommand: envString("LOCALPI_LLAMA_SERVER", "llama-server"),
29
+ host: envString("LOCALPI_HOST", "127.0.0.1"),
30
+ port: envPositiveInteger("LOCALPI_PORT", "18194"),
31
+ gpuLayers: envNonNegativeInteger("LOCALPI_GPU_LAYERS", "999"),
32
+ parallel: envPositiveInteger("LOCALPI_PARALLEL", "1"),
33
+ chatTemplate: process.env["LOCALPI_CHAT_TEMPLATE"],
34
+ tools: envString("LOCALPI_TOOLS", "read,bash,edit,write,grep,find,ls"),
35
+ approval: envBoolean("LOCALPI_APPROVAL", true),
36
+ tokenStatus: envBoolean("LOCALPI_TOKEN_STATUS", true),
37
+ status: false,
38
+ stop: false,
39
+ list: false,
40
+ forwardedArgs: []
41
+ };
42
+ }
43
+ export function parseLocalpiArgs(args) {
44
+ let options = defaultOptions();
45
+ const forwardedArgs = [];
46
+ for (let index = 0; index < args.length; index += 1) {
47
+ const arg = args[index];
48
+ if (arg === undefined) {
49
+ continue;
50
+ }
51
+ if (arg === "--") {
52
+ forwardedArgs.push(...args.slice(index + 1));
53
+ break;
54
+ }
55
+ if (arg === "-h" || arg === "--help") {
56
+ return { ...options, forwardedArgs: ["--help"] };
57
+ }
58
+ const parsed = parseLocalpiFlag(options, args, index);
59
+ if (parsed !== undefined) {
60
+ options = parsed.options;
61
+ index += parsed.advance;
62
+ continue;
63
+ }
64
+ forwardedArgs.push(arg);
65
+ }
66
+ return { ...options, forwardedArgs };
67
+ }
68
+ export function usage() {
69
+ return `${[
70
+ "localpi - Pi, automatically pointed at a local model",
71
+ "",
72
+ "usage:",
73
+ " localpi [localpi options] [pi options/messages]",
74
+ "",
75
+ "localpi options:",
76
+ " --runtime <kind> auto, llama-server, lmstudio, vllm, or openai-compatible",
77
+ " --provider <id> catalog provider id to use",
78
+ " --model <alias|id|path> model alias, backend id, or GGUF path",
79
+ " --base-url <url> OpenAI-compatible endpoint",
80
+ " --ctx <n> model context window",
81
+ " --context-window <n> alias for --ctx",
82
+ " --max-tokens <n> generated model max output tokens",
83
+ " --server-command <path> llama-server executable",
84
+ " --llama-server <path> alias for --server-command",
85
+ " --host <host> managed llama-server host",
86
+ " --port <n> managed llama-server port",
87
+ " --gpu-layers <n> llama-server GPU layers",
88
+ " --parallel <n> llama-server parallel slots",
89
+ " --chat-template <path> llama.cpp chat template file",
90
+ " --tools <list> Pi tools allow list",
91
+ " --providers-file <path> localpi provider registry JSON",
92
+ " --no-approval do not ask before tool calls",
93
+ " --no-token-status do not install token status extension",
94
+ " --status print runtime status and exit",
95
+ " --stop stop the localpi-owned llama-server",
96
+ " --list list model aliases",
97
+ " --state-dir <path> localpi runtime state directory",
98
+ " --session-dir <path> Pi session directory",
99
+ " --pi-command <command> Pi launch command",
100
+ " --thinking <level> thinking level: off, minimal, low, medium, high, xhigh",
101
+ " --timeout-ms <n> backend probe timeout",
102
+ " -h, --help show this help",
103
+ "",
104
+ "removed options:",
105
+ " --final-schema and --schema belong in localpager-agent, not localpi",
106
+ "",
107
+ "examples:",
108
+ " localpi --list",
109
+ " localpi --status",
110
+ ' localpi --model gemma-e4b -p "say ok"',
111
+ " localpi --runtime lmstudio --model gemma-4-e4b-it",
112
+ " localpi -- --help"
113
+ ].join("\n")}\n`;
114
+ }
115
+ function parseLocalpiFlag(options, args, index) {
116
+ const arg = args[index];
117
+ const booleanResult = arg === undefined ? undefined : parseBooleanFlag(options, arg);
118
+ if (booleanResult !== undefined) {
119
+ return booleanResult;
120
+ }
121
+ if (arg === "--schema" || arg === "--final-schema") {
122
+ throw new Error(`${arg} was removed from localpi; use localpager-agent for schema output`);
123
+ }
124
+ return arg === undefined ? undefined : parseValueFlag(options, args, index, arg);
125
+ }
126
+ function parseBooleanFlag(options, arg) {
127
+ const updater = booleanFlagUpdaters[arg];
128
+ return updater === undefined ? undefined : { options: updater(options), advance: 0 };
129
+ }
130
+ const booleanFlagUpdaters = {
131
+ "--status": (options) => ({ ...options, status: true }),
132
+ "--stop": (options) => ({ ...options, stop: true }),
133
+ "--list": (options) => ({ ...options, list: true }),
134
+ "--no-approval": (options) => ({ ...options, approval: false }),
135
+ "--no-token-status": (options) => ({ ...options, tokenStatus: false })
136
+ };
137
+ const valueFlagUpdaters = {
138
+ "--runtime": (options, value) => ({ ...options, runtime: parseRuntime(value) }),
139
+ "--base-url": (options, value) => ({ ...options, baseUrl: normalizeBaseUrl(value) }),
140
+ "--model": (options, value) => ({ ...options, model: value }),
141
+ "--provider": (options, value) => ({ ...options, provider: value }),
142
+ "--provider-id": (options, value) => ({ ...options, customProviderId: value }),
143
+ "--providers-file": (options, value) => ({ ...options, providersFile: value }),
144
+ "--state-dir": (options, value) => ({ ...options, stateDir: value }),
145
+ "--session-dir": (options, value) => ({ ...options, sessionDir: value }),
146
+ "--pi-command": (options, value) => ({ ...options, piCommand: value }),
147
+ "--thinking": (options, value) => ({ ...options, thinking: parseThinkingLevel(value) }),
148
+ "--ctx": (options, value) => ({ ...options, contextWindow: parsePositiveInteger(value) }),
149
+ "--context-window": (options, value) => ({
150
+ ...options,
151
+ contextWindow: parsePositiveInteger(value)
152
+ }),
153
+ "--max-tokens": (options, value) => ({ ...options, maxTokens: parsePositiveInteger(value) }),
154
+ "--timeout-ms": (options, value) => ({ ...options, timeoutMs: parsePositiveInteger(value) }),
155
+ "--server-command": (options, value) => ({ ...options, serverCommand: value }),
156
+ "--llama-server": (options, value) => ({ ...options, serverCommand: value }),
157
+ "--host": (options, value) => ({ ...options, host: value }),
158
+ "--port": (options, value) => ({ ...options, port: parsePositiveInteger(value) }),
159
+ "--gpu-layers": (options, value) => ({ ...options, gpuLayers: parseNonNegativeInteger(value) }),
160
+ "--parallel": (options, value) => ({ ...options, parallel: parsePositiveInteger(value) }),
161
+ "--chat-template": (options, value) => ({ ...options, chatTemplate: value }),
162
+ "--tools": (options, value) => ({ ...options, tools: value })
163
+ };
164
+ function parseValueFlag(options, args, index, flag) {
165
+ const updater = valueFlagUpdaters[flag];
166
+ if (updater === undefined) {
167
+ return undefined;
168
+ }
169
+ return { options: updater(options, requiredValue(args, index + 1, flag)), advance: 1 };
170
+ }
171
+ function parseRuntime(value) {
172
+ if (value === "auto" ||
173
+ value === "llama-server" ||
174
+ value === "lmstudio" ||
175
+ value === "vllm" ||
176
+ value === "openai-compatible") {
177
+ return value;
178
+ }
179
+ throw new Error(`unknown runtime ${value}; expected auto, llama-server, lmstudio, vllm, or openai-compatible`);
180
+ }
181
+ export function parseThinkingLevel(value) {
182
+ for (const level of thinkingLevels) {
183
+ if (value === level) {
184
+ return level;
185
+ }
186
+ }
187
+ throw new Error(`unknown thinking level ${value}; expected off, minimal, low, medium, high, or xhigh`);
188
+ }
189
+ function envString(name, fallback) {
190
+ return process.env[name] ?? fallback;
191
+ }
192
+ function envOptionalBaseUrl(name) {
193
+ const value = process.env[name];
194
+ return value === undefined ? undefined : normalizeBaseUrl(value);
195
+ }
196
+ function envPositiveInteger(name, fallback) {
197
+ return parsePositiveInteger(envString(name, fallback));
198
+ }
199
+ function envNonNegativeInteger(name, fallback) {
200
+ return parseNonNegativeInteger(envString(name, fallback));
201
+ }
202
+ function envOptionalPositiveInteger(name) {
203
+ const value = process.env[name];
204
+ return value === undefined ? undefined : parsePositiveInteger(value);
205
+ }
206
+ function envBoolean(name, fallback) {
207
+ const value = process.env[name];
208
+ if (value === undefined) {
209
+ return fallback;
210
+ }
211
+ if (["1", "true", "yes", "on"].includes(value.toLowerCase())) {
212
+ return true;
213
+ }
214
+ if (["0", "false", "no", "off"].includes(value.toLowerCase())) {
215
+ return false;
216
+ }
217
+ throw new Error(`${name} must be boolean-like, got ${value}`);
218
+ }
219
+ function defaultSessionDir(stateDir) {
220
+ return envString("LOCALPI_SESSION_DIR", envString("PI_CODING_AGENT_SESSION_DIR", path.join(stateDir, "sessions")));
221
+ }
222
+ function requiredValue(args, index, flag) {
223
+ const value = args[index];
224
+ if (value === undefined) {
225
+ throw new Error(`${flag} requires a value`);
226
+ }
227
+ return value;
228
+ }
229
+ function parsePositiveInteger(value) {
230
+ if (!/^[1-9]\d*$/u.test(value)) {
231
+ throw new Error(`expected a positive integer, got ${value}`);
232
+ }
233
+ return Number.parseInt(value, 10);
234
+ }
235
+ function parseNonNegativeInteger(value) {
236
+ if (!/^(0|[1-9]\d*)$/u.test(value)) {
237
+ throw new Error(`expected a non-negative integer, got ${value}`);
238
+ }
239
+ return Number.parseInt(value, 10);
240
+ }