Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d30506c82a | ||
|
|
c3f7f6ec08 | ||
|
|
d2570188f9 |
@@ -15,6 +15,8 @@ Operating rules:
|
|||||||
- Never answer a latest/current question from arXiv or alpha-backed paper search alone.
|
- Never answer a latest/current question from arXiv or alpha-backed paper search alone.
|
||||||
- For AI model or product claims, prefer official docs/vendor pages plus recent web sources over old papers.
|
- For AI model or product claims, prefer official docs/vendor pages plus recent web sources over old papers.
|
||||||
- Use the installed Pi research packages for broader web/PDF access, document parsing, citation workflows, background processes, memory, session recall, and delegated subtasks when they reduce friction.
|
- Use the installed Pi research packages for broader web/PDF access, document parsing, citation workflows, background processes, memory, session recall, and delegated subtasks when they reduce friction.
|
||||||
|
- You are running inside the Feynman/Pi runtime with filesystem tools, package tools, and configured extensions. Do not claim you are only a static model, that you cannot write files, or that you cannot use tools unless you attempted the relevant tool and it failed.
|
||||||
|
- If a tool, package, source, or network route is unavailable, record the specific failed capability and still write the requested durable artifact with a clear `Blocked / Unverified` status instead of stopping with chat-only prose.
|
||||||
- Feynman ships project subagents for research work. Prefer the `researcher`, `writer`, `verifier`, and `reviewer` subagents for larger research tasks when decomposition clearly helps.
|
- Feynman ships project subagents for research work. Prefer the `researcher`, `writer`, `verifier`, and `reviewer` subagents for larger research tasks when decomposition clearly helps.
|
||||||
- Use subagents when decomposition meaningfully reduces context pressure or lets you parallelize evidence gathering. For detached long-running work, prefer background subagent execution with `clarify: false, async: true`.
|
- Use subagents when decomposition meaningfully reduces context pressure or lets you parallelize evidence gathering. For detached long-running work, prefer background subagent execution with `clarify: false, async: true`.
|
||||||
- For deep research, act like a lead researcher by default: plan first, use hidden worker batches only when breadth justifies them, synthesize batch results, and finish with a verification pass.
|
- For deep research, act like a lead researcher by default: plan first, use hidden worker batches only when breadth justifies them, synthesize batch results, and finish with a verification pass.
|
||||||
@@ -44,6 +46,7 @@ Operating rules:
|
|||||||
- When citing papers from alpha-backed tools, prefer direct arXiv or alphaXiv links and include the arXiv ID.
|
- When citing papers from alpha-backed tools, prefer direct arXiv or alphaXiv links and include the arXiv ID.
|
||||||
- Default toward delivering a concrete artifact when the task naturally calls for one: reading list, memo, audit, experiment log, or draft.
|
- Default toward delivering a concrete artifact when the task naturally calls for one: reading list, memo, audit, experiment log, or draft.
|
||||||
- For user-facing workflows, produce exactly one canonical durable Markdown artifact unless the user explicitly asks for multiple deliverables.
|
- For user-facing workflows, produce exactly one canonical durable Markdown artifact unless the user explicitly asks for multiple deliverables.
|
||||||
|
- If a workflow requests a durable artifact, verify the file exists on disk before the final response. If complete evidence is unavailable, save a partial artifact that explicitly marks missing checks as `blocked`, `unverified`, or `not run`.
|
||||||
- Do not create extra user-facing intermediate markdown files just because the workflow has multiple reasoning stages.
|
- Do not create extra user-facing intermediate markdown files just because the workflow has multiple reasoning stages.
|
||||||
- Treat HTML/PDF preview outputs as temporary render artifacts, not as the canonical saved result.
|
- Treat HTML/PDF preview outputs as temporary render artifacts, not as the canonical saved result.
|
||||||
- Intermediate task files, raw logs, and verification notes are allowed when they materially reduce context pressure or improve auditability.
|
- Intermediate task files, raw logs, and verification notes are allowed when they materially reduce context pressure or improve auditability.
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ curl -fsSL https://feynman.is/install | bash
|
|||||||
irm https://feynman.is/install.ps1 | iex
|
irm https://feynman.is/install.ps1 | iex
|
||||||
```
|
```
|
||||||
|
|
||||||
The one-line installer fetches the latest tagged release. To pin a version, pass it explicitly, for example `curl -fsSL https://feynman.is/install | bash -s -- 0.2.21`.
|
The one-line installer fetches the latest tagged release. To pin a version, pass it explicitly, for example `curl -fsSL https://feynman.is/install | bash -s -- 0.2.24`.
|
||||||
|
|
||||||
The installer downloads a standalone native bundle with its own Node.js runtime.
|
The installer downloads a standalone native bundle with its own Node.js runtime.
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ To upgrade the standalone app later, rerun the installer. `feynman update` only
|
|||||||
|
|
||||||
To uninstall the standalone app, remove the launcher and runtime bundle, then optionally remove `~/.feynman` if you also want to delete settings, sessions, and installed package state. If you also want to delete alphaXiv login state, remove `~/.ahub`. See the installation guide for platform-specific paths.
|
To uninstall the standalone app, remove the launcher and runtime bundle, then optionally remove `~/.feynman` if you also want to delete settings, sessions, and installed package state. If you also want to delete alphaXiv login state, remove `~/.ahub`. See the installation guide for platform-specific paths.
|
||||||
|
|
||||||
Local models are supported through the custom-provider flow. For Ollama, run `feynman setup`, choose `Custom provider (baseUrl + API key)`, use `openai-completions`, and point it at `http://localhost:11434/v1`.
|
Local models are supported through the setup flow. For LM Studio, run `feynman setup`, choose `LM Studio`, and keep the default `http://localhost:1234/v1` unless you changed the server port. For Ollama or vLLM, choose `Custom provider (baseUrl + API key)`, use `openai-completions`, and point it at the local `/v1` endpoint.
|
||||||
|
|
||||||
### Skills Only
|
### Skills Only
|
||||||
|
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@companion-ai/feynman",
|
"name": "@companion-ai/feynman",
|
||||||
"version": "0.2.21",
|
"version": "0.2.24",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@companion-ai/feynman",
|
"name": "@companion-ai/feynman",
|
||||||
"version": "0.2.21",
|
"version": "0.2.24",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@companion-ai/feynman",
|
"name": "@companion-ai/feynman",
|
||||||
"version": "0.2.21",
|
"version": "0.2.24",
|
||||||
"description": "Research-first CLI agent built on Pi and alphaXiv",
|
"description": "Research-first CLI agent built on Pi and alphaXiv",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ Also save the plan with `memory_remember` (type: `fact`, key: `deepresearch.<slu
|
|||||||
|
|
||||||
Present the plan to the user. If this is an unattended or one-shot run, continue automatically. If the user is actively interacting in the terminal, give them a brief chance to request plan changes before proceeding.
|
Present the plan to the user. If this is an unattended or one-shot run, continue automatically. If the user is actively interacting in the terminal, give them a brief chance to request plan changes before proceeding.
|
||||||
|
|
||||||
|
Do not stop after planning. If live search, subagents, web access, alphaXiv, or any other capability is unavailable, continue in degraded mode and write a durable blocked/partial report that records exactly which capabilities failed.
|
||||||
|
|
||||||
## 2. Scale decision
|
## 2. Scale decision
|
||||||
|
|
||||||
| Query type | Execution |
|
| Query type | Execution |
|
||||||
@@ -105,6 +107,13 @@ When the work spans multiple rounds, also append a concise chronological entry t
|
|||||||
|
|
||||||
Most topics need 1-2 rounds. Stop when additional rounds would not materially change conclusions.
|
Most topics need 1-2 rounds. Stop when additional rounds would not materially change conclusions.
|
||||||
|
|
||||||
|
If no researcher files can be produced because tools, subagents, or network access failed, create `outputs/.drafts/<slug>-draft.md` yourself as a blocked report with:
|
||||||
|
- what was requested,
|
||||||
|
- which capabilities failed,
|
||||||
|
- what evidence was and was not gathered,
|
||||||
|
- a proposed source-gathering plan,
|
||||||
|
- no invented sources or results.
|
||||||
|
|
||||||
## 5. Write the report
|
## 5. Write the report
|
||||||
|
|
||||||
Once evidence is sufficient, YOU write the full research brief directly. Do not delegate writing to another agent. Read the research files, synthesize the findings, and produce a complete document:
|
Once evidence is sufficient, YOU write the full research brief directly. Do not delegate writing to another agent. Read the research files, synthesize the findings, and produce a complete document:
|
||||||
@@ -190,6 +199,7 @@ Before you stop, verify on disk that all of these exist:
|
|||||||
- `outputs/<slug>.provenance.md` or `papers/<slug>.provenance.md` provenance sidecar
|
- `outputs/<slug>.provenance.md` or `papers/<slug>.provenance.md` provenance sidecar
|
||||||
|
|
||||||
Do not stop at `<slug>-brief.md` alone. If the cited brief exists but the promoted final output or provenance sidecar does not, create them before responding.
|
Do not stop at `<slug>-brief.md` alone. If the cited brief exists but the promoted final output or provenance sidecar does not, create them before responding.
|
||||||
|
If full verification could not be completed, still create the final deliverable and provenance sidecar with `Verification: BLOCKED` or `PASS WITH NOTES` and list the missing checks. Never end with only an explanation in chat.
|
||||||
|
|
||||||
## Background execution
|
## Background execution
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ This usually means the release exists, but not all platform bundles were uploade
|
|||||||
Workarounds:
|
Workarounds:
|
||||||
- try again after the release finishes publishing
|
- try again after the release finishes publishing
|
||||||
- pass the latest published version explicitly, e.g.:
|
- pass the latest published version explicitly, e.g.:
|
||||||
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.21
|
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.24
|
||||||
"@
|
"@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ This usually means the release exists, but not all platform bundles were uploade
|
|||||||
Workarounds:
|
Workarounds:
|
||||||
- try again after the release finishes publishing
|
- try again after the release finishes publishing
|
||||||
- pass the latest published version explicitly, e.g.:
|
- pass the latest published version explicitly, e.g.:
|
||||||
curl -fsSL https://feynman.is/install | bash -s -- 0.2.21
|
curl -fsSL https://feynman.is/install | bash -s -- 0.2.24
|
||||||
EOF
|
EOF
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||||
import { createRequire } from "node:module";
|
import { createRequire } from "node:module";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { delimiter, dirname, resolve } from "node:path";
|
import { delimiter, dirname, resolve } from "node:path";
|
||||||
@@ -286,28 +286,53 @@ function linkPointsTo(linkPath, targetPath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function listWorkspacePackageNames(root) {
|
||||||
|
if (!existsSync(root)) return [];
|
||||||
|
const names = [];
|
||||||
|
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
||||||
|
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
||||||
|
if (entry.name.startsWith(".")) continue;
|
||||||
|
if (entry.name.startsWith("@")) {
|
||||||
|
const scopeRoot = resolve(root, entry.name);
|
||||||
|
for (const scopedEntry of readdirSync(scopeRoot, { withFileTypes: true })) {
|
||||||
|
if (!scopedEntry.isDirectory() && !scopedEntry.isSymbolicLink()) continue;
|
||||||
|
names.push(`${entry.name}/${scopedEntry.name}`);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
names.push(entry.name);
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
function linkBundledPackage(packageName) {
|
||||||
|
const sourcePath = resolve(workspaceRoot, packageName);
|
||||||
|
const targetPath = resolve(globalNodeModulesRoot, packageName);
|
||||||
|
if (!existsSync(sourcePath)) return false;
|
||||||
|
if (linkPointsTo(targetPath, sourcePath)) return false;
|
||||||
|
try {
|
||||||
|
if (lstatSync(targetPath).isSymbolicLink()) {
|
||||||
|
rmSync(targetPath, { force: true });
|
||||||
|
} else if (!installedPackageLooksUsable(targetPath, globalNodeModulesRoot)) {
|
||||||
|
rmSync(targetPath, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
if (existsSync(targetPath)) return false;
|
||||||
|
|
||||||
|
ensureParentDir(targetPath);
|
||||||
|
try {
|
||||||
|
symlinkSync(sourcePath, targetPath, process.platform === "win32" ? "junction" : "dir");
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function ensureBundledPackageLinks(packageSpecs) {
|
function ensureBundledPackageLinks(packageSpecs) {
|
||||||
if (!workspaceMatchesRuntime(packageSpecs)) return;
|
if (!workspaceMatchesRuntime(packageSpecs)) return;
|
||||||
|
|
||||||
for (const spec of packageSpecs) {
|
for (const packageName of listWorkspacePackageNames(workspaceRoot)) {
|
||||||
const packageName = parsePackageName(spec);
|
linkBundledPackage(packageName);
|
||||||
const sourcePath = resolve(workspaceRoot, packageName);
|
|
||||||
const targetPath = resolve(globalNodeModulesRoot, packageName);
|
|
||||||
if (!existsSync(sourcePath)) continue;
|
|
||||||
if (linkPointsTo(targetPath, sourcePath)) continue;
|
|
||||||
try {
|
|
||||||
if (lstatSync(targetPath).isSymbolicLink()) {
|
|
||||||
rmSync(targetPath, { force: true });
|
|
||||||
} else if (!installedPackageLooksUsable(targetPath, globalNodeModulesRoot)) {
|
|
||||||
rmSync(targetPath, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
if (existsSync(targetPath)) continue;
|
|
||||||
|
|
||||||
ensureParentDir(targetPath);
|
|
||||||
try {
|
|
||||||
symlinkSync(sourcePath, targetPath, process.platform === "win32" ? "junction" : "dir");
|
|
||||||
} catch {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ const API_KEY_PROVIDERS: ApiKeyProviderInfo[] = [
|
|||||||
{ id: "openai", label: "OpenAI Platform API", envVar: "OPENAI_API_KEY" },
|
{ id: "openai", label: "OpenAI Platform API", envVar: "OPENAI_API_KEY" },
|
||||||
{ id: "anthropic", label: "Anthropic API", envVar: "ANTHROPIC_API_KEY" },
|
{ id: "anthropic", label: "Anthropic API", envVar: "ANTHROPIC_API_KEY" },
|
||||||
{ id: "google", label: "Google Gemini API", envVar: "GEMINI_API_KEY" },
|
{ id: "google", label: "Google Gemini API", envVar: "GEMINI_API_KEY" },
|
||||||
|
{ id: "lm-studio", label: "LM Studio (local OpenAI-compatible server)" },
|
||||||
{ id: "__custom__", label: "Custom provider (local/self-hosted/proxy)" },
|
{ id: "__custom__", label: "Custom provider (local/self-hosted/proxy)" },
|
||||||
{ id: "amazon-bedrock", label: "Amazon Bedrock (AWS credential chain)" },
|
{ id: "amazon-bedrock", label: "Amazon Bedrock (AWS credential chain)" },
|
||||||
{ id: "openrouter", label: "OpenRouter", envVar: "OPENROUTER_API_KEY" },
|
{ id: "openrouter", label: "OpenRouter", envVar: "OPENROUTER_API_KEY" },
|
||||||
@@ -132,6 +133,8 @@ async function selectApiKeyProvider(): Promise<ApiKeyProviderInfo | undefined> {
|
|||||||
label: provider.label,
|
label: provider.label,
|
||||||
hint: provider.id === "__custom__"
|
hint: provider.id === "__custom__"
|
||||||
? "Ollama, vLLM, LM Studio, proxies"
|
? "Ollama, vLLM, LM Studio, proxies"
|
||||||
|
: provider.id === "lm-studio"
|
||||||
|
? "http://localhost:1234/v1"
|
||||||
: provider.envVar ?? provider.id,
|
: provider.envVar ?? provider.id,
|
||||||
}));
|
}));
|
||||||
options.push({ value: "cancel", label: "Cancel" });
|
options.push({ value: "cancel", label: "Cancel" });
|
||||||
@@ -362,6 +365,44 @@ async function promptCustomProviderSetup(): Promise<CustomProviderSetup | undefi
|
|||||||
return { providerId, modelIds, baseUrl, api, apiKeyConfig, authHeader };
|
return { providerId, modelIds, baseUrl, api, apiKeyConfig, authHeader };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function promptLmStudioProviderSetup(): Promise<CustomProviderSetup | undefined> {
|
||||||
|
printSection("LM Studio");
|
||||||
|
printInfo("Start the LM Studio local server first, then load a model.");
|
||||||
|
|
||||||
|
const baseUrlRaw = await promptText("Base URL", "http://localhost:1234/v1");
|
||||||
|
const { baseUrl } = normalizeCustomProviderBaseUrl("openai-completions", baseUrlRaw);
|
||||||
|
if (!baseUrl) {
|
||||||
|
printWarning("Base URL is required.");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const detectedModelIds = await bestEffortFetchOpenAiModelIds(baseUrl, "lm-studio", false);
|
||||||
|
let modelIdsDefault = "local-model";
|
||||||
|
if (detectedModelIds && detectedModelIds.length > 0) {
|
||||||
|
const sample = detectedModelIds.slice(0, 10).join(", ");
|
||||||
|
printInfo(`Detected LM Studio models: ${sample}${detectedModelIds.length > 10 ? ", ..." : ""}`);
|
||||||
|
modelIdsDefault = detectedModelIds[0]!;
|
||||||
|
} else {
|
||||||
|
printInfo("No models detected from /models. Enter the exact model id shown in LM Studio.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelIdsRaw = await promptText("Model id(s) (comma-separated)", modelIdsDefault);
|
||||||
|
const modelIds = normalizeModelIds(modelIdsRaw);
|
||||||
|
if (modelIds.length === 0) {
|
||||||
|
printWarning("At least one model id is required.");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
providerId: "lm-studio",
|
||||||
|
modelIds,
|
||||||
|
baseUrl,
|
||||||
|
api: "openai-completions",
|
||||||
|
apiKeyConfig: "lm-studio",
|
||||||
|
authHeader: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function verifyCustomProvider(setup: CustomProviderSetup, authPath: string): Promise<void> {
|
async function verifyCustomProvider(setup: CustomProviderSetup, authPath: string): Promise<void> {
|
||||||
const registry = createModelRegistry(authPath);
|
const registry = createModelRegistry(authPath);
|
||||||
const modelsError = registry.getError();
|
const modelsError = registry.getError();
|
||||||
@@ -548,6 +589,31 @@ async function configureApiKeyProvider(authPath: string, providerId?: string): P
|
|||||||
return configureBedrockProvider(authPath);
|
return configureBedrockProvider(authPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (provider.id === "lm-studio") {
|
||||||
|
const setup = await promptLmStudioProviderSetup();
|
||||||
|
if (!setup) {
|
||||||
|
printInfo("LM Studio setup cancelled.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelsJsonPath = getModelsJsonPath(authPath);
|
||||||
|
const result = upsertProviderConfig(modelsJsonPath, setup.providerId, {
|
||||||
|
baseUrl: setup.baseUrl,
|
||||||
|
apiKey: setup.apiKeyConfig,
|
||||||
|
api: setup.api,
|
||||||
|
authHeader: setup.authHeader,
|
||||||
|
models: setup.modelIds.map((id) => ({ id })),
|
||||||
|
});
|
||||||
|
if (!result.ok) {
|
||||||
|
printWarning(result.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
printSuccess("Saved LM Studio provider.");
|
||||||
|
await verifyCustomProvider(setup, authPath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (provider.id === "__custom__") {
|
if (provider.id === "__custom__") {
|
||||||
const setup = await promptCustomProviderSetup();
|
const setup = await promptCustomProviderSetup();
|
||||||
if (!setup) {
|
if (!setup) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { dirname, join, resolve } from "node:path";
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
|
||||||
@@ -427,6 +427,28 @@ function packageNameToPath(root: string, packageName: string): string {
|
|||||||
return resolve(root, packageName);
|
return resolve(root, packageName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function listBundledWorkspacePackageNames(root: string): string[] {
|
||||||
|
if (!existsSync(root)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const names: string[] = [];
|
||||||
|
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
||||||
|
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
||||||
|
if (entry.name.startsWith(".")) continue;
|
||||||
|
if (entry.name.startsWith("@")) {
|
||||||
|
const scopeRoot = resolve(root, entry.name);
|
||||||
|
for (const scopedEntry of readdirSync(scopeRoot, { withFileTypes: true })) {
|
||||||
|
if (!scopedEntry.isDirectory() && !scopedEntry.isSymbolicLink()) continue;
|
||||||
|
names.push(`${entry.name}/${scopedEntry.name}`);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
names.push(entry.name);
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
function packageDependencyExists(packagePath: string, globalNodeModulesRoot: string, dependency: string): boolean {
|
function packageDependencyExists(packagePath: string, globalNodeModulesRoot: string, dependency: string): boolean {
|
||||||
return existsSync(packageNameToPath(resolve(packagePath, "node_modules"), dependency)) ||
|
return existsSync(packageNameToPath(resolve(packagePath, "node_modules"), dependency)) ||
|
||||||
existsSync(packageNameToPath(globalNodeModulesRoot, dependency));
|
existsSync(packageNameToPath(globalNodeModulesRoot, dependency));
|
||||||
@@ -464,6 +486,23 @@ function replaceBrokenPackageWithBundledCopy(targetPath: string, bundledPackageP
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function seedBundledPackage(globalNodeModulesRoot: string, bundledNodeModulesRoot: string, packageName: string): boolean {
|
||||||
|
const bundledPackagePath = resolve(bundledNodeModulesRoot, packageName);
|
||||||
|
if (!existsSync(bundledPackagePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPath = resolve(globalNodeModulesRoot, packageName);
|
||||||
|
if (replaceBrokenPackageWithBundledCopy(targetPath, bundledPackagePath, globalNodeModulesRoot)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!existsSync(targetPath)) {
|
||||||
|
linkDirectory(targetPath, bundledPackagePath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export function seedBundledWorkspacePackages(
|
export function seedBundledWorkspacePackages(
|
||||||
agentDir: string,
|
agentDir: string,
|
||||||
appRoot: string,
|
appRoot: string,
|
||||||
@@ -476,6 +515,10 @@ export function seedBundledWorkspacePackages(
|
|||||||
|
|
||||||
const globalNodeModulesRoot = resolve(getFeynmanNpmPrefixPath(agentDir), "lib", "node_modules");
|
const globalNodeModulesRoot = resolve(getFeynmanNpmPrefixPath(agentDir), "lib", "node_modules");
|
||||||
const seeded: string[] = [];
|
const seeded: string[] = [];
|
||||||
|
const bundledPackageNames = listBundledWorkspacePackageNames(bundledNodeModulesRoot);
|
||||||
|
for (const packageName of bundledPackageNames) {
|
||||||
|
seedBundledPackage(globalNodeModulesRoot, bundledNodeModulesRoot, packageName);
|
||||||
|
}
|
||||||
|
|
||||||
for (const source of sources) {
|
for (const source of sources) {
|
||||||
if (shouldSkipNativeSource(source)) continue;
|
if (shouldSkipNativeSource(source)) continue;
|
||||||
@@ -483,16 +526,8 @@ export function seedBundledWorkspacePackages(
|
|||||||
const parsed = parseNpmSource(source);
|
const parsed = parseNpmSource(source);
|
||||||
if (!parsed) continue;
|
if (!parsed) continue;
|
||||||
|
|
||||||
const bundledPackagePath = resolve(bundledNodeModulesRoot, parsed.name);
|
|
||||||
if (!existsSync(bundledPackagePath)) continue;
|
|
||||||
|
|
||||||
const targetPath = resolve(globalNodeModulesRoot, parsed.name);
|
const targetPath = resolve(globalNodeModulesRoot, parsed.name);
|
||||||
if (replaceBrokenPackageWithBundledCopy(targetPath, bundledPackagePath, globalNodeModulesRoot)) {
|
if (pathsMatchSymlinkTarget(targetPath, resolve(bundledNodeModulesRoot, parsed.name))) {
|
||||||
seeded.push(source);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!existsSync(targetPath)) {
|
|
||||||
linkDirectory(targetPath, bundledPackagePath);
|
|
||||||
seeded.push(source);
|
seeded.push(source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,3 +57,15 @@ test("research writing prompts forbid fabricated results and unproven figures",
|
|||||||
assert.match(draftPrompt, /placeholder or proposed experimental plan/i);
|
assert.match(draftPrompt, /placeholder or proposed experimental plan/i);
|
||||||
assert.match(draftPrompt, /source-backed quantitative data/i);
|
assert.match(draftPrompt, /source-backed quantitative data/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("deepresearch workflow requires durable artifacts even when blocked", () => {
|
||||||
|
const systemPrompt = readFileSync(join(repoRoot, ".feynman", "SYSTEM.md"), "utf8");
|
||||||
|
const deepResearchPrompt = readFileSync(join(repoRoot, "prompts", "deepresearch.md"), "utf8");
|
||||||
|
|
||||||
|
assert.match(systemPrompt, /Do not claim you are only a static model/i);
|
||||||
|
assert.match(systemPrompt, /write the requested durable artifact/i);
|
||||||
|
assert.match(deepResearchPrompt, /Do not stop after planning/i);
|
||||||
|
assert.match(deepResearchPrompt, /degraded mode/i);
|
||||||
|
assert.match(deepResearchPrompt, /Verification: BLOCKED/i);
|
||||||
|
assert.match(deepResearchPrompt, /Never end with only an explanation in chat/i);
|
||||||
|
});
|
||||||
|
|||||||
@@ -79,6 +79,15 @@ test("resolveModelProviderForCommand falls back to API-key providers when OAuth
|
|||||||
assert.equal(resolved?.id, "google");
|
assert.equal(resolved?.id, "google");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("resolveModelProviderForCommand supports LM Studio as a first-class local provider", () => {
|
||||||
|
const authPath = createAuthPath({});
|
||||||
|
|
||||||
|
const resolved = resolveModelProviderForCommand(authPath, "lm-studio");
|
||||||
|
|
||||||
|
assert.equal(resolved?.kind, "api-key");
|
||||||
|
assert.equal(resolved?.id, "lm-studio");
|
||||||
|
});
|
||||||
|
|
||||||
test("resolveModelProviderForCommand prefers OAuth when a provider supports both auth modes", () => {
|
test("resolveModelProviderForCommand prefers OAuth when a provider supports both auth modes", () => {
|
||||||
const authPath = createAuthPath({});
|
const authPath = createAuthPath({});
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ test("seedBundledWorkspacePackages repairs broken existing bundled packages", ()
|
|||||||
|
|
||||||
assert.deepEqual(seeded, ["npm:pi-markdown-preview"]);
|
assert.deepEqual(seeded, ["npm:pi-markdown-preview"]);
|
||||||
assert.equal(lstatSync(existingPackageDir).isSymbolicLink(), true);
|
assert.equal(lstatSync(existingPackageDir).isSymbolicLink(), true);
|
||||||
|
assert.equal(lstatSync(resolve(homeRoot, "npm-global", "lib", "node_modules", "puppeteer-core")).isSymbolicLink(), true);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
readFileSync(resolve(existingPackageDir, "package.json"), "utf8").includes('"version": "1.0.0"'),
|
readFileSync(resolve(existingPackageDir, "package.json"), "utf8").includes('"version": "1.0.0"'),
|
||||||
true,
|
true,
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ This usually means the release exists, but not all platform bundles were uploade
|
|||||||
Workarounds:
|
Workarounds:
|
||||||
- try again after the release finishes publishing
|
- try again after the release finishes publishing
|
||||||
- pass the latest published version explicitly, e.g.:
|
- pass the latest published version explicitly, e.g.:
|
||||||
curl -fsSL https://feynman.is/install | bash -s -- 0.2.21
|
curl -fsSL https://feynman.is/install | bash -s -- 0.2.24
|
||||||
EOF
|
EOF
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ This usually means the release exists, but not all platform bundles were uploade
|
|||||||
Workarounds:
|
Workarounds:
|
||||||
- try again after the release finishes publishing
|
- try again after the release finishes publishing
|
||||||
- pass the latest published version explicitly, e.g.:
|
- pass the latest published version explicitly, e.g.:
|
||||||
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.21
|
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.24
|
||||||
"@
|
"@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,13 +117,13 @@ These installers download the bundled `skills/` and `prompts/` trees plus the re
|
|||||||
The one-line installer already targets the latest tagged release. To pin an exact version, pass it explicitly:
|
The one-line installer already targets the latest tagged release. To pin an exact version, pass it explicitly:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://feynman.is/install | bash -s -- 0.2.21
|
curl -fsSL https://feynman.is/install | bash -s -- 0.2.24
|
||||||
```
|
```
|
||||||
|
|
||||||
On Windows:
|
On Windows:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.21
|
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.24
|
||||||
```
|
```
|
||||||
|
|
||||||
## Post-install setup
|
## Post-install setup
|
||||||
|
|||||||
@@ -52,9 +52,25 @@ Amazon Bedrock (AWS credential chain)
|
|||||||
|
|
||||||
Feynman verifies the same AWS credential chain Pi uses at runtime, including `AWS_PROFILE`, `~/.aws` credentials/config, SSO, ECS/IRSA, and EC2 instance roles. Once that check passes, Bedrock models become available in `feynman model list` without needing a traditional API key.
|
Feynman verifies the same AWS credential chain Pi uses at runtime, including `AWS_PROFILE`, `~/.aws` credentials/config, SSO, ECS/IRSA, and EC2 instance roles. Once that check passes, Bedrock models become available in `feynman model list` without needing a traditional API key.
|
||||||
|
|
||||||
### Local models: Ollama, LM Studio, vLLM
|
### Local models: LM Studio, Ollama, vLLM
|
||||||
|
|
||||||
If you want to use a model running locally, choose the API-key flow and then select:
|
If you want to use LM Studio, start the LM Studio local server, load a model, choose the API-key flow, and then select:
|
||||||
|
|
||||||
|
```text
|
||||||
|
LM Studio (local OpenAI-compatible server)
|
||||||
|
```
|
||||||
|
|
||||||
|
The default settings are:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Base URL: http://localhost:1234/v1
|
||||||
|
Authorization header: No
|
||||||
|
API key: lm-studio
|
||||||
|
```
|
||||||
|
|
||||||
|
Feynman attempts to read LM Studio's `/models` endpoint and prefill the loaded model id.
|
||||||
|
|
||||||
|
For Ollama, vLLM, or another OpenAI-compatible local server, choose:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Custom provider (baseUrl + API key)
|
Custom provider (baseUrl + API key)
|
||||||
@@ -70,7 +86,7 @@ Model ids: llama3.1:8b
|
|||||||
API key: local
|
API key: local
|
||||||
```
|
```
|
||||||
|
|
||||||
That same custom-provider flow also works for other OpenAI-compatible local servers such as LM Studio or vLLM. After saving the provider, run:
|
After saving the provider, run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
feynman model list
|
feynman model list
|
||||||
|
|||||||
Reference in New Issue
Block a user