@chiwanpark/pi-squared-agent 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/README.md +13 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +144 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/auth-store.d.ts +36 -0
- package/dist/runtime/auth-store.d.ts.map +1 -0
- package/dist/runtime/auth-store.js +131 -0
- package/dist/runtime/auth-store.js.map +1 -0
- package/dist/runtime/config-store.d.ts +44 -0
- package/dist/runtime/config-store.d.ts.map +1 -0
- package/dist/runtime/config-store.js +138 -0
- package/dist/runtime/config-store.js.map +1 -0
- package/dist/runtime/messages.d.ts +12 -0
- package/dist/runtime/messages.d.ts.map +1 -0
- package/dist/runtime/messages.js +101 -0
- package/dist/runtime/messages.js.map +1 -0
- package/dist/runtime/model-resolver.d.ts +52 -0
- package/dist/runtime/model-resolver.d.ts.map +1 -0
- package/dist/runtime/model-resolver.js +175 -0
- package/dist/runtime/model-resolver.js.map +1 -0
- package/dist/runtime/pi-agent.d.ts +83 -0
- package/dist/runtime/pi-agent.d.ts.map +1 -0
- package/dist/runtime/pi-agent.js +405 -0
- package/dist/runtime/pi-agent.js.map +1 -0
- package/dist/runtime/status-store.d.ts +50 -0
- package/dist/runtime/status-store.d.ts.map +1 -0
- package/dist/runtime/status-store.js +65 -0
- package/dist/runtime/status-store.js.map +1 -0
- package/dist/runtime/system-prompt.d.ts +11 -0
- package/dist/runtime/system-prompt.d.ts.map +1 -0
- package/dist/runtime/system-prompt.js +43 -0
- package/dist/runtime/system-prompt.js.map +1 -0
- package/dist/runtime/tools/bash.d.ts +23 -0
- package/dist/runtime/tools/bash.d.ts.map +1 -0
- package/dist/runtime/tools/bash.js +206 -0
- package/dist/runtime/tools/bash.js.map +1 -0
- package/dist/runtime/tools/child-process.d.ts +13 -0
- package/dist/runtime/tools/child-process.d.ts.map +1 -0
- package/dist/runtime/tools/child-process.js +75 -0
- package/dist/runtime/tools/child-process.js.map +1 -0
- package/dist/runtime/tools/output-accumulator.d.ts +52 -0
- package/dist/runtime/tools/output-accumulator.d.ts.map +1 -0
- package/dist/runtime/tools/output-accumulator.js +160 -0
- package/dist/runtime/tools/output-accumulator.js.map +1 -0
- package/dist/runtime/tools/shell.d.ts +26 -0
- package/dist/runtime/tools/shell.d.ts.map +1 -0
- package/dist/runtime/tools/shell.js +116 -0
- package/dist/runtime/tools/shell.js.map +1 -0
- package/dist/runtime/tools/truncate.d.ts +38 -0
- package/dist/runtime/tools/truncate.d.ts.map +1 -0
- package/dist/runtime/tools/truncate.js +98 -0
- package/dist/runtime/tools/truncate.js.map +1 -0
- package/dist/tools/bash/child-process.d.ts +13 -0
- package/dist/tools/bash/child-process.d.ts.map +1 -0
- package/dist/tools/bash/child-process.js +79 -0
- package/dist/tools/bash/child-process.js.map +1 -0
- package/dist/tools/bash/output-accumulator.d.ts +54 -0
- package/dist/tools/bash/output-accumulator.d.ts.map +1 -0
- package/dist/tools/bash/output-accumulator.js +171 -0
- package/dist/tools/bash/output-accumulator.js.map +1 -0
- package/dist/tools/bash/shell.d.ts +26 -0
- package/dist/tools/bash/shell.d.ts.map +1 -0
- package/dist/tools/bash/shell.js +129 -0
- package/dist/tools/bash/shell.js.map +1 -0
- package/dist/tools/bash/tool.d.ts +23 -0
- package/dist/tools/bash/tool.d.ts.map +1 -0
- package/dist/tools/bash/tool.js +219 -0
- package/dist/tools/bash/tool.js.map +1 -0
- package/dist/tools/bash/truncate.d.ts +50 -0
- package/dist/tools/bash/truncate.d.ts.map +1 -0
- package/dist/tools/bash/truncate.js +184 -0
- package/dist/tools/bash/truncate.js.map +1 -0
- package/dist/tools/external-tool.d.ts +2 -0
- package/dist/tools/external-tool.d.ts.map +1 -0
- package/dist/tools/external-tool.js +15 -0
- package/dist/tools/external-tool.js.map +1 -0
- package/dist/tools/file/edit-diff.d.ts +27 -0
- package/dist/tools/file/edit-diff.d.ts.map +1 -0
- package/dist/tools/file/edit-diff.js +196 -0
- package/dist/tools/file/edit-diff.js.map +1 -0
- package/dist/tools/file/edit.d.ts +26 -0
- package/dist/tools/file/edit.d.ts.map +1 -0
- package/dist/tools/file/edit.js +106 -0
- package/dist/tools/file/edit.js.map +1 -0
- package/dist/tools/file/find.d.ts +27 -0
- package/dist/tools/file/find.d.ts.map +1 -0
- package/dist/tools/file/find.js +150 -0
- package/dist/tools/file/find.js.map +1 -0
- package/dist/tools/file/glob.d.ts +6 -0
- package/dist/tools/file/glob.d.ts.map +1 -0
- package/dist/tools/file/glob.js +50 -0
- package/dist/tools/file/glob.js.map +1 -0
- package/dist/tools/file/grep.d.ts +29 -0
- package/dist/tools/file/grep.d.ts.map +1 -0
- package/dist/tools/file/grep.js +220 -0
- package/dist/tools/file/grep.js.map +1 -0
- package/dist/tools/file/index.d.ts +7 -0
- package/dist/tools/file/index.d.ts.map +1 -0
- package/dist/tools/file/index.js +7 -0
- package/dist/tools/file/index.js.map +1 -0
- package/dist/tools/file/ls.d.ts +27 -0
- package/dist/tools/file/ls.d.ts.map +1 -0
- package/dist/tools/file/ls.js +82 -0
- package/dist/tools/file/ls.js.map +1 -0
- package/dist/tools/file/read.d.ts +23 -0
- package/dist/tools/file/read.d.ts.map +1 -0
- package/dist/tools/file/read.js +98 -0
- package/dist/tools/file/read.js.map +1 -0
- package/dist/tools/file/write.d.ts +17 -0
- package/dist/tools/file/write.d.ts.map +1 -0
- package/dist/tools/file/write.js +45 -0
- package/dist/tools/file/write.js.map +1 -0
- package/dist/tools/file-mutation-queue.d.ts +3 -0
- package/dist/tools/file-mutation-queue.d.ts.map +1 -0
- package/dist/tools/file-mutation-queue.js +47 -0
- package/dist/tools/file-mutation-queue.js.map +1 -0
- package/dist/tools/path-utils.d.ts +5 -0
- package/dist/tools/path-utils.d.ts.map +1 -0
- package/dist/tools/path-utils.js +79 -0
- package/dist/tools/path-utils.js.map +1 -0
- package/dist/tools/web/index.d.ts +2 -0
- package/dist/tools/web/index.d.ts.map +1 -0
- package/dist/tools/web/index.js +2 -0
- package/dist/tools/web/index.js.map +1 -0
- package/dist/tools/web/search.d.ts +44 -0
- package/dist/tools/web/search.d.ts.map +1 -0
- package/dist/tools/web/search.js +209 -0
- package/dist/tools/web/search.js.map +1 -0
- package/dist/tui/box.d.ts +11 -0
- package/dist/tui/box.d.ts.map +1 -0
- package/dist/tui/box.js +24 -0
- package/dist/tui/box.js.map +1 -0
- package/dist/tui/chat-screen.d.ts +29 -0
- package/dist/tui/chat-screen.d.ts.map +1 -0
- package/dist/tui/chat-screen.js +475 -0
- package/dist/tui/chat-screen.js.map +1 -0
- package/dist/tui/commands.d.ts +34 -0
- package/dist/tui/commands.d.ts.map +1 -0
- package/dist/tui/commands.js +691 -0
- package/dist/tui/commands.js.map +1 -0
- package/dist/tui/interactive.d.ts +9 -0
- package/dist/tui/interactive.d.ts.map +1 -0
- package/dist/tui/interactive.js +88 -0
- package/dist/tui/interactive.js.map +1 -0
- package/dist/tui/overlays.d.ts +39 -0
- package/dist/tui/overlays.d.ts.map +1 -0
- package/dist/tui/overlays.js +157 -0
- package/dist/tui/overlays.js.map +1 -0
- package/dist/tui/theme.d.ts +40 -0
- package/dist/tui/theme.d.ts.map +1 -0
- package/dist/tui/theme.js +120 -0
- package/dist/tui/theme.js.map +1 -0
- package/dist/tui/welcome.d.ts +3 -0
- package/dist/tui/welcome.d.ts.map +1 -0
- package/dist/tui/welcome.js +37 -0
- package/dist/tui/welcome.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
import { getOAuthProvider, getOAuthProviders } from "@earendil-works/pi-ai/oauth";
|
|
2
|
+
import { Sequence as OscSequence } from "@tsports/go-osc52";
|
|
3
|
+
import { findModelByReference, listApiKeyProviders, listModelsForProvider, listOAuthProviders, listProvidersForSelection, normalizeThinkingLevel, } from "../runtime/model-resolver.js";
|
|
4
|
+
import { DEFAULT_MAX_SOURCES, DEFAULT_SEARCH_MODEL, DEFAULT_SEARCH_TIMEOUT_MS, MAX_ALLOWED_SOURCES, } from "../tools/web/index.js";
|
|
5
|
+
import { showInfo, showPrompt, showSelect } from "./overlays.js";
|
|
6
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
7
|
+
export function parseSlashCommand(text) {
|
|
8
|
+
const trimmed = text.trim();
|
|
9
|
+
if (!trimmed.startsWith("/"))
|
|
10
|
+
return null;
|
|
11
|
+
const body = trimmed.slice(1);
|
|
12
|
+
const space = body.indexOf(" ");
|
|
13
|
+
if (space === -1)
|
|
14
|
+
return { name: body.trim(), args: "" };
|
|
15
|
+
return { name: body.slice(0, space).trim(), args: body.slice(space + 1).trim() };
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Build the slash command registry. Returns both the commands the editor
|
|
19
|
+
* autocomplete uses and the handler map for dispatching submitted commands.
|
|
20
|
+
*/
|
|
21
|
+
export function createCommands(authStore, configStore) {
|
|
22
|
+
return [
|
|
23
|
+
helpCommand(),
|
|
24
|
+
newSessionCommand(),
|
|
25
|
+
continueCommand(),
|
|
26
|
+
quitCommand("quit"),
|
|
27
|
+
quitCommand("exit"),
|
|
28
|
+
modelCommand(authStore, configStore),
|
|
29
|
+
thinkingCommand(configStore),
|
|
30
|
+
searchCommand(configStore),
|
|
31
|
+
loginCommand(),
|
|
32
|
+
logoutCommand(),
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
export function buildRegistry(definitions = createCommands()) {
|
|
36
|
+
const byName = new Map();
|
|
37
|
+
for (const def of definitions)
|
|
38
|
+
byName.set(def.command.name, def);
|
|
39
|
+
return {
|
|
40
|
+
commands: definitions.map((def) => def.command),
|
|
41
|
+
async execute(text, ctx) {
|
|
42
|
+
const parsed = parseSlashCommand(text);
|
|
43
|
+
if (!parsed)
|
|
44
|
+
return false;
|
|
45
|
+
const def = byName.get(parsed.name);
|
|
46
|
+
if (!def) {
|
|
47
|
+
ctx.runtime.setNotice(`Unknown command '/${parsed.name}'. Type /help for available commands.`, "warn");
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
await def.execute(parsed.args, ctx);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
55
|
+
ctx.runtime.setNotice(`/${parsed.name} failed: ${message}`, "error");
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function helpCommand() {
|
|
62
|
+
return {
|
|
63
|
+
command: { name: "help", description: "Show available slash commands" },
|
|
64
|
+
async execute(_args, ctx) {
|
|
65
|
+
const lines = [
|
|
66
|
+
"/help show this help",
|
|
67
|
+
"/new clear chat and start a new session",
|
|
68
|
+
"/continue retry the last cancelled or failed request",
|
|
69
|
+
"/quit, /exit leave the chat",
|
|
70
|
+
"/model [ref] switch model (e.g. anthropic/claude-sonnet-4-5)",
|
|
71
|
+
"/thinking [level] off | minimal | low | medium | high | xhigh",
|
|
72
|
+
"/search configure search_web defaults",
|
|
73
|
+
"/login [provider] authenticate via OAuth (Anthropic, ChatGPT, GitHub Copilot)",
|
|
74
|
+
"/logout [provider] clear OAuth credentials",
|
|
75
|
+
"",
|
|
76
|
+
"Pass /command with no argument to open an interactive selector.",
|
|
77
|
+
];
|
|
78
|
+
await showInfo(ctx.screen, { title: "Slash Commands", body: lines.join("\n") }).promise;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function newSessionCommand() {
|
|
83
|
+
return {
|
|
84
|
+
command: { name: "new", description: "Clear chat and start a new session" },
|
|
85
|
+
async execute(_args, ctx) {
|
|
86
|
+
ctx.runtime.newSession();
|
|
87
|
+
ctx.screen.editor.setText("");
|
|
88
|
+
ctx.runtime.setNotice("Started a new session.", "info");
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function continueCommand() {
|
|
93
|
+
return {
|
|
94
|
+
command: { name: "continue", description: "Retry the last cancelled or failed request" },
|
|
95
|
+
async execute(_args, ctx) {
|
|
96
|
+
await ctx.runtime.continueLast();
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function quitCommand(name) {
|
|
101
|
+
return {
|
|
102
|
+
command: { name, description: name === "quit" ? "Exit pi-squared" : "Exit pi-squared (alias for /quit)" },
|
|
103
|
+
async execute(_args, ctx) {
|
|
104
|
+
ctx.requestExit();
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function modelCommand(authStore, configStore) {
|
|
109
|
+
return {
|
|
110
|
+
command: {
|
|
111
|
+
name: "model",
|
|
112
|
+
description: "Switch model",
|
|
113
|
+
argumentHint: "[provider/]model-id",
|
|
114
|
+
getArgumentCompletions: (prefix) => completeModelReference(prefix, authStore),
|
|
115
|
+
},
|
|
116
|
+
async execute(args, ctx) {
|
|
117
|
+
if (args.length > 0) {
|
|
118
|
+
const next = resolveModelReference(args, ctx.runtime.authStore);
|
|
119
|
+
await applyModel(ctx, next.provider, next.model, configStore);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const providers = listProvidersForSelection(ctx.runtime.authStore);
|
|
123
|
+
const providerChoice = await showSelect(ctx.screen, {
|
|
124
|
+
title: "Select provider",
|
|
125
|
+
items: providers.map((entry) => ({
|
|
126
|
+
value: entry.id,
|
|
127
|
+
label: entry.id,
|
|
128
|
+
description: providerAvailabilityLabel(entry.via),
|
|
129
|
+
})),
|
|
130
|
+
});
|
|
131
|
+
if (!providerChoice)
|
|
132
|
+
return;
|
|
133
|
+
const models = listModelsForProvider(providerChoice);
|
|
134
|
+
const modelChoice = await showSelect(ctx.screen, {
|
|
135
|
+
title: `Select model (${providerChoice})`,
|
|
136
|
+
items: models.map((entry) => ({
|
|
137
|
+
value: entry.id,
|
|
138
|
+
label: entry.id,
|
|
139
|
+
description: entry.reasoning ? `${entry.name} · reasoning` : entry.name,
|
|
140
|
+
})),
|
|
141
|
+
});
|
|
142
|
+
if (!modelChoice)
|
|
143
|
+
return;
|
|
144
|
+
await applyModel(ctx, providerChoice, modelChoice, configStore);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function thinkingCommand(configStore) {
|
|
149
|
+
return {
|
|
150
|
+
command: {
|
|
151
|
+
name: "thinking",
|
|
152
|
+
description: "Set thinking level",
|
|
153
|
+
argumentHint: "off|minimal|low|medium|high|xhigh",
|
|
154
|
+
getArgumentCompletions: (prefix) => THINKING_LEVELS.filter((level) => level.startsWith(prefix)).map((level) => ({
|
|
155
|
+
value: level,
|
|
156
|
+
label: level,
|
|
157
|
+
})),
|
|
158
|
+
},
|
|
159
|
+
async execute(args, ctx) {
|
|
160
|
+
let target = args.trim();
|
|
161
|
+
if (target.length === 0) {
|
|
162
|
+
const selected = await showSelect(ctx.screen, {
|
|
163
|
+
title: "Select thinking level",
|
|
164
|
+
items: THINKING_LEVELS.map((level) => ({ value: level, label: level })),
|
|
165
|
+
});
|
|
166
|
+
if (!selected)
|
|
167
|
+
return;
|
|
168
|
+
target = selected;
|
|
169
|
+
}
|
|
170
|
+
if (!THINKING_LEVELS.includes(target)) {
|
|
171
|
+
ctx.runtime.setNotice(`Invalid thinking level '${target}'.`, "warn");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const snapshot = ctx.runtime.status.getSnapshot();
|
|
175
|
+
const next = normalizeThinkingLevel(ctx.runtime.agent.state.model, target);
|
|
176
|
+
ctx.runtime.setThinkingLevel(next);
|
|
177
|
+
await configStore?.setThinkingLevel(next);
|
|
178
|
+
if (next !== target) {
|
|
179
|
+
ctx.runtime.setNotice(`Model ${snapshot.model.provider}/${snapshot.model.id} does not support thinking='${target}'. Using '${next}'.`, "warn");
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
ctx.runtime.setNotice(`Thinking level set to ${next}.`, "info");
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function searchCommand(configStore) {
|
|
188
|
+
return {
|
|
189
|
+
command: {
|
|
190
|
+
name: "search",
|
|
191
|
+
description: "Configure search_web defaults",
|
|
192
|
+
},
|
|
193
|
+
async execute(args, ctx) {
|
|
194
|
+
const trimmed = args.trim();
|
|
195
|
+
if (trimmed.length === 0) {
|
|
196
|
+
await showSearchConfigMenu(ctx, configStore);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
// Keep the old argument form working for users who already have it in muscle memory,
|
|
200
|
+
// but the command now advertises and defaults to the interactive menu above.
|
|
201
|
+
await applySearchConfigFromArgs(trimmed, ctx, configStore);
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
async function showSearchConfigMenu(ctx, configStore) {
|
|
206
|
+
const config = ctx.runtime.getWebSearchConfig();
|
|
207
|
+
const selected = await showSelect(ctx.screen, {
|
|
208
|
+
title: "Search Configuration",
|
|
209
|
+
items: [
|
|
210
|
+
{
|
|
211
|
+
value: "model",
|
|
212
|
+
label: "Model",
|
|
213
|
+
description: searchConfigDescription(config.model, DEFAULT_SEARCH_MODEL),
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
value: "max-sources",
|
|
217
|
+
label: "Max sources",
|
|
218
|
+
description: searchConfigDescription(config.maxSources, DEFAULT_MAX_SOURCES),
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
value: "timeout",
|
|
222
|
+
label: "Timeout",
|
|
223
|
+
description: searchConfigDescription(config.timeoutMs === undefined ? undefined : `${config.timeoutMs}ms`, `${DEFAULT_SEARCH_TIMEOUT_MS}ms`),
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
value: "reset",
|
|
227
|
+
label: "Reset to defaults",
|
|
228
|
+
description: `model ${DEFAULT_SEARCH_MODEL}, ${DEFAULT_MAX_SOURCES} sources, ${DEFAULT_SEARCH_TIMEOUT_MS}ms`,
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
});
|
|
232
|
+
if (!selected)
|
|
233
|
+
return;
|
|
234
|
+
if (selected === "model") {
|
|
235
|
+
await configureSearchModel(ctx, configStore);
|
|
236
|
+
}
|
|
237
|
+
else if (selected === "max-sources") {
|
|
238
|
+
await configureSearchMaxSources(ctx, configStore);
|
|
239
|
+
}
|
|
240
|
+
else if (selected === "timeout") {
|
|
241
|
+
await configureSearchTimeout(ctx, configStore);
|
|
242
|
+
}
|
|
243
|
+
else if (selected === "reset") {
|
|
244
|
+
await applySearchConfig(ctx, {}, configStore, "Search configuration reset to defaults.");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function configureSearchModel(ctx, configStore) {
|
|
248
|
+
const current = ctx.runtime.getWebSearchConfig().model ?? DEFAULT_SEARCH_MODEL;
|
|
249
|
+
const selected = await showSelect(ctx.screen, {
|
|
250
|
+
title: "Search model",
|
|
251
|
+
items: [
|
|
252
|
+
{ value: "default", label: DEFAULT_SEARCH_MODEL, description: "default" },
|
|
253
|
+
{ value: "custom", label: "Custom model…", description: `current: ${current}` },
|
|
254
|
+
],
|
|
255
|
+
});
|
|
256
|
+
if (!selected)
|
|
257
|
+
return;
|
|
258
|
+
const next = { ...ctx.runtime.getWebSearchConfig() };
|
|
259
|
+
if (selected === "default") {
|
|
260
|
+
delete next.model;
|
|
261
|
+
await applySearchConfig(ctx, next, configStore, `Search model reset to default (${DEFAULT_SEARCH_MODEL}).`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const model = await showPrompt(ctx.screen, {
|
|
265
|
+
title: "Custom search model",
|
|
266
|
+
message: `Current model: ${current}`,
|
|
267
|
+
allowEmpty: false,
|
|
268
|
+
});
|
|
269
|
+
if (model === undefined)
|
|
270
|
+
return;
|
|
271
|
+
const value = model.trim();
|
|
272
|
+
if (value === DEFAULT_SEARCH_MODEL)
|
|
273
|
+
delete next.model;
|
|
274
|
+
else
|
|
275
|
+
next.model = value;
|
|
276
|
+
await applySearchConfig(ctx, next, configStore, `Search model set to ${value}.`);
|
|
277
|
+
}
|
|
278
|
+
async function configureSearchMaxSources(ctx, configStore) {
|
|
279
|
+
const selected = await showSelect(ctx.screen, {
|
|
280
|
+
title: "Search max sources",
|
|
281
|
+
items: [
|
|
282
|
+
{ value: "default", label: `${DEFAULT_MAX_SOURCES}`, description: "default" },
|
|
283
|
+
...Array.from({ length: MAX_ALLOWED_SOURCES }, (_, index) => index + 1)
|
|
284
|
+
.filter((value) => value !== DEFAULT_MAX_SOURCES)
|
|
285
|
+
.map((value) => ({ value: String(value), label: String(value) })),
|
|
286
|
+
],
|
|
287
|
+
});
|
|
288
|
+
if (!selected)
|
|
289
|
+
return;
|
|
290
|
+
const next = { ...ctx.runtime.getWebSearchConfig() };
|
|
291
|
+
if (selected === "default") {
|
|
292
|
+
delete next.maxSources;
|
|
293
|
+
await applySearchConfig(ctx, next, configStore, `Search max sources reset to default (${DEFAULT_MAX_SOURCES}).`);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const maxSources = parsePositiveInteger(selected);
|
|
297
|
+
if (!maxSources)
|
|
298
|
+
return;
|
|
299
|
+
next.maxSources = maxSources;
|
|
300
|
+
await applySearchConfig(ctx, next, configStore, `Search max sources set to ${maxSources}.`);
|
|
301
|
+
}
|
|
302
|
+
async function configureSearchTimeout(ctx, configStore) {
|
|
303
|
+
const current = ctx.runtime.getWebSearchConfig().timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS;
|
|
304
|
+
const presets = [30_000, 60_000, DEFAULT_SEARCH_TIMEOUT_MS, 300_000];
|
|
305
|
+
const selected = await showSelect(ctx.screen, {
|
|
306
|
+
title: "Search timeout",
|
|
307
|
+
items: [
|
|
308
|
+
{ value: "default", label: `${DEFAULT_SEARCH_TIMEOUT_MS}ms`, description: "default" },
|
|
309
|
+
...presets
|
|
310
|
+
.filter((value) => value !== DEFAULT_SEARCH_TIMEOUT_MS)
|
|
311
|
+
.map((value) => ({ value: String(value), label: `${value}ms` })),
|
|
312
|
+
{ value: "custom", label: "Custom timeout…", description: `current: ${current}ms` },
|
|
313
|
+
],
|
|
314
|
+
});
|
|
315
|
+
if (!selected)
|
|
316
|
+
return;
|
|
317
|
+
const next = { ...ctx.runtime.getWebSearchConfig() };
|
|
318
|
+
if (selected === "default") {
|
|
319
|
+
delete next.timeoutMs;
|
|
320
|
+
await applySearchConfig(ctx, next, configStore, `Search timeout reset to default (${DEFAULT_SEARCH_TIMEOUT_MS}ms).`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
let timeoutMs = parsePositiveInteger(selected);
|
|
324
|
+
if (selected === "custom") {
|
|
325
|
+
const value = await showPrompt(ctx.screen, {
|
|
326
|
+
title: "Custom search timeout",
|
|
327
|
+
message: `Enter timeout in milliseconds. Current timeout: ${current}ms`,
|
|
328
|
+
allowEmpty: false,
|
|
329
|
+
});
|
|
330
|
+
if (value === undefined)
|
|
331
|
+
return;
|
|
332
|
+
timeoutMs = parsePositiveInteger(value.trim());
|
|
333
|
+
if (!timeoutMs) {
|
|
334
|
+
ctx.runtime.setNotice("Search timeout must be a positive integer in milliseconds.", "warn");
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (!timeoutMs)
|
|
339
|
+
return;
|
|
340
|
+
if (timeoutMs === DEFAULT_SEARCH_TIMEOUT_MS)
|
|
341
|
+
delete next.timeoutMs;
|
|
342
|
+
else
|
|
343
|
+
next.timeoutMs = timeoutMs;
|
|
344
|
+
await applySearchConfig(ctx, next, configStore, `Search timeout set to ${timeoutMs}ms.`);
|
|
345
|
+
}
|
|
346
|
+
async function applySearchConfigFromArgs(trimmed, ctx, configStore) {
|
|
347
|
+
const [rawSetting, ...valueParts] = trimmed.split(/\s+/);
|
|
348
|
+
const setting = normalizeSearchSetting(rawSetting ?? "");
|
|
349
|
+
if (setting === "reset") {
|
|
350
|
+
await applySearchConfig(ctx, {}, configStore, "Search configuration reset to defaults.");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const value = valueParts.join(" ").trim();
|
|
354
|
+
if (value.length === 0) {
|
|
355
|
+
ctx.runtime.setNotice("Usage: /search (or legacy: /search model <id> | max-sources <n> | timeout <ms> | reset)", "warn");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const next = { ...ctx.runtime.getWebSearchConfig() };
|
|
359
|
+
if (setting === "model") {
|
|
360
|
+
next.model = value;
|
|
361
|
+
await applySearchConfig(ctx, next, configStore, `Search model set to ${value}.`);
|
|
362
|
+
}
|
|
363
|
+
else if (setting === "max-sources") {
|
|
364
|
+
const maxSources = parsePositiveInteger(value);
|
|
365
|
+
if (!maxSources) {
|
|
366
|
+
ctx.runtime.setNotice("Search max-sources must be a positive integer.", "warn");
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
next.maxSources = maxSources;
|
|
370
|
+
await applySearchConfig(ctx, next, configStore, `Search max sources set to ${maxSources}.`);
|
|
371
|
+
}
|
|
372
|
+
else if (setting === "timeout") {
|
|
373
|
+
const timeoutMs = parsePositiveInteger(value);
|
|
374
|
+
if (!timeoutMs) {
|
|
375
|
+
ctx.runtime.setNotice("Search timeout must be a positive integer in milliseconds.", "warn");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
next.timeoutMs = timeoutMs;
|
|
379
|
+
await applySearchConfig(ctx, next, configStore, `Search timeout set to ${timeoutMs}ms.`);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
ctx.runtime.setNotice(`Unknown search setting '${rawSetting}'.`, "warn");
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function searchConfigDescription(value, defaultValue) {
|
|
386
|
+
return value === undefined ? `default: ${defaultValue}` : `current: ${value}`;
|
|
387
|
+
}
|
|
388
|
+
function normalizeSearchSetting(value) {
|
|
389
|
+
switch (value) {
|
|
390
|
+
case "model":
|
|
391
|
+
return "model";
|
|
392
|
+
case "max-sources":
|
|
393
|
+
case "maxSources":
|
|
394
|
+
case "sources":
|
|
395
|
+
return "max-sources";
|
|
396
|
+
case "timeout":
|
|
397
|
+
case "timeout-ms":
|
|
398
|
+
case "timeoutMs":
|
|
399
|
+
return "timeout";
|
|
400
|
+
case "reset":
|
|
401
|
+
return "reset";
|
|
402
|
+
default:
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async function applySearchConfig(ctx, config, configStore, notice) {
|
|
407
|
+
ctx.runtime.setWebSearchConfig(config);
|
|
408
|
+
await configStore?.setSearchConfig(config);
|
|
409
|
+
ctx.runtime.setNotice(notice, "info");
|
|
410
|
+
}
|
|
411
|
+
function parsePositiveInteger(value) {
|
|
412
|
+
const parsed = Number(value);
|
|
413
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
414
|
+
return undefined;
|
|
415
|
+
return Math.floor(parsed);
|
|
416
|
+
}
|
|
417
|
+
function loginCommand() {
|
|
418
|
+
return {
|
|
419
|
+
command: {
|
|
420
|
+
name: "login",
|
|
421
|
+
description: "Authenticate with a provider (OAuth or API key)",
|
|
422
|
+
argumentHint: "provider",
|
|
423
|
+
getArgumentCompletions: (prefix) => [
|
|
424
|
+
...getOAuthProviders()
|
|
425
|
+
.filter((entry) => entry.id.startsWith(prefix))
|
|
426
|
+
.map((entry) => ({ value: `oauth:${entry.id}`, label: entry.name, description: "OAuth" })),
|
|
427
|
+
...listApiKeyProviders()
|
|
428
|
+
.filter((entry) => entry.id.startsWith(prefix))
|
|
429
|
+
.map((entry) => ({ value: `apikey:${entry.id}`, label: entry.id, description: "API key" })),
|
|
430
|
+
],
|
|
431
|
+
},
|
|
432
|
+
async execute(args, ctx) {
|
|
433
|
+
let choice = args.trim();
|
|
434
|
+
if (choice.length === 0) {
|
|
435
|
+
const oauthItems = listOAuthProviders(ctx.runtime.authStore).map((entry) => ({
|
|
436
|
+
value: `oauth:${entry.id}`,
|
|
437
|
+
label: entry.name,
|
|
438
|
+
description: entry.loggedIn ? "OAuth · already signed in" : "OAuth",
|
|
439
|
+
}));
|
|
440
|
+
const apiKeyItems = listApiKeyProviders(ctx.runtime.authStore).map((entry) => ({
|
|
441
|
+
value: `apikey:${entry.id}`,
|
|
442
|
+
label: entry.id,
|
|
443
|
+
description: entry.hasKey ? "API key · already stored" : "API key",
|
|
444
|
+
}));
|
|
445
|
+
const selected = await showSelect(ctx.screen, {
|
|
446
|
+
title: "Sign in with",
|
|
447
|
+
items: [...oauthItems, ...apiKeyItems],
|
|
448
|
+
});
|
|
449
|
+
if (!selected)
|
|
450
|
+
return;
|
|
451
|
+
choice = selected;
|
|
452
|
+
}
|
|
453
|
+
if (choice.startsWith("apikey:")) {
|
|
454
|
+
const providerId = choice.slice("apikey:".length);
|
|
455
|
+
const key = await showPrompt(ctx.screen, {
|
|
456
|
+
title: `API key — ${providerId}`,
|
|
457
|
+
message: `Enter your API key for ${providerId}:`,
|
|
458
|
+
allowEmpty: false,
|
|
459
|
+
});
|
|
460
|
+
if (key === undefined)
|
|
461
|
+
return;
|
|
462
|
+
await ctx.runtime.authStore.setApiKey(providerId, key);
|
|
463
|
+
ctx.runtime.refreshModelFromAuth();
|
|
464
|
+
ctx.runtime.setNotice(`API key stored for ${providerId}.`, "info");
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const providerId = choice.startsWith("oauth:") ? choice.slice("oauth:".length) : choice;
|
|
468
|
+
const provider = getOAuthProvider(providerId);
|
|
469
|
+
if (!provider) {
|
|
470
|
+
ctx.runtime.setNotice(`Unknown OAuth provider '${providerId}'.`, "warn");
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const { callbacks, cleanup } = createLoginCallbacks(ctx, provider);
|
|
474
|
+
try {
|
|
475
|
+
const credentials = await provider.login(callbacks).finally(cleanup);
|
|
476
|
+
await ctx.runtime.authStore.setOAuth(provider.id, credentials);
|
|
477
|
+
ctx.runtime.refreshModelFromAuth();
|
|
478
|
+
ctx.runtime.setNotice(`Signed in to ${provider.name}.`, "info");
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
482
|
+
ctx.runtime.setNotice(`Login to ${provider.name} failed: ${message}`, "error");
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function logoutCommand() {
|
|
488
|
+
return {
|
|
489
|
+
command: {
|
|
490
|
+
name: "logout",
|
|
491
|
+
description: "Clear stored credentials (OAuth or API key)",
|
|
492
|
+
argumentHint: "provider",
|
|
493
|
+
getArgumentCompletions: (prefix) => [
|
|
494
|
+
...getOAuthProviders()
|
|
495
|
+
.filter((entry) => entry.id.startsWith(prefix))
|
|
496
|
+
.map((entry) => ({ value: `oauth:${entry.id}`, label: entry.name, description: "OAuth" })),
|
|
497
|
+
...listApiKeyProviders()
|
|
498
|
+
.filter((entry) => entry.id.startsWith(prefix))
|
|
499
|
+
.map((entry) => ({ value: `apikey:${entry.id}`, label: entry.id, description: "API key" })),
|
|
500
|
+
],
|
|
501
|
+
},
|
|
502
|
+
async execute(args, ctx) {
|
|
503
|
+
let choice = args.trim();
|
|
504
|
+
if (choice.length === 0) {
|
|
505
|
+
const oauthStored = listOAuthProviders(ctx.runtime.authStore)
|
|
506
|
+
.filter((entry) => entry.loggedIn)
|
|
507
|
+
.map((entry) => ({ value: `oauth:${entry.id}`, label: entry.name, description: "OAuth" }));
|
|
508
|
+
const apiKeyStored = listApiKeyProviders(ctx.runtime.authStore)
|
|
509
|
+
.filter((entry) => entry.hasKey)
|
|
510
|
+
.map((entry) => ({ value: `apikey:${entry.id}`, label: entry.id, description: "API key" }));
|
|
511
|
+
const allStored = [...oauthStored, ...apiKeyStored];
|
|
512
|
+
if (allStored.length === 0) {
|
|
513
|
+
ctx.runtime.setNotice("No stored credentials to clear.", "info");
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const selected = await showSelect(ctx.screen, { title: "Sign out of", items: allStored });
|
|
517
|
+
if (!selected)
|
|
518
|
+
return;
|
|
519
|
+
choice = selected;
|
|
520
|
+
}
|
|
521
|
+
if (choice.startsWith("apikey:")) {
|
|
522
|
+
const providerId = choice.slice("apikey:".length);
|
|
523
|
+
const removed = await ctx.runtime.authStore.removeApiKey(providerId);
|
|
524
|
+
if (removed) {
|
|
525
|
+
ctx.runtime.refreshModelFromAuth();
|
|
526
|
+
ctx.runtime.setNotice(`API key removed for ${providerId}.`, "info");
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
ctx.runtime.setNotice(`No API key stored for ${providerId}.`, "info");
|
|
530
|
+
}
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const providerId = choice.startsWith("oauth:") ? choice.slice("oauth:".length) : choice;
|
|
534
|
+
const provider = getOAuthProvider(providerId);
|
|
535
|
+
if (!provider) {
|
|
536
|
+
ctx.runtime.setNotice(`Unknown OAuth provider '${providerId}'.`, "warn");
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const removed = await ctx.runtime.authStore.removeOAuth(providerId);
|
|
540
|
+
if (removed) {
|
|
541
|
+
ctx.runtime.refreshModelFromAuth();
|
|
542
|
+
ctx.runtime.setNotice(`Signed out of ${provider.name}.`, "info");
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
ctx.runtime.setNotice(`No credentials stored for ${provider.name}.`, "info");
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
async function applyModel(ctx, provider, modelId, configStore) {
|
|
551
|
+
const model = findModelByReference(provider, modelId);
|
|
552
|
+
ctx.runtime.setModel(model);
|
|
553
|
+
const thinking = normalizeThinkingLevel(model, ctx.runtime.status.getSnapshot().thinkingLevel);
|
|
554
|
+
ctx.runtime.setThinkingLevel(thinking);
|
|
555
|
+
await configStore?.setModelAndThinking(model.provider, model.id, thinking);
|
|
556
|
+
ctx.runtime.setNotice(`Switched to ${model.provider}/${model.id}.`, "info");
|
|
557
|
+
}
|
|
558
|
+
function resolveModelReference(reference, authStore) {
|
|
559
|
+
const slash = reference.indexOf("/");
|
|
560
|
+
if (slash > 0) {
|
|
561
|
+
return { provider: reference.slice(0, slash), model: reference.slice(slash + 1) };
|
|
562
|
+
}
|
|
563
|
+
// No provider: search authenticated providers in preference order (first match wins).
|
|
564
|
+
for (const entry of listProvidersForSelection(authStore)) {
|
|
565
|
+
try {
|
|
566
|
+
const model = findModelByReference(entry.id, reference);
|
|
567
|
+
return { provider: entry.id, model: model.id };
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
// Not in this provider, keep looking.
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
throw new Error(`Could not locate model '${reference}'. Use the form 'provider/model-id'.`);
|
|
574
|
+
}
|
|
575
|
+
function completeModelReference(prefix, authStore) {
|
|
576
|
+
const slash = prefix.indexOf("/");
|
|
577
|
+
if (slash === -1) {
|
|
578
|
+
return listProvidersForSelection(authStore)
|
|
579
|
+
.filter((entry) => entry.id.startsWith(prefix))
|
|
580
|
+
.map((entry) => ({ value: `${entry.id}/`, label: entry.id, description: "provider" }));
|
|
581
|
+
}
|
|
582
|
+
const provider = prefix.slice(0, slash);
|
|
583
|
+
const modelPrefix = prefix.slice(slash + 1);
|
|
584
|
+
try {
|
|
585
|
+
return listModelsForProvider(provider)
|
|
586
|
+
.filter((entry) => entry.id.startsWith(modelPrefix))
|
|
587
|
+
.map((entry) => ({
|
|
588
|
+
value: `${provider}/${entry.id}`,
|
|
589
|
+
label: entry.id,
|
|
590
|
+
description: entry.name,
|
|
591
|
+
}));
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
return [];
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
function providerAvailabilityLabel(via) {
|
|
598
|
+
switch (via) {
|
|
599
|
+
case "env":
|
|
600
|
+
return "env api key";
|
|
601
|
+
case "oauth":
|
|
602
|
+
return "oauth";
|
|
603
|
+
case "stored-key":
|
|
604
|
+
return "stored api key";
|
|
605
|
+
case "none":
|
|
606
|
+
return "no credentials";
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function createLoginCallbacks(ctx, provider) {
|
|
610
|
+
let authUrl;
|
|
611
|
+
let dismissAuthOverlay;
|
|
612
|
+
let dismissDeviceOverlay;
|
|
613
|
+
const cleanup = () => {
|
|
614
|
+
dismissAuthOverlay?.();
|
|
615
|
+
dismissAuthOverlay = undefined;
|
|
616
|
+
dismissDeviceOverlay?.();
|
|
617
|
+
dismissDeviceOverlay = undefined;
|
|
618
|
+
};
|
|
619
|
+
const callbacks = {
|
|
620
|
+
onAuth: (info) => {
|
|
621
|
+
authUrl = info.url;
|
|
622
|
+
process.stdout.write(new OscSequence(info.url).toString());
|
|
623
|
+
const body = [
|
|
624
|
+
`Open this URL in your browser:`,
|
|
625
|
+
`(copied to clipboard)`,
|
|
626
|
+
``,
|
|
627
|
+
info.url,
|
|
628
|
+
info.instructions ? `` : "",
|
|
629
|
+
info.instructions ?? "",
|
|
630
|
+
provider.usesCallbackServer ? `` : "",
|
|
631
|
+
provider.usesCallbackServer
|
|
632
|
+
? `On a headless server: after the browser redirects to localhost, copy that URL and paste it when prompted.`
|
|
633
|
+
: "",
|
|
634
|
+
]
|
|
635
|
+
.filter((line) => line.length > 0 || line === "")
|
|
636
|
+
.join("\n");
|
|
637
|
+
dismissAuthOverlay = showInfo(ctx.screen, { title: "Authorize pi-squared", body }).dismiss;
|
|
638
|
+
},
|
|
639
|
+
onDeviceCode: (info) => {
|
|
640
|
+
process.stdout.write(new OscSequence(info.verificationUri).toString());
|
|
641
|
+
const body = [
|
|
642
|
+
`Open: ${info.verificationUri}`,
|
|
643
|
+
`(copied to clipboard)`,
|
|
644
|
+
``,
|
|
645
|
+
`Enter the code: ${info.userCode}`,
|
|
646
|
+
info.expiresInSeconds ? `Expires in ${Math.floor(info.expiresInSeconds / 60)} min` : "",
|
|
647
|
+
]
|
|
648
|
+
.filter((line) => line.length > 0)
|
|
649
|
+
.join("\n");
|
|
650
|
+
dismissDeviceOverlay = showInfo(ctx.screen, { title: "Device authorization", body }).dismiss;
|
|
651
|
+
},
|
|
652
|
+
onPrompt: async (prompt) => {
|
|
653
|
+
const result = await showPrompt(ctx.screen, {
|
|
654
|
+
title: "Login",
|
|
655
|
+
message: prompt.message,
|
|
656
|
+
allowEmpty: prompt.allowEmpty ?? false,
|
|
657
|
+
});
|
|
658
|
+
if (result === undefined) {
|
|
659
|
+
throw new Error("Login cancelled");
|
|
660
|
+
}
|
|
661
|
+
return result;
|
|
662
|
+
},
|
|
663
|
+
onManualCodeInput: async () => {
|
|
664
|
+
dismissAuthOverlay?.();
|
|
665
|
+
dismissAuthOverlay = undefined;
|
|
666
|
+
dismissDeviceOverlay?.();
|
|
667
|
+
dismissDeviceOverlay = undefined;
|
|
668
|
+
const urlNote = authUrl ? `${authUrl}\n\n` : "";
|
|
669
|
+
const result = await showPrompt(ctx.screen, {
|
|
670
|
+
title: "Paste redirect URL",
|
|
671
|
+
message: `${urlNote}Copy the full redirect URL from your browser's address bar and paste it here:`,
|
|
672
|
+
allowEmpty: false,
|
|
673
|
+
});
|
|
674
|
+
if (result === undefined) {
|
|
675
|
+
throw new Error("Login cancelled");
|
|
676
|
+
}
|
|
677
|
+
return result;
|
|
678
|
+
},
|
|
679
|
+
onProgress: (message) => {
|
|
680
|
+
ctx.runtime.setNotice(message, "info");
|
|
681
|
+
},
|
|
682
|
+
onSelect: async (prompt) => {
|
|
683
|
+
return showSelect(ctx.screen, {
|
|
684
|
+
title: prompt.message,
|
|
685
|
+
items: prompt.options.map((option) => ({ value: option.id, label: option.label })),
|
|
686
|
+
});
|
|
687
|
+
},
|
|
688
|
+
};
|
|
689
|
+
return { callbacks, cleanup };
|
|
690
|
+
}
|
|
691
|
+
//# sourceMappingURL=commands.js.map
|