@drewsepsi/nextpi 0.1.2 → 0.1.3
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/lib/.gitkeep +0 -0
- package/lib/agent.ts +105 -0
- package/lib/config.js +45 -0
- package/lib/config.ts +50 -0
- package/lib/root-path.ts +15 -0
- package/lib/session-singleton.ts +48 -0
- package/lib/utils.ts +6 -0
- package/package.json +2 -1
package/lib/.gitkeep
ADDED
|
File without changes
|
package/lib/agent.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAgentSession,
|
|
3
|
+
SessionManager,
|
|
4
|
+
type AgentSession
|
|
5
|
+
} from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { getModel, streamSimple } from "@mariozechner/pi-ai";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import { getProjectRoot } from "./root-path";
|
|
10
|
+
import { getEffectiveApiKey } from "./config";
|
|
11
|
+
|
|
12
|
+
export interface ResearchAgentConfig {
|
|
13
|
+
sessionDir?: string;
|
|
14
|
+
sessionFile?: string;
|
|
15
|
+
modelId?: string;
|
|
16
|
+
thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Default OpenRouter model
|
|
20
|
+
const DEFAULT_MODEL = "openrouter/free";
|
|
21
|
+
|
|
22
|
+
// Research tools (web search, web fetch)
|
|
23
|
+
const researchTools = [
|
|
24
|
+
{
|
|
25
|
+
name: "web_search",
|
|
26
|
+
description: "Search the web for information using Exa AI",
|
|
27
|
+
parameters: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
query: { type: "string", description: "Search query" },
|
|
31
|
+
numResults: { type: "number", description: "Number of results (default: 5)" }
|
|
32
|
+
},
|
|
33
|
+
required: ["query"]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "web_fetch",
|
|
38
|
+
description: "Fetch content from a specific URL",
|
|
39
|
+
parameters: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
url: { type: "string", description: "URL to fetch" }
|
|
43
|
+
},
|
|
44
|
+
required: ["url"]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
export async function createResearchAgent(
|
|
50
|
+
config: ResearchAgentConfig = {}
|
|
51
|
+
): Promise<AgentSession> {
|
|
52
|
+
const {
|
|
53
|
+
sessionDir = path.join(getProjectRoot(), ".sessions"),
|
|
54
|
+
sessionFile = path.join(sessionDir, "research-agent.jsonl"),
|
|
55
|
+
modelId = DEFAULT_MODEL,
|
|
56
|
+
thinkingLevel = "low",
|
|
57
|
+
} = config;
|
|
58
|
+
|
|
59
|
+
// Ensure session directory exists
|
|
60
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
61
|
+
|
|
62
|
+
// Use OpenRouter model
|
|
63
|
+
const model = getModel("openrouter", modelId as "openrouter/free");
|
|
64
|
+
|
|
65
|
+
// Create session with research tools and system prompt
|
|
66
|
+
const { session } = await createAgentSession({
|
|
67
|
+
model,
|
|
68
|
+
thinkingLevel,
|
|
69
|
+
systemPrompt: RESEARCH_SYSTEM_PROMPT,
|
|
70
|
+
sessionManager: SessionManager.open(sessionFile),
|
|
71
|
+
customTools: researchTools as any,
|
|
72
|
+
} as any);
|
|
73
|
+
|
|
74
|
+
// Wrap stream function with OpenRouter headers
|
|
75
|
+
const originalStreamFn = session.agent.streamFn;
|
|
76
|
+
session.agent.streamFn = (model, context, options) => {
|
|
77
|
+
return streamSimple(model, context, {
|
|
78
|
+
...options,
|
|
79
|
+
headers: {
|
|
80
|
+
...options?.headers,
|
|
81
|
+
"X-Title": "NextPi Agent",
|
|
82
|
+
"HTTP-Referer": "https://github.com/drewsepsi/nextpi",
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return session;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// System prompt for research and coding
|
|
91
|
+
export const RESEARCH_SYSTEM_PROMPT = `You are NextPi, an expert research and coding assistant. You have access to the web and the local file system.
|
|
92
|
+
|
|
93
|
+
### GUIDELINES
|
|
94
|
+
- ALWAYS start by explaining your plan to the user in the main message or thinking block.
|
|
95
|
+
- BEFORE calling any tool that modifies the file system (write_file, edit_file, etc.), you MUST describe the specific changes you are about to make and why.
|
|
96
|
+
- If you are editing a file, summarize the intended changes first.
|
|
97
|
+
- If you are researching, explain what you hope to find.
|
|
98
|
+
|
|
99
|
+
### CAPABILITIES
|
|
100
|
+
1. **Web Search**: Search for docs, errors, news, etc.
|
|
101
|
+
2. **Web Fetch**: Read specific URLs.
|
|
102
|
+
3. **File Operations**: Read, write, list (ls), and edit files.
|
|
103
|
+
4. **Shell**: Execute bash commands for testing or exploration.
|
|
104
|
+
|
|
105
|
+
Always be surgical with edits and transparent with your process.`;
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), '.nextpi');
|
|
6
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
7
|
+
|
|
8
|
+
export function getConfig() {
|
|
9
|
+
try {
|
|
10
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
const data = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
14
|
+
return JSON.parse(data);
|
|
15
|
+
} catch (err) {
|
|
16
|
+
console.error('Error reading config:', err);
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function saveConfig(newConfig) {
|
|
22
|
+
try {
|
|
23
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
24
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
const current = getConfig();
|
|
27
|
+
const updated = { ...current, ...newConfig };
|
|
28
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2));
|
|
29
|
+
|
|
30
|
+
// Also update current process env for immediate use
|
|
31
|
+
if (updated.openRouterApiKey) {
|
|
32
|
+
process.env.OPENROUTER_API_KEY = updated.openRouterApiKey;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return updated;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error('Error saving config:', err);
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getEffectiveApiKey() {
|
|
43
|
+
// Priority: 1. Environment Variable, 2. Config File
|
|
44
|
+
return process.env.OPENROUTER_API_KEY || getConfig().openRouterApiKey;
|
|
45
|
+
}
|
package/lib/config.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), '.nextpi');
|
|
6
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
7
|
+
|
|
8
|
+
export interface NextPiConfig {
|
|
9
|
+
openRouterApiKey?: string;
|
|
10
|
+
defaultModel?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getConfig(): NextPiConfig {
|
|
14
|
+
try {
|
|
15
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
const data = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
19
|
+
return JSON.parse(data);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error('Error reading config:', err);
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function saveConfig(newConfig: Partial<NextPiConfig>) {
|
|
27
|
+
try {
|
|
28
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
29
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
const current = getConfig();
|
|
32
|
+
const updated = { ...current, ...newConfig };
|
|
33
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2));
|
|
34
|
+
|
|
35
|
+
// Also update current process env for immediate use
|
|
36
|
+
if (updated.openRouterApiKey) {
|
|
37
|
+
process.env.OPENROUTER_API_KEY = updated.openRouterApiKey;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return updated;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error('Error saving config:', err);
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getEffectiveApiKey(): string | undefined {
|
|
48
|
+
// Priority: 1. Environment Variable, 2. Config File
|
|
49
|
+
return process.env.OPENROUTER_API_KEY || getConfig().openRouterApiKey;
|
|
50
|
+
}
|
package/lib/root-path.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
export function getProjectRoot() {
|
|
4
|
+
// PI_ROOT can be set via environment variable to override the root directory
|
|
5
|
+
const envRoot = process.env.PI_ROOT;
|
|
6
|
+
|
|
7
|
+
if (envRoot) {
|
|
8
|
+
// Resolve the path to handle relative paths from where the process was started
|
|
9
|
+
return path.resolve(envRoot);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// If no PI_ROOT is set, fallback to process.cwd()
|
|
13
|
+
// Note: in a Next.js environment during dev/build, process.cwd() is the project root.
|
|
14
|
+
return process.cwd();
|
|
15
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createResearchAgent } from '@/lib/agent';
|
|
2
|
+
|
|
3
|
+
type AgentSession = Awaited<ReturnType<typeof createResearchAgent>>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* In Next.js development mode, modules are frequently re-evaluated due to HMR.
|
|
7
|
+
* Attaching the session to `globalThis` ensures that all API routes (which may
|
|
8
|
+
* be running in different contexts or re-evaluated modules) share exactly
|
|
9
|
+
* the same AgentSession instance.
|
|
10
|
+
*/
|
|
11
|
+
const globalForAgent = globalThis as unknown as {
|
|
12
|
+
_sharedSession?: AgentSession;
|
|
13
|
+
_sharedSessionPromise?: Promise<AgentSession>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let currentModelId: string | undefined = undefined;
|
|
17
|
+
|
|
18
|
+
export async function getSharedSession(): Promise<AgentSession> {
|
|
19
|
+
// Return the existing session if already initialized
|
|
20
|
+
if (globalForAgent._sharedSession) {
|
|
21
|
+
return globalForAgent._sharedSession;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// If a session is being created, wait for it
|
|
25
|
+
if (globalForAgent._sharedSessionPromise) {
|
|
26
|
+
return globalForAgent._sharedSessionPromise;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Otherwise, create a new session and store the promise globally
|
|
30
|
+
console.log(`[Agent] Initializing new shared session singleton (Model: ${currentModelId || 'default'})...`);
|
|
31
|
+
globalForAgent._sharedSessionPromise = createResearchAgent({ modelId: currentModelId }).then((session) => {
|
|
32
|
+
globalForAgent._sharedSession = session;
|
|
33
|
+
console.log('[Agent] Shared session initialized');
|
|
34
|
+
return session;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return globalForAgent._sharedSessionPromise;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resetSharedSession(modelId?: string): void {
|
|
41
|
+
console.log(`[Agent] Resetting shared session${modelId ? ` to model ${modelId}` : ''}...`);
|
|
42
|
+
if (modelId) {
|
|
43
|
+
currentModelId = modelId;
|
|
44
|
+
}
|
|
45
|
+
globalForAgent._sharedSession = undefined;
|
|
46
|
+
globalForAgent._sharedSessionPromise = undefined;
|
|
47
|
+
}
|
|
48
|
+
|
package/lib/utils.ts
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drewsepsi/nextpi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "The Agentic IDE - An AI-powered research and coding assistant workspace.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
11
|
+
"lib",
|
|
11
12
|
".next/standalone",
|
|
12
13
|
".next/static",
|
|
13
14
|
"public",
|