@companion-ai/feynman 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- package/.feynman/SYSTEM.md +62 -0
- package/.feynman/agents/researcher.md +63 -0
- package/.feynman/agents/reviewer.md +84 -0
- package/.feynman/agents/verifier.md +38 -0
- package/.feynman/agents/writer.md +51 -0
- package/.feynman/settings.json +20 -0
- package/.feynman/themes/feynman.json +85 -0
- package/AGENTS.md +53 -0
- package/README.md +99 -0
- package/bin/feynman.js +2 -0
- package/dist/bootstrap/sync.js +98 -0
- package/dist/cli.js +297 -0
- package/dist/config/commands.js +71 -0
- package/dist/config/feynman-config.js +42 -0
- package/dist/config/paths.js +32 -0
- package/dist/feynman-prompt.js +63 -0
- package/dist/index.js +5 -0
- package/dist/model/catalog.js +238 -0
- package/dist/model/commands.js +165 -0
- package/dist/pi/launch.js +31 -0
- package/dist/pi/runtime.js +70 -0
- package/dist/pi/settings.js +101 -0
- package/dist/pi/web-access.js +74 -0
- package/dist/search/commands.js +12 -0
- package/dist/setup/doctor.js +126 -0
- package/dist/setup/preview.js +20 -0
- package/dist/setup/prompts.js +29 -0
- package/dist/setup/setup.js +119 -0
- package/dist/system/executables.js +38 -0
- package/dist/system/promise-polyfill.js +12 -0
- package/dist/ui/terminal.js +53 -0
- package/dist/web-search.js +1 -0
- package/extensions/research-tools/alpha.ts +212 -0
- package/extensions/research-tools/header.ts +379 -0
- package/extensions/research-tools/help.ts +93 -0
- package/extensions/research-tools/preview.ts +233 -0
- package/extensions/research-tools/project.ts +116 -0
- package/extensions/research-tools/session-search.ts +223 -0
- package/extensions/research-tools/shared.ts +46 -0
- package/extensions/research-tools.ts +25 -0
- package/metadata/commands.d.mts +46 -0
- package/metadata/commands.mjs +133 -0
- package/package.json +71 -0
- package/prompts/audit.md +15 -0
- package/prompts/autoresearch.md +63 -0
- package/prompts/compare.md +16 -0
- package/prompts/deepresearch.md +167 -0
- package/prompts/delegate.md +21 -0
- package/prompts/draft.md +16 -0
- package/prompts/jobs.md +16 -0
- package/prompts/lit.md +16 -0
- package/prompts/log.md +14 -0
- package/prompts/replicate.md +22 -0
- package/prompts/review.md +15 -0
- package/prompts/watch.md +14 -0
- package/scripts/patch-embedded-pi.mjs +319 -0
- package/skills/agentcomputer/SKILL.md +108 -0
- package/skills/agentcomputer/references/acp-flow.md +23 -0
- package/skills/agentcomputer/references/cli-cheatsheet.md +68 -0
- package/skills/autoresearch/SKILL.md +12 -0
- package/skills/deep-research/SKILL.md +12 -0
- package/skills/docker/SKILL.md +84 -0
- package/skills/jobs/SKILL.md +10 -0
- package/skills/literature-review/SKILL.md +12 -0
- package/skills/paper-code-audit/SKILL.md +12 -0
- package/skills/paper-writing/SKILL.md +12 -0
- package/skills/peer-review/SKILL.md +12 -0
- package/skills/replication/SKILL.md +14 -0
- package/skills/session-log/SKILL.md +10 -0
- package/skills/source-comparison/SKILL.md +12 -0
- package/skills/watch/SKILL.md +12 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
const PROVIDER_LABELS = {
|
|
3
|
+
anthropic: "Anthropic",
|
|
4
|
+
openai: "OpenAI",
|
|
5
|
+
"openai-codex": "OpenAI Codex",
|
|
6
|
+
openrouter: "OpenRouter",
|
|
7
|
+
google: "Google",
|
|
8
|
+
"google-gemini-cli": "Google Gemini CLI",
|
|
9
|
+
zai: "Z.AI / GLM",
|
|
10
|
+
minimax: "MiniMax",
|
|
11
|
+
"minimax-cn": "MiniMax (China)",
|
|
12
|
+
"github-copilot": "GitHub Copilot",
|
|
13
|
+
"vercel-ai-gateway": "Vercel AI Gateway",
|
|
14
|
+
opencode: "OpenCode",
|
|
15
|
+
"opencode-go": "OpenCode Go",
|
|
16
|
+
"kimi-coding": "Kimi / Moonshot",
|
|
17
|
+
xai: "xAI",
|
|
18
|
+
groq: "Groq",
|
|
19
|
+
mistral: "Mistral",
|
|
20
|
+
cerebras: "Cerebras",
|
|
21
|
+
huggingface: "Hugging Face",
|
|
22
|
+
"amazon-bedrock": "Amazon Bedrock",
|
|
23
|
+
"azure-openai-responses": "Azure OpenAI Responses",
|
|
24
|
+
};
|
|
25
|
+
const RESEARCH_MODEL_PREFERENCES = [
|
|
26
|
+
{
|
|
27
|
+
spec: "anthropic/claude-opus-4-6",
|
|
28
|
+
reason: "strong long-context reasoning for source-heavy research work",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
spec: "anthropic/claude-opus-4-5",
|
|
32
|
+
reason: "strong long-context reasoning for source-heavy research work",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
spec: "anthropic/claude-sonnet-4-6",
|
|
36
|
+
reason: "balanced reasoning and speed for iterative research sessions",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
spec: "anthropic/claude-sonnet-4-5",
|
|
40
|
+
reason: "balanced reasoning and speed for iterative research sessions",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
spec: "openai/gpt-5.4",
|
|
44
|
+
reason: "strong general reasoning and drafting quality for research tasks",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
spec: "openai/gpt-5",
|
|
48
|
+
reason: "strong general reasoning and drafting quality for research tasks",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
spec: "openai-codex/gpt-5.4",
|
|
52
|
+
reason: "strong research + coding balance when Pi exposes Codex directly",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
spec: "google/gemini-3-pro-preview",
|
|
56
|
+
reason: "good fallback for broad web-and-doc research work",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
spec: "google/gemini-2.5-pro",
|
|
60
|
+
reason: "good fallback for broad web-and-doc research work",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
spec: "openrouter/openai/gpt-5.1-codex",
|
|
64
|
+
reason: "good routed fallback when only OpenRouter is configured",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
spec: "zai/glm-5",
|
|
68
|
+
reason: "good fallback when GLM is the available research model",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
spec: "kimi-coding/kimi-k2-thinking",
|
|
72
|
+
reason: "good fallback when Kimi is the available research model",
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
const PROVIDER_SORT_ORDER = [
|
|
76
|
+
"anthropic",
|
|
77
|
+
"openai",
|
|
78
|
+
"openai-codex",
|
|
79
|
+
"google",
|
|
80
|
+
"openrouter",
|
|
81
|
+
"zai",
|
|
82
|
+
"kimi-coding",
|
|
83
|
+
"minimax",
|
|
84
|
+
"minimax-cn",
|
|
85
|
+
"github-copilot",
|
|
86
|
+
"vercel-ai-gateway",
|
|
87
|
+
];
|
|
88
|
+
function formatProviderLabel(provider) {
|
|
89
|
+
return PROVIDER_LABELS[provider] ?? provider;
|
|
90
|
+
}
|
|
91
|
+
function modelSpec(model) {
|
|
92
|
+
return `${model.provider}/${model.id}`;
|
|
93
|
+
}
|
|
94
|
+
function compareByResearchPreference(left, right) {
|
|
95
|
+
const leftSpec = modelSpec(left);
|
|
96
|
+
const rightSpec = modelSpec(right);
|
|
97
|
+
const leftIndex = RESEARCH_MODEL_PREFERENCES.findIndex((entry) => entry.spec === leftSpec);
|
|
98
|
+
const rightIndex = RESEARCH_MODEL_PREFERENCES.findIndex((entry) => entry.spec === rightSpec);
|
|
99
|
+
if (leftIndex !== -1 || rightIndex !== -1) {
|
|
100
|
+
if (leftIndex === -1)
|
|
101
|
+
return 1;
|
|
102
|
+
if (rightIndex === -1)
|
|
103
|
+
return -1;
|
|
104
|
+
return leftIndex - rightIndex;
|
|
105
|
+
}
|
|
106
|
+
const leftProviderIndex = PROVIDER_SORT_ORDER.indexOf(left.provider);
|
|
107
|
+
const rightProviderIndex = PROVIDER_SORT_ORDER.indexOf(right.provider);
|
|
108
|
+
if (leftProviderIndex !== -1 || rightProviderIndex !== -1) {
|
|
109
|
+
if (leftProviderIndex === -1)
|
|
110
|
+
return 1;
|
|
111
|
+
if (rightProviderIndex === -1)
|
|
112
|
+
return -1;
|
|
113
|
+
return leftProviderIndex - rightProviderIndex;
|
|
114
|
+
}
|
|
115
|
+
return modelSpec(left).localeCompare(modelSpec(right));
|
|
116
|
+
}
|
|
117
|
+
function sortProviders(left, right) {
|
|
118
|
+
if (left.configured !== right.configured) {
|
|
119
|
+
return left.configured ? -1 : 1;
|
|
120
|
+
}
|
|
121
|
+
if (left.current !== right.current) {
|
|
122
|
+
return left.current ? -1 : 1;
|
|
123
|
+
}
|
|
124
|
+
if (left.recommended !== right.recommended) {
|
|
125
|
+
return left.recommended ? -1 : 1;
|
|
126
|
+
}
|
|
127
|
+
const leftIndex = PROVIDER_SORT_ORDER.indexOf(left.id);
|
|
128
|
+
const rightIndex = PROVIDER_SORT_ORDER.indexOf(right.id);
|
|
129
|
+
if (leftIndex !== -1 || rightIndex !== -1) {
|
|
130
|
+
if (leftIndex === -1)
|
|
131
|
+
return 1;
|
|
132
|
+
if (rightIndex === -1)
|
|
133
|
+
return -1;
|
|
134
|
+
return leftIndex - rightIndex;
|
|
135
|
+
}
|
|
136
|
+
return left.label.localeCompare(right.label);
|
|
137
|
+
}
|
|
138
|
+
function createModelRegistry(authPath) {
|
|
139
|
+
return new ModelRegistry(AuthStorage.create(authPath));
|
|
140
|
+
}
|
|
141
|
+
export function getAvailableModelRecords(authPath) {
|
|
142
|
+
return createModelRegistry(authPath)
|
|
143
|
+
.getAvailable()
|
|
144
|
+
.map((model) => ({ provider: model.provider, id: model.id, name: model.name }));
|
|
145
|
+
}
|
|
146
|
+
export function getSupportedModelRecords(authPath) {
|
|
147
|
+
return createModelRegistry(authPath)
|
|
148
|
+
.getAll()
|
|
149
|
+
.map((model) => ({ provider: model.provider, id: model.id, name: model.name }));
|
|
150
|
+
}
|
|
151
|
+
export function chooseRecommendedModel(authPath) {
|
|
152
|
+
const available = getAvailableModelRecords(authPath).sort(compareByResearchPreference);
|
|
153
|
+
if (available.length === 0) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
const matchedPreference = RESEARCH_MODEL_PREFERENCES.find((entry) => entry.spec === modelSpec(available[0]));
|
|
157
|
+
if (matchedPreference) {
|
|
158
|
+
return matchedPreference;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
spec: modelSpec(available[0]),
|
|
162
|
+
reason: "best currently authenticated fallback for research work",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export function buildModelStatusSnapshotFromRecords(supported, available, current) {
|
|
166
|
+
const availableSpecs = available
|
|
167
|
+
.slice()
|
|
168
|
+
.sort(compareByResearchPreference)
|
|
169
|
+
.map((model) => modelSpec(model));
|
|
170
|
+
const recommended = available.length > 0
|
|
171
|
+
? (() => {
|
|
172
|
+
const preferred = available.slice().sort(compareByResearchPreference)[0];
|
|
173
|
+
const matched = RESEARCH_MODEL_PREFERENCES.find((entry) => entry.spec === modelSpec(preferred));
|
|
174
|
+
return {
|
|
175
|
+
spec: modelSpec(preferred),
|
|
176
|
+
reason: matched?.reason ?? "best currently authenticated fallback for research work",
|
|
177
|
+
};
|
|
178
|
+
})()
|
|
179
|
+
: undefined;
|
|
180
|
+
const currentValid = current ? availableSpecs.includes(current) : false;
|
|
181
|
+
const providerMap = new Map();
|
|
182
|
+
for (const model of supported) {
|
|
183
|
+
const provider = providerMap.get(model.provider) ?? {
|
|
184
|
+
id: model.provider,
|
|
185
|
+
label: formatProviderLabel(model.provider),
|
|
186
|
+
supportedModels: 0,
|
|
187
|
+
availableModels: 0,
|
|
188
|
+
configured: false,
|
|
189
|
+
current: false,
|
|
190
|
+
recommended: false,
|
|
191
|
+
};
|
|
192
|
+
provider.supportedModels += 1;
|
|
193
|
+
provider.current ||= current?.startsWith(`${model.provider}/`) ?? false;
|
|
194
|
+
provider.recommended ||= recommended?.spec.startsWith(`${model.provider}/`) ?? false;
|
|
195
|
+
providerMap.set(model.provider, provider);
|
|
196
|
+
}
|
|
197
|
+
for (const model of available) {
|
|
198
|
+
const provider = providerMap.get(model.provider) ?? {
|
|
199
|
+
id: model.provider,
|
|
200
|
+
label: formatProviderLabel(model.provider),
|
|
201
|
+
supportedModels: 0,
|
|
202
|
+
availableModels: 0,
|
|
203
|
+
configured: false,
|
|
204
|
+
current: false,
|
|
205
|
+
recommended: false,
|
|
206
|
+
};
|
|
207
|
+
provider.availableModels += 1;
|
|
208
|
+
provider.configured = true;
|
|
209
|
+
provider.current ||= current?.startsWith(`${model.provider}/`) ?? false;
|
|
210
|
+
provider.recommended ||= recommended?.spec.startsWith(`${model.provider}/`) ?? false;
|
|
211
|
+
providerMap.set(model.provider, provider);
|
|
212
|
+
}
|
|
213
|
+
const guidance = [];
|
|
214
|
+
if (available.length === 0) {
|
|
215
|
+
guidance.push("No authenticated Pi models are available yet.");
|
|
216
|
+
guidance.push("Run `feynman model login <provider>` or add provider credentials that Pi can see.");
|
|
217
|
+
guidance.push("After auth is in place, rerun `feynman model list` or `feynman setup model`.");
|
|
218
|
+
}
|
|
219
|
+
else if (!current) {
|
|
220
|
+
guidance.push(`No default research model is set. Recommended: ${recommended?.spec}.`);
|
|
221
|
+
guidance.push("Run `feynman model set <provider/model>` or `feynman setup model`.");
|
|
222
|
+
}
|
|
223
|
+
else if (!currentValid) {
|
|
224
|
+
guidance.push(`Configured default model is unavailable: ${current}.`);
|
|
225
|
+
if (recommended) {
|
|
226
|
+
guidance.push(`Switch to the current research recommendation: ${recommended.spec}.`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
current,
|
|
231
|
+
currentValid,
|
|
232
|
+
recommended: recommended?.spec,
|
|
233
|
+
recommendationReason: recommended?.reason,
|
|
234
|
+
availableModels: availableSpecs,
|
|
235
|
+
providers: Array.from(providerMap.values()).sort(sortProviders),
|
|
236
|
+
guidance,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { AuthStorage } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { readJson } from "../pi/settings.js";
|
|
4
|
+
import { promptChoice, promptText } from "../setup/prompts.js";
|
|
5
|
+
import { printInfo, printSection, printSuccess, printWarning } from "../ui/terminal.js";
|
|
6
|
+
import { buildModelStatusSnapshotFromRecords, getAvailableModelRecords, getSupportedModelRecords, } from "./catalog.js";
|
|
7
|
+
function collectModelStatus(settingsPath, authPath) {
|
|
8
|
+
return buildModelStatusSnapshotFromRecords(getSupportedModelRecords(authPath), getAvailableModelRecords(authPath), getCurrentModelSpec(settingsPath));
|
|
9
|
+
}
|
|
10
|
+
function getOAuthProviders(authPath) {
|
|
11
|
+
return AuthStorage.create(authPath).getOAuthProviders();
|
|
12
|
+
}
|
|
13
|
+
function resolveOAuthProvider(authPath, input) {
|
|
14
|
+
const normalizedInput = input.trim().toLowerCase();
|
|
15
|
+
if (!normalizedInput) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return getOAuthProviders(authPath).find((provider) => provider.id.toLowerCase() === normalizedInput);
|
|
19
|
+
}
|
|
20
|
+
async function selectOAuthProvider(authPath, action) {
|
|
21
|
+
const providers = getOAuthProviders(authPath);
|
|
22
|
+
if (providers.length === 0) {
|
|
23
|
+
printWarning("No Pi OAuth model providers are available.");
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
if (providers.length === 1) {
|
|
27
|
+
return providers[0];
|
|
28
|
+
}
|
|
29
|
+
const choices = providers.map((provider) => `${provider.id} — ${provider.name ?? provider.id}`);
|
|
30
|
+
choices.push("Cancel");
|
|
31
|
+
const selection = await promptChoice(`Choose an OAuth provider to ${action}:`, choices, 0);
|
|
32
|
+
if (selection >= providers.length) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return providers[selection];
|
|
36
|
+
}
|
|
37
|
+
function resolveAvailableModelSpec(authPath, input) {
|
|
38
|
+
const normalizedInput = input.trim().toLowerCase();
|
|
39
|
+
if (!normalizedInput) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const available = getAvailableModelRecords(authPath);
|
|
43
|
+
const fullSpecMatch = available.find((model) => `${model.provider}/${model.id}`.toLowerCase() === normalizedInput);
|
|
44
|
+
if (fullSpecMatch) {
|
|
45
|
+
return `${fullSpecMatch.provider}/${fullSpecMatch.id}`;
|
|
46
|
+
}
|
|
47
|
+
const exactIdMatches = available.filter((model) => model.id.toLowerCase() === normalizedInput);
|
|
48
|
+
if (exactIdMatches.length === 1) {
|
|
49
|
+
return `${exactIdMatches[0].provider}/${exactIdMatches[0].id}`;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
export function getCurrentModelSpec(settingsPath) {
|
|
54
|
+
const settings = readJson(settingsPath);
|
|
55
|
+
if (typeof settings.defaultProvider === "string" && typeof settings.defaultModel === "string") {
|
|
56
|
+
return `${settings.defaultProvider}/${settings.defaultModel}`;
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
export function printModelList(settingsPath, authPath) {
|
|
61
|
+
const status = collectModelStatus(settingsPath, authPath);
|
|
62
|
+
if (status.availableModels.length === 0) {
|
|
63
|
+
printWarning("No authenticated Pi models are currently available.");
|
|
64
|
+
for (const line of status.guidance) {
|
|
65
|
+
printInfo(line);
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
let lastProvider;
|
|
70
|
+
for (const spec of status.availableModels) {
|
|
71
|
+
const [provider] = spec.split("/", 1);
|
|
72
|
+
if (provider !== lastProvider) {
|
|
73
|
+
lastProvider = provider;
|
|
74
|
+
printSection(provider);
|
|
75
|
+
}
|
|
76
|
+
const markers = [
|
|
77
|
+
spec === status.current ? "current" : undefined,
|
|
78
|
+
spec === status.recommended ? "recommended" : undefined,
|
|
79
|
+
].filter(Boolean);
|
|
80
|
+
printInfo(`${spec}${markers.length > 0 ? ` (${markers.join(", ")})` : ""}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export async function loginModelProvider(authPath, providerId) {
|
|
84
|
+
const provider = providerId ? resolveOAuthProvider(authPath, providerId) : await selectOAuthProvider(authPath, "login");
|
|
85
|
+
if (!provider) {
|
|
86
|
+
if (providerId) {
|
|
87
|
+
throw new Error(`Unknown OAuth model provider: ${providerId}`);
|
|
88
|
+
}
|
|
89
|
+
printInfo("Login cancelled.");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const authStorage = AuthStorage.create(authPath);
|
|
93
|
+
const abortController = new AbortController();
|
|
94
|
+
await authStorage.login(provider.id, {
|
|
95
|
+
onAuth: (info) => {
|
|
96
|
+
printSection(`Login: ${provider.name ?? provider.id}`);
|
|
97
|
+
printInfo(`Open this URL: ${info.url}`);
|
|
98
|
+
if (info.instructions) {
|
|
99
|
+
printInfo(info.instructions);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
onPrompt: async (prompt) => {
|
|
103
|
+
return promptText(prompt.message, prompt.placeholder ?? "");
|
|
104
|
+
},
|
|
105
|
+
onProgress: (message) => {
|
|
106
|
+
printInfo(message);
|
|
107
|
+
},
|
|
108
|
+
onManualCodeInput: async () => {
|
|
109
|
+
return promptText("Paste redirect URL or auth code");
|
|
110
|
+
},
|
|
111
|
+
signal: abortController.signal,
|
|
112
|
+
});
|
|
113
|
+
printSuccess(`Model provider login complete: ${provider.id}`);
|
|
114
|
+
}
|
|
115
|
+
export async function logoutModelProvider(authPath, providerId) {
|
|
116
|
+
const provider = providerId ? resolveOAuthProvider(authPath, providerId) : await selectOAuthProvider(authPath, "logout");
|
|
117
|
+
if (!provider) {
|
|
118
|
+
if (providerId) {
|
|
119
|
+
throw new Error(`Unknown OAuth model provider: ${providerId}`);
|
|
120
|
+
}
|
|
121
|
+
printInfo("Logout cancelled.");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
AuthStorage.create(authPath).logout(provider.id);
|
|
125
|
+
printSuccess(`Model provider logout complete: ${provider.id}`);
|
|
126
|
+
}
|
|
127
|
+
export function setDefaultModelSpec(settingsPath, authPath, spec) {
|
|
128
|
+
const resolvedSpec = resolveAvailableModelSpec(authPath, spec);
|
|
129
|
+
if (!resolvedSpec) {
|
|
130
|
+
throw new Error(`Model not available in Pi auth storage: ${spec}. Run \`feynman model list\` first.`);
|
|
131
|
+
}
|
|
132
|
+
const [provider, ...rest] = resolvedSpec.split("/");
|
|
133
|
+
const modelId = rest.join("/");
|
|
134
|
+
const settings = readJson(settingsPath);
|
|
135
|
+
settings.defaultProvider = provider;
|
|
136
|
+
settings.defaultModel = modelId;
|
|
137
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
138
|
+
printSuccess(`Default model set to ${resolvedSpec}`);
|
|
139
|
+
}
|
|
140
|
+
export async function runModelSetup(settingsPath, authPath) {
|
|
141
|
+
const status = collectModelStatus(settingsPath, authPath);
|
|
142
|
+
if (status.availableModels.length === 0) {
|
|
143
|
+
printWarning("No Pi models are currently authenticated for Feynman.");
|
|
144
|
+
for (const line of status.guidance) {
|
|
145
|
+
printInfo(line);
|
|
146
|
+
}
|
|
147
|
+
printInfo("Tip: run `feynman model login <provider>` if your provider supports Pi OAuth login.");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const choices = status.availableModels.map((spec) => {
|
|
151
|
+
const markers = [
|
|
152
|
+
spec === status.recommended ? "recommended" : undefined,
|
|
153
|
+
spec === status.current ? "current" : undefined,
|
|
154
|
+
].filter(Boolean);
|
|
155
|
+
return `${spec}${markers.length > 0 ? ` (${markers.join(", ")})` : ""}`;
|
|
156
|
+
});
|
|
157
|
+
choices.push(`Keep current (${status.current ?? "unset"})`);
|
|
158
|
+
const defaultIndex = status.current ? Math.max(0, status.availableModels.indexOf(status.current)) : 0;
|
|
159
|
+
const selection = await promptChoice("Select your default research model:", choices, defaultIndex >= 0 ? defaultIndex : 0);
|
|
160
|
+
if (selection >= status.availableModels.length) {
|
|
161
|
+
printInfo("Skipped (keeping current model)");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
setDefaultModelSpec(settingsPath, authPath, status.availableModels[selection]);
|
|
165
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { buildPiArgs, buildPiEnv, resolvePiPaths } from "./runtime.js";
|
|
4
|
+
export async function launchPiChat(options) {
|
|
5
|
+
const { piCliPath, promisePolyfillPath } = resolvePiPaths(options.appRoot);
|
|
6
|
+
if (!existsSync(piCliPath)) {
|
|
7
|
+
throw new Error(`Pi CLI not found: ${piCliPath}`);
|
|
8
|
+
}
|
|
9
|
+
if (!existsSync(promisePolyfillPath)) {
|
|
10
|
+
throw new Error(`Promise polyfill not found: ${promisePolyfillPath}`);
|
|
11
|
+
}
|
|
12
|
+
if (process.stdout.isTTY) {
|
|
13
|
+
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
14
|
+
}
|
|
15
|
+
const child = spawn(process.execPath, ["--import", promisePolyfillPath, piCliPath, ...buildPiArgs(options)], {
|
|
16
|
+
cwd: options.workingDir,
|
|
17
|
+
stdio: "inherit",
|
|
18
|
+
env: buildPiEnv(options),
|
|
19
|
+
});
|
|
20
|
+
await new Promise((resolvePromise, reject) => {
|
|
21
|
+
child.on("error", reject);
|
|
22
|
+
child.on("exit", (code, signal) => {
|
|
23
|
+
if (signal) {
|
|
24
|
+
process.kill(process.pid, signal);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
process.exitCode = code ?? 0;
|
|
28
|
+
resolvePromise();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { BROWSER_FALLBACK_PATHS, MERMAID_FALLBACK_PATHS, PANDOC_FALLBACK_PATHS, resolveExecutable, } from "../system/executables.js";
|
|
4
|
+
export function resolvePiPaths(appRoot) {
|
|
5
|
+
return {
|
|
6
|
+
piPackageRoot: resolve(appRoot, "node_modules", "@mariozechner", "pi-coding-agent"),
|
|
7
|
+
piCliPath: resolve(appRoot, "node_modules", "@mariozechner", "pi-coding-agent", "dist", "cli.js"),
|
|
8
|
+
promisePolyfillPath: resolve(appRoot, "dist", "system", "promise-polyfill.js"),
|
|
9
|
+
researchToolsPath: resolve(appRoot, "extensions", "research-tools.ts"),
|
|
10
|
+
promptTemplatePath: resolve(appRoot, "prompts"),
|
|
11
|
+
systemPromptPath: resolve(appRoot, ".feynman", "SYSTEM.md"),
|
|
12
|
+
piWorkspaceNodeModulesPath: resolve(appRoot, ".feynman", "npm", "node_modules"),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function validatePiInstallation(appRoot) {
|
|
16
|
+
const paths = resolvePiPaths(appRoot);
|
|
17
|
+
const missing = [];
|
|
18
|
+
if (!existsSync(paths.piCliPath))
|
|
19
|
+
missing.push(paths.piCliPath);
|
|
20
|
+
if (!existsSync(paths.promisePolyfillPath))
|
|
21
|
+
missing.push(paths.promisePolyfillPath);
|
|
22
|
+
if (!existsSync(paths.researchToolsPath))
|
|
23
|
+
missing.push(paths.researchToolsPath);
|
|
24
|
+
if (!existsSync(paths.promptTemplatePath))
|
|
25
|
+
missing.push(paths.promptTemplatePath);
|
|
26
|
+
return missing;
|
|
27
|
+
}
|
|
28
|
+
export function buildPiArgs(options) {
|
|
29
|
+
const paths = resolvePiPaths(options.appRoot);
|
|
30
|
+
const args = [
|
|
31
|
+
"--session-dir",
|
|
32
|
+
options.sessionDir,
|
|
33
|
+
"--extension",
|
|
34
|
+
paths.researchToolsPath,
|
|
35
|
+
"--prompt-template",
|
|
36
|
+
paths.promptTemplatePath,
|
|
37
|
+
];
|
|
38
|
+
if (existsSync(paths.systemPromptPath)) {
|
|
39
|
+
args.push("--system-prompt", readFileSync(paths.systemPromptPath, "utf8"));
|
|
40
|
+
}
|
|
41
|
+
if (options.explicitModelSpec) {
|
|
42
|
+
args.push("--model", options.explicitModelSpec);
|
|
43
|
+
}
|
|
44
|
+
if (options.thinkingLevel) {
|
|
45
|
+
args.push("--thinking", options.thinkingLevel);
|
|
46
|
+
}
|
|
47
|
+
if (options.oneShotPrompt) {
|
|
48
|
+
args.push("-p", options.oneShotPrompt);
|
|
49
|
+
}
|
|
50
|
+
else if (options.initialPrompt) {
|
|
51
|
+
args.push(options.initialPrompt);
|
|
52
|
+
}
|
|
53
|
+
return args;
|
|
54
|
+
}
|
|
55
|
+
export function buildPiEnv(options) {
|
|
56
|
+
const paths = resolvePiPaths(options.appRoot);
|
|
57
|
+
return {
|
|
58
|
+
...process.env,
|
|
59
|
+
FEYNMAN_VERSION: options.feynmanVersion,
|
|
60
|
+
FEYNMAN_SESSION_DIR: options.sessionDir,
|
|
61
|
+
FEYNMAN_MEMORY_DIR: resolve(dirname(options.feynmanAgentDir), "memory"),
|
|
62
|
+
FEYNMAN_NODE_EXECUTABLE: process.execPath,
|
|
63
|
+
FEYNMAN_BIN_PATH: resolve(options.appRoot, "bin", "feynman.js"),
|
|
64
|
+
PANDOC_PATH: process.env.PANDOC_PATH ?? resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS),
|
|
65
|
+
PI_HARDWARE_CURSOR: process.env.PI_HARDWARE_CURSOR ?? "1",
|
|
66
|
+
PI_SKIP_VERSION_CHECK: process.env.PI_SKIP_VERSION_CHECK ?? "1",
|
|
67
|
+
MERMAID_CLI_PATH: process.env.MERMAID_CLI_PATH ?? resolveExecutable("mmdc", MERMAID_FALLBACK_PATHS),
|
|
68
|
+
PUPPETEER_EXECUTABLE_PATH: process.env.PUPPETEER_EXECUTABLE_PATH ?? resolveExecutable("google-chrome", BROWSER_FALLBACK_PATHS),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
export function parseModelSpec(spec, modelRegistry) {
|
|
5
|
+
const trimmed = spec.trim();
|
|
6
|
+
const separator = trimmed.includes(":") ? ":" : trimmed.includes("/") ? "/" : null;
|
|
7
|
+
if (!separator) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const [provider, ...rest] = trimmed.split(separator);
|
|
11
|
+
const id = rest.join(separator);
|
|
12
|
+
if (!provider || !id) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
return modelRegistry.find(provider, id);
|
|
16
|
+
}
|
|
17
|
+
export function normalizeThinkingLevel(value) {
|
|
18
|
+
if (!value) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const normalized = value.toLowerCase();
|
|
22
|
+
if (normalized === "off" ||
|
|
23
|
+
normalized === "minimal" ||
|
|
24
|
+
normalized === "low" ||
|
|
25
|
+
normalized === "medium" ||
|
|
26
|
+
normalized === "high" ||
|
|
27
|
+
normalized === "xhigh") {
|
|
28
|
+
return normalized;
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
function choosePreferredModel(availableModels) {
|
|
33
|
+
const preferences = [
|
|
34
|
+
{ provider: "anthropic", id: "claude-opus-4-6" },
|
|
35
|
+
{ provider: "anthropic", id: "claude-opus-4-5" },
|
|
36
|
+
{ provider: "anthropic", id: "claude-sonnet-4-5" },
|
|
37
|
+
{ provider: "openai", id: "gpt-5.4" },
|
|
38
|
+
{ provider: "openai", id: "gpt-5" },
|
|
39
|
+
];
|
|
40
|
+
for (const preferred of preferences) {
|
|
41
|
+
const match = availableModels.find((model) => model.provider === preferred.provider && model.id === preferred.id);
|
|
42
|
+
if (match) {
|
|
43
|
+
return match;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return availableModels[0];
|
|
47
|
+
}
|
|
48
|
+
export function readJson(path) {
|
|
49
|
+
if (!existsSync(path)) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function normalizeFeynmanSettings(settingsPath, bundledSettingsPath, defaultThinkingLevel, authPath) {
|
|
60
|
+
let settings = {};
|
|
61
|
+
if (existsSync(settingsPath)) {
|
|
62
|
+
try {
|
|
63
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
settings = {};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (existsSync(bundledSettingsPath)) {
|
|
70
|
+
try {
|
|
71
|
+
settings = JSON.parse(readFileSync(bundledSettingsPath, "utf8"));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
settings = {};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!settings.defaultThinkingLevel) {
|
|
78
|
+
settings.defaultThinkingLevel = defaultThinkingLevel;
|
|
79
|
+
}
|
|
80
|
+
if (settings.editorPaddingX === undefined) {
|
|
81
|
+
settings.editorPaddingX = 1;
|
|
82
|
+
}
|
|
83
|
+
settings.theme = "feynman";
|
|
84
|
+
settings.quietStartup = true;
|
|
85
|
+
settings.collapseChangelog = true;
|
|
86
|
+
const authStorage = AuthStorage.create(authPath);
|
|
87
|
+
const modelRegistry = new ModelRegistry(authStorage);
|
|
88
|
+
const availableModels = modelRegistry.getAvailable().map((model) => ({
|
|
89
|
+
provider: model.provider,
|
|
90
|
+
id: model.id,
|
|
91
|
+
}));
|
|
92
|
+
if ((!settings.defaultProvider || !settings.defaultModel) && availableModels.length > 0) {
|
|
93
|
+
const preferredModel = choosePreferredModel(availableModels);
|
|
94
|
+
if (preferredModel) {
|
|
95
|
+
settings.defaultProvider = preferredModel.provider;
|
|
96
|
+
settings.defaultModel = preferredModel.id;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
100
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
101
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
export function getPiWebSearchConfigPath(home = process.env.HOME ?? homedir()) {
|
|
5
|
+
return resolve(home, ".feynman", "web-search.json");
|
|
6
|
+
}
|
|
7
|
+
function normalizeProvider(value) {
|
|
8
|
+
return value === "auto" || value === "perplexity" || value === "gemini" ? value : undefined;
|
|
9
|
+
}
|
|
10
|
+
function normalizeNonEmptyString(value) {
|
|
11
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
12
|
+
}
|
|
13
|
+
export function loadPiWebAccessConfig(configPath = getPiWebSearchConfigPath()) {
|
|
14
|
+
if (!existsSync(configPath)) {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
19
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function formatRouteLabel(provider) {
|
|
26
|
+
switch (provider) {
|
|
27
|
+
case "perplexity":
|
|
28
|
+
return "Perplexity";
|
|
29
|
+
case "gemini":
|
|
30
|
+
return "Gemini";
|
|
31
|
+
default:
|
|
32
|
+
return "Auto";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function formatRouteNote(provider) {
|
|
36
|
+
switch (provider) {
|
|
37
|
+
case "perplexity":
|
|
38
|
+
return "Pi web-access will use Perplexity for search.";
|
|
39
|
+
case "gemini":
|
|
40
|
+
return "Pi web-access will use Gemini API or Gemini Browser.";
|
|
41
|
+
default:
|
|
42
|
+
return "Pi web-access will try Perplexity, then Gemini API, then Gemini Browser.";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function getPiWebAccessStatus(config = loadPiWebAccessConfig(), configPath = getPiWebSearchConfigPath()) {
|
|
46
|
+
const searchProvider = normalizeProvider(config.searchProvider) ?? "auto";
|
|
47
|
+
const requestProvider = normalizeProvider(config.provider) ?? searchProvider;
|
|
48
|
+
const perplexityConfigured = Boolean(normalizeNonEmptyString(config.perplexityApiKey));
|
|
49
|
+
const geminiApiConfigured = Boolean(normalizeNonEmptyString(config.geminiApiKey));
|
|
50
|
+
const chromeProfile = normalizeNonEmptyString(config.chromeProfile);
|
|
51
|
+
const effectiveProvider = searchProvider;
|
|
52
|
+
return {
|
|
53
|
+
configPath,
|
|
54
|
+
searchProvider,
|
|
55
|
+
requestProvider,
|
|
56
|
+
perplexityConfigured,
|
|
57
|
+
geminiApiConfigured,
|
|
58
|
+
chromeProfile,
|
|
59
|
+
routeLabel: formatRouteLabel(effectiveProvider),
|
|
60
|
+
note: formatRouteNote(effectiveProvider),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function formatPiWebAccessDoctorLines(status = getPiWebAccessStatus()) {
|
|
64
|
+
return [
|
|
65
|
+
"web access: pi-web-access",
|
|
66
|
+
` search route: ${status.routeLabel}`,
|
|
67
|
+
` request route: ${status.requestProvider}`,
|
|
68
|
+
` perplexity api: ${status.perplexityConfigured ? "configured" : "not configured"}`,
|
|
69
|
+
` gemini api: ${status.geminiApiConfigured ? "configured" : "not configured"}`,
|
|
70
|
+
` browser profile: ${status.chromeProfile ?? "default Chromium profile"}`,
|
|
71
|
+
` config path: ${status.configPath}`,
|
|
72
|
+
` note: ${status.note}`,
|
|
73
|
+
];
|
|
74
|
+
}
|