min-agent 0.1.0 → 0.1.1
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 +6 -0
- package/bin/min-agent.js +2 -2
- package/dist/agent.d.ts +23 -0
- package/dist/agent.js +566 -0
- package/dist/assistant-stream.d.ts +23 -0
- package/dist/assistant-stream.js +114 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +471 -0
- package/dist/compaction.d.ts +14 -0
- package/dist/compaction.js +99 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +142 -0
- package/dist/confirm.d.ts +6 -0
- package/dist/confirm.js +37 -0
- package/dist/instructions.d.ts +1 -0
- package/dist/instructions.js +115 -0
- package/dist/markdown.d.ts +15 -0
- package/dist/markdown.js +130 -0
- package/dist/mcp.d.ts +61 -0
- package/dist/mcp.js +237 -0
- package/dist/memory.d.ts +31 -0
- package/dist/memory.js +131 -0
- package/dist/output.d.ts +6 -0
- package/dist/output.js +52 -0
- package/dist/plugins.d.ts +2 -0
- package/dist/plugins.js +66 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +41 -0
- package/dist/serve.d.ts +9 -0
- package/dist/serve.js +351 -0
- package/dist/sessions.d.ts +17 -0
- package/dist/sessions.js +74 -0
- package/dist/skills.d.ts +12 -0
- package/dist/skills.js +127 -0
- package/dist/tool-output.d.ts +31 -0
- package/dist/tool-output.js +119 -0
- package/dist/tools/bash.d.ts +6 -0
- package/dist/tools/bash.js +93 -0
- package/dist/tools/edit.d.ts +7 -0
- package/dist/tools/edit.js +51 -0
- package/dist/tools/glob.d.ts +6 -0
- package/dist/tools/glob.js +36 -0
- package/dist/tools/grep.d.ts +7 -0
- package/dist/tools/grep.js +35 -0
- package/dist/tools/index.d.ts +37 -0
- package/dist/tools/index.js +20 -0
- package/dist/tools/read.d.ts +7 -0
- package/dist/tools/read.js +36 -0
- package/dist/tools/web_fetch.d.ts +6 -0
- package/dist/tools/web_fetch.js +83 -0
- package/dist/tools/web_search.d.ts +6 -0
- package/dist/tools/web_search.js +40 -0
- package/dist/tools/write.d.ts +6 -0
- package/dist/tools/write.js +32 -0
- package/package.json +4 -5
- package/src/agent.ts +0 -609
- package/src/assistant-stream.ts +0 -128
- package/src/cli.ts +0 -494
- package/src/compaction.ts +0 -119
- package/src/config.ts +0 -172
- package/src/confirm.ts +0 -42
- package/src/instructions.ts +0 -123
- package/src/markdown.ts +0 -140
- package/src/mcp.ts +0 -300
- package/src/memory.ts +0 -164
- package/src/output.ts +0 -58
- package/src/plugins.ts +0 -94
- package/src/provider.ts +0 -50
- package/src/serve.ts +0 -400
- package/src/sessions.ts +0 -94
- package/src/skills.ts +0 -146
- package/src/tool-output.ts +0 -146
- package/src/tools/bash.ts +0 -108
- package/src/tools/edit.ts +0 -65
- package/src/tools/glob.ts +0 -37
- package/src/tools/grep.ts +0 -37
- package/src/tools/index.ts +0 -21
- package/src/tools/read.ts +0 -38
- package/src/tools/web_fetch.ts +0 -87
- package/src/tools/web_search.ts +0 -42
- package/src/tools/write.ts +0 -36
- package/tsconfig.json +0 -15
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { generateText } from "ai";
|
|
2
|
+
/**
|
|
3
|
+
* Context compaction system.
|
|
4
|
+
*
|
|
5
|
+
* When conversation history exceeds a token threshold, older messages are
|
|
6
|
+
* summarized into a compact form to free up context window space.
|
|
7
|
+
*
|
|
8
|
+
* Strategy:
|
|
9
|
+
* 1. Estimate token count of messages (rough: 1 token ≈ 4 chars for English, 2 chars for CJK)
|
|
10
|
+
* 2. When over threshold, take older messages and summarize them via the LLM
|
|
11
|
+
* 3. Replace old messages with a single system summary message
|
|
12
|
+
* 4. Keep recent N turns verbatim for continuity
|
|
13
|
+
*/
|
|
14
|
+
const COMPACTION_PROMPT = `You are a conversation summarizer. Summarize the following conversation history into a concise but complete summary that preserves:
|
|
15
|
+
- Key decisions made
|
|
16
|
+
- Important context and facts discussed
|
|
17
|
+
- Current state of any tasks in progress
|
|
18
|
+
- User preferences mentioned
|
|
19
|
+
- File paths, code snippets, or technical details that are still relevant
|
|
20
|
+
|
|
21
|
+
Be concise but don't lose critical information. Output only the summary, no preamble.`;
|
|
22
|
+
// Default: trigger compaction at ~80% of context window
|
|
23
|
+
const DEFAULT_MAX_TOKENS = 128000;
|
|
24
|
+
const COMPACTION_RATIO = 0.75;
|
|
25
|
+
const KEEP_RECENT_TURNS = 4; // Keep last N user+assistant pairs verbatim
|
|
26
|
+
/** Rough token estimation */
|
|
27
|
+
export function estimateTokens(messages) {
|
|
28
|
+
let chars = 0;
|
|
29
|
+
for (const msg of messages) {
|
|
30
|
+
if (typeof msg.content === "string") {
|
|
31
|
+
chars += msg.content.length;
|
|
32
|
+
}
|
|
33
|
+
else if (Array.isArray(msg.content)) {
|
|
34
|
+
for (const part of msg.content) {
|
|
35
|
+
if ("text" in part && typeof part.text === "string") {
|
|
36
|
+
chars += part.text.length;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Rough estimate: mix of English (~4 chars/token) and CJK (~2 chars/token)
|
|
42
|
+
return Math.ceil(chars / 3);
|
|
43
|
+
}
|
|
44
|
+
/** Check if compaction is needed */
|
|
45
|
+
export function needsCompaction(messages, config) {
|
|
46
|
+
const maxTokens = config?.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
47
|
+
const threshold = maxTokens * COMPACTION_RATIO;
|
|
48
|
+
return estimateTokens(messages) > threshold;
|
|
49
|
+
}
|
|
50
|
+
/** Compact messages by summarizing older history */
|
|
51
|
+
export async function compactMessages(messages, model, config) {
|
|
52
|
+
const keepTurns = config?.keepRecentTurns ?? KEEP_RECENT_TURNS;
|
|
53
|
+
if (messages.length <= keepTurns * 2) {
|
|
54
|
+
// Not enough messages to compact
|
|
55
|
+
return { messages, compacted: false };
|
|
56
|
+
}
|
|
57
|
+
// Split: older messages to summarize, recent messages to keep
|
|
58
|
+
const splitIdx = messages.length - keepTurns * 2;
|
|
59
|
+
const toSummarize = messages.slice(0, splitIdx);
|
|
60
|
+
const toKeep = messages.slice(splitIdx);
|
|
61
|
+
// Build conversation text for summarization
|
|
62
|
+
const conversationText = toSummarize
|
|
63
|
+
.map((msg) => {
|
|
64
|
+
const role = msg.role;
|
|
65
|
+
const content = typeof msg.content === "string"
|
|
66
|
+
? msg.content
|
|
67
|
+
: Array.isArray(msg.content)
|
|
68
|
+
? msg.content
|
|
69
|
+
.filter((p) => "text" in p)
|
|
70
|
+
.map((p) => p.text)
|
|
71
|
+
.join("\n")
|
|
72
|
+
: "";
|
|
73
|
+
return `[${role}]: ${content.slice(0, 2000)}`;
|
|
74
|
+
})
|
|
75
|
+
.join("\n\n");
|
|
76
|
+
try {
|
|
77
|
+
const result = await generateText({
|
|
78
|
+
model,
|
|
79
|
+
messages: [
|
|
80
|
+
{ role: "system", content: COMPACTION_PROMPT },
|
|
81
|
+
{ role: "user", content: `Summarize this conversation:\n\n${conversationText}` },
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
const summary = result.text;
|
|
85
|
+
// Build compacted message list
|
|
86
|
+
const compactedMessages = [
|
|
87
|
+
{
|
|
88
|
+
role: "system",
|
|
89
|
+
content: `[Context Summary - Previous conversation was compacted]\n\n${summary}`,
|
|
90
|
+
},
|
|
91
|
+
...toKeep,
|
|
92
|
+
];
|
|
93
|
+
return { messages: compactedMessages, compacted: true };
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// If summarization fails, just truncate older messages
|
|
97
|
+
return { messages: toKeep, compacted: true };
|
|
98
|
+
}
|
|
99
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ProviderConfig {
|
|
2
|
+
type?: "openai-compatible" | "openai" | "ollama";
|
|
3
|
+
baseURL: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
defaultModel?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface AppConfig {
|
|
8
|
+
provider?: ProviderConfig;
|
|
9
|
+
instructions?: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare function getConfigDir(): string;
|
|
12
|
+
export declare function getRulesFile(): string;
|
|
13
|
+
export declare function loadConfig(): AppConfig;
|
|
14
|
+
export declare function saveConfig(config: AppConfig): void;
|
|
15
|
+
export declare function isConfigured(): boolean;
|
|
16
|
+
export declare function fetchModels(baseURL: string, apiKey: string): Promise<string[]>;
|
|
17
|
+
export declare function runSetup(): Promise<void>;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import readline from "readline";
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), ".min-agent");
|
|
6
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
7
|
+
const RULES_FILE = path.join(CONFIG_DIR, "rules.md");
|
|
8
|
+
export function getConfigDir() {
|
|
9
|
+
return CONFIG_DIR;
|
|
10
|
+
}
|
|
11
|
+
export function getRulesFile() {
|
|
12
|
+
return RULES_FILE;
|
|
13
|
+
}
|
|
14
|
+
export function loadConfig() {
|
|
15
|
+
if (!existsSync(CONFIG_FILE))
|
|
16
|
+
return {};
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function saveConfig(config) {
|
|
25
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
26
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
27
|
+
}
|
|
28
|
+
export function isConfigured() {
|
|
29
|
+
const config = loadConfig();
|
|
30
|
+
return !!(config.provider?.baseURL && config.provider?.apiKey);
|
|
31
|
+
}
|
|
32
|
+
async function fetchModelsFromURL(url, apiKey) {
|
|
33
|
+
const noCacheURL = new URL(url);
|
|
34
|
+
noCacheURL.searchParams.set("_t", Date.now().toString());
|
|
35
|
+
const response = await fetch(noCacheURL.toString(), {
|
|
36
|
+
headers: {
|
|
37
|
+
Authorization: `Bearer ${apiKey}`,
|
|
38
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
39
|
+
Pragma: "no-cache",
|
|
40
|
+
Expires: "0",
|
|
41
|
+
},
|
|
42
|
+
cache: "no-store",
|
|
43
|
+
signal: AbortSignal.timeout(10000),
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok)
|
|
46
|
+
return [];
|
|
47
|
+
const data = (await response.json());
|
|
48
|
+
const models = (data.data ?? data ?? []);
|
|
49
|
+
return models.map((m) => m.id).sort();
|
|
50
|
+
}
|
|
51
|
+
function ask(rl, question, defaultValue) {
|
|
52
|
+
const suffix = defaultValue ? ` (${defaultValue})` : "";
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
55
|
+
resolve(answer.trim() || defaultValue || "");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export async function fetchModels(baseURL, apiKey) {
|
|
60
|
+
try {
|
|
61
|
+
const trimmed = baseURL.replace(/\/$/, "");
|
|
62
|
+
const primary = await fetchModelsFromURL(`${trimmed}/models`, apiKey);
|
|
63
|
+
if (primary.length > 0)
|
|
64
|
+
return primary;
|
|
65
|
+
// Ollama users often provide host without /v1; auto-retry that variant.
|
|
66
|
+
if (!trimmed.endsWith("/v1")) {
|
|
67
|
+
return await fetchModelsFromURL(`${trimmed}/v1/models`, apiKey);
|
|
68
|
+
}
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export async function runSetup() {
|
|
76
|
+
const config = loadConfig();
|
|
77
|
+
const existing = config.provider;
|
|
78
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
79
|
+
console.log("\n🔧 min-agent 配置\n");
|
|
80
|
+
console.log("Provider 类型:");
|
|
81
|
+
console.log(" 1. openai-compatible (默认,兼容 OpenAI API 的任意服务)");
|
|
82
|
+
console.log(" 2. openai (OpenAI 官方)");
|
|
83
|
+
console.log(" 3. ollama (本地 Ollama)");
|
|
84
|
+
console.log();
|
|
85
|
+
const typeChoice = await ask(rl, "选择 Provider (1/2/3)", existing?.type === "ollama" ? "3" : existing?.type === "openai" ? "2" : "1");
|
|
86
|
+
const providerType = typeChoice === "3" ? "ollama"
|
|
87
|
+
: typeChoice === "2" ? "openai"
|
|
88
|
+
: "openai-compatible";
|
|
89
|
+
let baseURL;
|
|
90
|
+
let apiKey;
|
|
91
|
+
if (providerType === "ollama") {
|
|
92
|
+
baseURL = await ask(rl, "Ollama API URL", existing?.baseURL || "http://localhost:11434/v1");
|
|
93
|
+
if (!baseURL.replace(/\/$/, "").endsWith("/v1")) {
|
|
94
|
+
baseURL = `${baseURL.replace(/\/$/, "")}/v1`;
|
|
95
|
+
}
|
|
96
|
+
apiKey = "ollama"; // Ollama doesn't need a real key
|
|
97
|
+
}
|
|
98
|
+
else if (providerType === "openai") {
|
|
99
|
+
baseURL = "https://api.openai.com/v1";
|
|
100
|
+
apiKey = await ask(rl, "OpenAI API Key", existing?.apiKey);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
baseURL = await ask(rl, "API Base URL", existing?.baseURL || "https://api.openai.com/v1");
|
|
104
|
+
apiKey = await ask(rl, "API Key", existing?.apiKey);
|
|
105
|
+
}
|
|
106
|
+
if (!baseURL || (!apiKey && providerType !== "ollama")) {
|
|
107
|
+
console.error("URL 和 Key 不能为空");
|
|
108
|
+
rl.close();
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
console.log("\n正在获取模型列表...");
|
|
112
|
+
const models = await fetchModels(baseURL, apiKey);
|
|
113
|
+
let defaultModel = existing?.defaultModel ?? "";
|
|
114
|
+
if (models.length > 0) {
|
|
115
|
+
console.log(`\n可用模型 (${models.length}):`);
|
|
116
|
+
models.forEach((m, i) => {
|
|
117
|
+
const marker = m === defaultModel ? " ← 当前默认" : "";
|
|
118
|
+
console.log(` ${i + 1}. ${m}${marker}`);
|
|
119
|
+
});
|
|
120
|
+
console.log();
|
|
121
|
+
const choice = await ask(rl, "选择默认模型 (输入序号或模型名)", defaultModel);
|
|
122
|
+
const idx = parseInt(choice) - 1;
|
|
123
|
+
if (idx >= 0 && idx < models.length) {
|
|
124
|
+
defaultModel = models[idx];
|
|
125
|
+
}
|
|
126
|
+
else if (choice) {
|
|
127
|
+
defaultModel = choice;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.log(" ⚠ 无法获取模型列表,请手动输入模型名");
|
|
132
|
+
const hint = providerType === "ollama" ? "llama3" : providerType === "openai" ? "gpt-4o" : "";
|
|
133
|
+
defaultModel = await ask(rl, "默认模型", defaultModel || hint);
|
|
134
|
+
}
|
|
135
|
+
rl.close();
|
|
136
|
+
config.provider = { type: providerType, baseURL, apiKey, defaultModel };
|
|
137
|
+
saveConfig(config);
|
|
138
|
+
console.log(`\n✓ 配置已保存到 ${CONFIG_FILE}`);
|
|
139
|
+
console.log(` Type: ${providerType}`);
|
|
140
|
+
console.log(` URL: ${baseURL}`);
|
|
141
|
+
console.log(` Model: ${defaultModel}\n`);
|
|
142
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function setAutoApprove(value: boolean): void;
|
|
2
|
+
export declare function isAutoApprove(): boolean;
|
|
3
|
+
/** Ask user for confirmation. Returns true if approved. */
|
|
4
|
+
export declare function confirm(message: string): Promise<boolean>;
|
|
5
|
+
/** Check if a shell command is potentially dangerous */
|
|
6
|
+
export declare function isDangerousCommand(command: string): boolean;
|
package/dist/confirm.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import readline from "readline";
|
|
2
|
+
let autoApprove = false;
|
|
3
|
+
export function setAutoApprove(value) {
|
|
4
|
+
autoApprove = value;
|
|
5
|
+
}
|
|
6
|
+
export function isAutoApprove() {
|
|
7
|
+
return autoApprove;
|
|
8
|
+
}
|
|
9
|
+
/** Ask user for confirmation. Returns true if approved. */
|
|
10
|
+
export async function confirm(message) {
|
|
11
|
+
if (autoApprove)
|
|
12
|
+
return true;
|
|
13
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
rl.question(`\x1b[33m⚠ ${message} [y/N] \x1b[0m`, (answer) => {
|
|
16
|
+
rl.close();
|
|
17
|
+
resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/** Check if a shell command is potentially dangerous */
|
|
22
|
+
export function isDangerousCommand(command) {
|
|
23
|
+
const dangerous = [
|
|
24
|
+
/\brm\s+(-rf?|--recursive)\s/,
|
|
25
|
+
/\brm\s+-[a-z]*f/,
|
|
26
|
+
/\bsudo\b/,
|
|
27
|
+
/\bmkfs\b/,
|
|
28
|
+
/\bdd\s+/,
|
|
29
|
+
/\b(shutdown|reboot|halt|poweroff)\b/,
|
|
30
|
+
/\bgit\s+(push|reset\s+--hard|clean\s+-[a-z]*f)/,
|
|
31
|
+
/\bnpm\s+publish\b/,
|
|
32
|
+
/\bdrop\s+(table|database)\b/i,
|
|
33
|
+
/\btruncate\s+table\b/i,
|
|
34
|
+
/\bformat\b.*\b[a-z]:\b/i,
|
|
35
|
+
];
|
|
36
|
+
return dangerous.some((re) => re.test(command));
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function loadInstructions(): Promise<string[]>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { getRulesFile, loadConfig } from "./config.js";
|
|
5
|
+
/**
|
|
6
|
+
* Instruction/rules system.
|
|
7
|
+
*
|
|
8
|
+
* All config lives under ~/.min-agent/:
|
|
9
|
+
* ~/.min-agent/rules.md — Global rules (always loaded)
|
|
10
|
+
* ~/.min-agent/config.json — Can specify extra "instructions" paths/URLs
|
|
11
|
+
*
|
|
12
|
+
* Project-level rules (auto-discovered from cwd):
|
|
13
|
+
* ./AGENTS.md, ./RULES.md, ./.min-agent/AGENTS.md
|
|
14
|
+
*/
|
|
15
|
+
const PROJECT_FILES = ["AGENTS.md", "RULES.md", "CLAUDE.md"];
|
|
16
|
+
function findProjectInstructions() {
|
|
17
|
+
const results = [];
|
|
18
|
+
let current = process.cwd();
|
|
19
|
+
const root = path.parse(current).root;
|
|
20
|
+
while (current !== root) {
|
|
21
|
+
// Check .min-agent/AGENTS.md in project
|
|
22
|
+
const dotDir = path.join(current, ".min-agent", "AGENTS.md");
|
|
23
|
+
if (existsSync(dotDir)) {
|
|
24
|
+
results.push(dotDir);
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
for (const file of PROJECT_FILES) {
|
|
28
|
+
const filepath = path.join(current, file);
|
|
29
|
+
if (existsSync(filepath)) {
|
|
30
|
+
results.push(filepath);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (results.length > 0)
|
|
35
|
+
break;
|
|
36
|
+
current = path.dirname(current);
|
|
37
|
+
}
|
|
38
|
+
return results;
|
|
39
|
+
}
|
|
40
|
+
function findGlobalRules() {
|
|
41
|
+
const results = [];
|
|
42
|
+
const rulesFile = getRulesFile();
|
|
43
|
+
if (existsSync(rulesFile))
|
|
44
|
+
results.push(rulesFile);
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
function resolveConfigInstructions() {
|
|
48
|
+
const config = loadConfig();
|
|
49
|
+
const instructions = config.instructions ?? [];
|
|
50
|
+
const results = [];
|
|
51
|
+
for (const item of instructions) {
|
|
52
|
+
if (item.startsWith("http://") || item.startsWith("https://"))
|
|
53
|
+
continue;
|
|
54
|
+
const resolved = item.startsWith("~/")
|
|
55
|
+
? path.join(os.homedir(), item.slice(2))
|
|
56
|
+
: path.isAbsolute(item)
|
|
57
|
+
? item
|
|
58
|
+
: path.resolve(process.cwd(), item);
|
|
59
|
+
if (existsSync(resolved))
|
|
60
|
+
results.push(resolved);
|
|
61
|
+
}
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
64
|
+
async function fetchRemoteInstructions() {
|
|
65
|
+
const config = loadConfig();
|
|
66
|
+
const instructions = config.instructions ?? [];
|
|
67
|
+
const results = [];
|
|
68
|
+
const urls = instructions.filter((i) => i.startsWith("http://") || i.startsWith("https://"));
|
|
69
|
+
for (const url of urls) {
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
72
|
+
if (response.ok) {
|
|
73
|
+
const text = await response.text();
|
|
74
|
+
if (text.trim())
|
|
75
|
+
results.push(`Instructions from: ${url}\n${text}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
}
|
|
80
|
+
return results;
|
|
81
|
+
}
|
|
82
|
+
export async function loadInstructions() {
|
|
83
|
+
const parts = [];
|
|
84
|
+
// Global rules from ~/.min-agent/rules.md
|
|
85
|
+
for (const filepath of findGlobalRules()) {
|
|
86
|
+
try {
|
|
87
|
+
const content = readFileSync(filepath, "utf-8").trim();
|
|
88
|
+
if (content)
|
|
89
|
+
parts.push(`Instructions from: ${filepath}\n${content}`);
|
|
90
|
+
}
|
|
91
|
+
catch { }
|
|
92
|
+
}
|
|
93
|
+
// Project-level instructions
|
|
94
|
+
for (const filepath of findProjectInstructions()) {
|
|
95
|
+
try {
|
|
96
|
+
const content = readFileSync(filepath, "utf-8").trim();
|
|
97
|
+
if (content)
|
|
98
|
+
parts.push(`Instructions from: ${filepath}\n${content}`);
|
|
99
|
+
}
|
|
100
|
+
catch { }
|
|
101
|
+
}
|
|
102
|
+
// Config-defined file instructions
|
|
103
|
+
for (const filepath of resolveConfigInstructions()) {
|
|
104
|
+
try {
|
|
105
|
+
const content = readFileSync(filepath, "utf-8").trim();
|
|
106
|
+
if (content)
|
|
107
|
+
parts.push(`Instructions from: ${filepath}\n${content}`);
|
|
108
|
+
}
|
|
109
|
+
catch { }
|
|
110
|
+
}
|
|
111
|
+
// Remote URL instructions
|
|
112
|
+
const remote = await fetchRemoteInstructions();
|
|
113
|
+
parts.push(...remote);
|
|
114
|
+
return parts;
|
|
115
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight streaming markdown renderer for terminal.
|
|
3
|
+
* Tracks state across text deltas to apply ANSI formatting.
|
|
4
|
+
*/
|
|
5
|
+
export declare class MarkdownRenderer {
|
|
6
|
+
private buffer;
|
|
7
|
+
private inCodeBlock;
|
|
8
|
+
private codeLang;
|
|
9
|
+
/** Process a text delta and return formatted output */
|
|
10
|
+
write(text: string): string;
|
|
11
|
+
/** Flush remaining buffer */
|
|
12
|
+
flush(): string;
|
|
13
|
+
private formatInline;
|
|
14
|
+
private formatLine;
|
|
15
|
+
}
|
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight streaming markdown renderer for terminal.
|
|
3
|
+
* Tracks state across text deltas to apply ANSI formatting.
|
|
4
|
+
*/
|
|
5
|
+
function useColor() {
|
|
6
|
+
if ("NO_COLOR" in process.env)
|
|
7
|
+
return false;
|
|
8
|
+
if (process.env.FORCE_COLOR === "0")
|
|
9
|
+
return false;
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
const C = {
|
|
13
|
+
reset: "\x1b[0m",
|
|
14
|
+
bold: "\x1b[1m",
|
|
15
|
+
dim: "\x1b[2m",
|
|
16
|
+
italic: "\x1b[3m",
|
|
17
|
+
cyan: "\x1b[36m",
|
|
18
|
+
green: "\x1b[32m",
|
|
19
|
+
yellow: "\x1b[33m",
|
|
20
|
+
magenta: "\x1b[35m",
|
|
21
|
+
gray: "\x1b[90m",
|
|
22
|
+
underline: "\x1b[4m",
|
|
23
|
+
};
|
|
24
|
+
const Z = {
|
|
25
|
+
reset: "",
|
|
26
|
+
bold: "",
|
|
27
|
+
dim: "",
|
|
28
|
+
italic: "",
|
|
29
|
+
cyan: "",
|
|
30
|
+
green: "",
|
|
31
|
+
yellow: "",
|
|
32
|
+
magenta: "",
|
|
33
|
+
gray: "",
|
|
34
|
+
underline: "",
|
|
35
|
+
};
|
|
36
|
+
export class MarkdownRenderer {
|
|
37
|
+
buffer = "";
|
|
38
|
+
inCodeBlock = false;
|
|
39
|
+
codeLang = "";
|
|
40
|
+
/** Process a text delta and return formatted output */
|
|
41
|
+
write(text) {
|
|
42
|
+
this.buffer += text;
|
|
43
|
+
let output = "";
|
|
44
|
+
const c = useColor() ? C : Z;
|
|
45
|
+
// Process complete lines
|
|
46
|
+
while (true) {
|
|
47
|
+
const nlIdx = this.buffer.indexOf("\n");
|
|
48
|
+
if (nlIdx === -1)
|
|
49
|
+
break;
|
|
50
|
+
const line = this.buffer.slice(0, nlIdx);
|
|
51
|
+
this.buffer = this.buffer.slice(nlIdx + 1);
|
|
52
|
+
output += this.formatLine(line, c) + "\n";
|
|
53
|
+
}
|
|
54
|
+
return output;
|
|
55
|
+
}
|
|
56
|
+
/** Flush remaining buffer */
|
|
57
|
+
flush() {
|
|
58
|
+
if (!this.buffer)
|
|
59
|
+
return "";
|
|
60
|
+
const c = useColor() ? C : Z;
|
|
61
|
+
const out = this.formatLine(this.buffer, c);
|
|
62
|
+
this.buffer = "";
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
formatInline(line, c) {
|
|
66
|
+
// Split by inline code spans; format outside segments only
|
|
67
|
+
const parts = line.split(/(`[^`]*`)/g);
|
|
68
|
+
return parts
|
|
69
|
+
.map((seg) => {
|
|
70
|
+
if (seg.startsWith("`") && seg.endsWith("`") && seg.length >= 2) {
|
|
71
|
+
const inner = seg.slice(1, -1);
|
|
72
|
+
return `${c.cyan}${inner}${c.reset}`;
|
|
73
|
+
}
|
|
74
|
+
let s = seg;
|
|
75
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, `${c.bold}$1${c.reset}`);
|
|
76
|
+
s = s.replace(/\*([^*]+)\*/g, `${c.italic}$1${c.reset}`);
|
|
77
|
+
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, `${c.cyan}$1${c.reset} ${c.dim}($2)${c.reset}`);
|
|
78
|
+
return s;
|
|
79
|
+
})
|
|
80
|
+
.join("");
|
|
81
|
+
}
|
|
82
|
+
formatLine(line, c) {
|
|
83
|
+
// Code block fence
|
|
84
|
+
if (line.startsWith("```")) {
|
|
85
|
+
if (!this.inCodeBlock) {
|
|
86
|
+
this.inCodeBlock = true;
|
|
87
|
+
this.codeLang = line.slice(3).trim();
|
|
88
|
+
return `${c.dim}┌─ ${this.codeLang || "code"} ${"─".repeat(Math.max(0, 40 - (this.codeLang?.length ?? 0)))}${c.reset}`;
|
|
89
|
+
}
|
|
90
|
+
this.inCodeBlock = false;
|
|
91
|
+
this.codeLang = "";
|
|
92
|
+
return `${c.dim}└${"─".repeat(44)}${c.reset}`;
|
|
93
|
+
}
|
|
94
|
+
// Inside code block — dim
|
|
95
|
+
if (this.inCodeBlock) {
|
|
96
|
+
return `${c.dim}│${c.reset} ${line}`;
|
|
97
|
+
}
|
|
98
|
+
// Headers
|
|
99
|
+
if (line.startsWith("### "))
|
|
100
|
+
return `${c.bold}${this.formatInline(line.slice(4), c)}${c.reset}`;
|
|
101
|
+
if (line.startsWith("## "))
|
|
102
|
+
return `${c.bold}${this.formatInline(line.slice(3), c)}${c.reset}`;
|
|
103
|
+
if (line.startsWith("# "))
|
|
104
|
+
return `${c.bold}${c.cyan}${this.formatInline(line.slice(2), c)}${c.reset}`;
|
|
105
|
+
// Horizontal rule
|
|
106
|
+
if (/^---+$/.test(line))
|
|
107
|
+
return `${c.dim}${"─".repeat(44)}${c.reset}`;
|
|
108
|
+
// Blockquote
|
|
109
|
+
const bq = line.match(/^(\s*)>\s?(.*)$/);
|
|
110
|
+
if (bq) {
|
|
111
|
+
const indent = bq[1];
|
|
112
|
+
const body = bq[2];
|
|
113
|
+
return `${indent}${c.dim}▎${c.reset} ${this.formatInline(body, c)}`;
|
|
114
|
+
}
|
|
115
|
+
// Numbered list (1. item)
|
|
116
|
+
if (/^\s*\d+\.\s/.test(line)) {
|
|
117
|
+
const m = line.match(/^(\s*)(\d+\.)(\s)(.*)$/);
|
|
118
|
+
if (m) {
|
|
119
|
+
return `${m[1]}${c.yellow}${m[2]}${c.reset}${m[3]}${this.formatInline(m[4], c)}`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Bullet points
|
|
123
|
+
if (line.match(/^\s*[-*]\s/)) {
|
|
124
|
+
return line.replace(/^(\s*)([-*])(\s)(.*)$/, (_a, sp, _mark, sp2, rest) => {
|
|
125
|
+
return `${sp}${c.cyan}•${c.reset}${sp2}${this.formatInline(rest, c)}`;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return this.formatInline(line, c);
|
|
129
|
+
}
|
|
130
|
+
}
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
5
|
+
import { type Tool } from "ai";
|
|
6
|
+
export interface McpServerConfig {
|
|
7
|
+
/** Local stdio server: command + args. Ignored when `url` is set. */
|
|
8
|
+
command?: string[];
|
|
9
|
+
/** Remote MCP endpoint (http/https). When set, connects via HTTP transport instead of stdio. */
|
|
10
|
+
url?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Remote transport mode.
|
|
13
|
+
* - `streamable-http`: MCP Streamable HTTP (default for new remote servers)
|
|
14
|
+
* - `sse`: legacy HTTP + SSE transport
|
|
15
|
+
* - `auto`: try streamable-http first, then fall back to sse
|
|
16
|
+
*/
|
|
17
|
+
remoteTransport?: "streamable-http" | "sse" | "auto";
|
|
18
|
+
/** Shorthand: sets `Authorization: Bearer <token>` if not already present in `headers`. */
|
|
19
|
+
token?: string;
|
|
20
|
+
/** Extra HTTP headers for remote transports. */
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
environment?: Record<string, string>;
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
timeout?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface McpConfig {
|
|
27
|
+
mcpServers: Record<string, McpServerConfig>;
|
|
28
|
+
}
|
|
29
|
+
type McpClientTransport = StdioClientTransport | StreamableHTTPClientTransport | SSEClientTransport;
|
|
30
|
+
interface ConnectedServer {
|
|
31
|
+
client: Client;
|
|
32
|
+
transport: McpClientTransport;
|
|
33
|
+
tools: Array<{
|
|
34
|
+
name: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
inputSchema: any;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
export interface McpCheckResult {
|
|
40
|
+
name: string;
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
ok: boolean;
|
|
43
|
+
toolCount: number;
|
|
44
|
+
error?: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function loadMcpConfig(): McpConfig;
|
|
47
|
+
export declare function saveMcpConfig(config: McpConfig): void;
|
|
48
|
+
export declare function isRemoteMcpConfig(config: McpServerConfig): boolean;
|
|
49
|
+
/** One-line summary for CLI / logs (no secrets). */
|
|
50
|
+
export declare function formatMcpServerBinding(config: McpServerConfig): string;
|
|
51
|
+
export declare function connectMcpServer(name: string, config: McpServerConfig): Promise<ConnectedServer | null>;
|
|
52
|
+
export declare function checkMcpServer(name: string, config: McpServerConfig): Promise<McpCheckResult>;
|
|
53
|
+
export declare function checkAllMcpServers(): Promise<McpCheckResult[]>;
|
|
54
|
+
export declare function initMcp(): Promise<void>;
|
|
55
|
+
export declare function shutdownMcp(): Promise<void>;
|
|
56
|
+
export declare function getMcpTools(): Record<string, Tool>;
|
|
57
|
+
export declare function getMcpStatus(): Record<string, {
|
|
58
|
+
connected: boolean;
|
|
59
|
+
tools: string[];
|
|
60
|
+
}>;
|
|
61
|
+
export {};
|