vipcode 0.0.0-stage → 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/LICENSE +21 -0
- package/README.en.md +56 -0
- package/README.md +382 -2
- package/dist/agent.js +502 -0
- package/dist/agents.js +171 -0
- package/dist/app.js +1013 -0
- package/dist/auth.js +108 -0
- package/dist/catalog.js +487 -0
- package/dist/commands.js +119 -0
- package/dist/config.js +225 -0
- package/dist/debug.js +32 -0
- package/dist/discovery.js +277 -0
- package/dist/frontmatter.js +45 -0
- package/dist/images.js +81 -0
- package/dist/index.js +574 -0
- package/dist/login.js +157 -0
- package/dist/lsp.js +480 -0
- package/dist/markdown.js +259 -0
- package/dist/match.js +8 -0
- package/dist/mcp.js +225 -0
- package/dist/meta.js +7 -0
- package/dist/paths.js +27 -0
- package/dist/permissions.js +120 -0
- package/dist/prompt.js +123 -0
- package/dist/ranking.js +230 -0
- package/dist/router.js +726 -0
- package/dist/session.js +219 -0
- package/dist/skills.js +373 -0
- package/dist/snapshot.js +199 -0
- package/dist/tools.js +1247 -0
- package/dist/tui/app.js +1015 -0
- package/dist/tui/dialogs.js +196 -0
- package/dist/tui/editor.js +120 -0
- package/dist/ui.js +181 -0
- package/dist/update.js +235 -0
- package/package.json +64 -4
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fg from "fast-glob";
|
|
4
|
+
import { GLOBAL_INSTRUCTIONS_PATH } from "./paths.js";
|
|
5
|
+
function readLimited(file, max = 15000) {
|
|
6
|
+
try {
|
|
7
|
+
return fs.readFileSync(file, "utf8").slice(0, max);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return "";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
// Instrucciones del proyecto, igual que hace opencode con AGENTS.md
|
|
14
|
+
function projectInstructions(root, extra) {
|
|
15
|
+
const parts = [];
|
|
16
|
+
const global = readLimited(GLOBAL_INSTRUCTIONS_PATH);
|
|
17
|
+
if (global.trim() !== "") {
|
|
18
|
+
parts.push(`# Instrucciones generales del usuario (~/.vipcode/AGENTS.md)\n${global}`);
|
|
19
|
+
}
|
|
20
|
+
const names = ["AGENTS.md", "CLAUDE.md", ".vipcode.md"];
|
|
21
|
+
for (const name of names) {
|
|
22
|
+
const file = path.join(root, name);
|
|
23
|
+
if (fs.existsSync(file)) {
|
|
24
|
+
parts.push(`# Instrucciones del proyecto (${name})\n${readLimited(file)}`);
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Ficheros extra del config ("instructions": ["docs/estilo.md", ".cursor/rules/*.md"])
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
for (const pattern of extra) {
|
|
31
|
+
let files = [];
|
|
32
|
+
try {
|
|
33
|
+
files = fg.sync(pattern, { cwd: root, onlyFiles: true, dot: true, absolute: true }).slice(0, 10);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
files = [];
|
|
37
|
+
}
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
if (seen.has(file)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
seen.add(file);
|
|
43
|
+
parts.push(`# Instrucciones (${path.relative(root, file)})\n${readLimited(file, 8000)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (parts.length === 0) {
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
return `\n\n${parts.join("\n\n")}`;
|
|
50
|
+
}
|
|
51
|
+
// Lo que hay en la raíz del proyecto, para que el modelo se oriente sin gastar una llamada
|
|
52
|
+
function topLevel(root) {
|
|
53
|
+
try {
|
|
54
|
+
const entries = fs
|
|
55
|
+
.readdirSync(root, { withFileTypes: true })
|
|
56
|
+
.filter((entry) => entry.name !== ".git" && entry.name !== "node_modules" && entry.name !== ".DS_Store")
|
|
57
|
+
.sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name))
|
|
58
|
+
.map((entry) => {
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
return `${entry.name}/`;
|
|
61
|
+
}
|
|
62
|
+
return entry.name;
|
|
63
|
+
});
|
|
64
|
+
if (entries.length === 0) {
|
|
65
|
+
return "(empty)";
|
|
66
|
+
}
|
|
67
|
+
let extra = "";
|
|
68
|
+
if (entries.length > 40) {
|
|
69
|
+
extra = ` … (+${entries.length - 40} more)`;
|
|
70
|
+
}
|
|
71
|
+
return entries.slice(0, 40).join(", ") + extra;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return "(unknown)";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function systemPrompt(root, options) {
|
|
78
|
+
const { agent, toolNames } = options;
|
|
79
|
+
const has = (name) => toolNames.includes(name);
|
|
80
|
+
let modeText = "You are in BUILD mode: you may read, create and edit files and run shell commands (the user approves risky actions).";
|
|
81
|
+
if (agent.readOnly && !options.subagent) {
|
|
82
|
+
modeText = "";
|
|
83
|
+
}
|
|
84
|
+
const rules = [
|
|
85
|
+
"Explore before acting: use list_dir, glob, grep and read_file to understand the code. Never guess file contents.",
|
|
86
|
+
];
|
|
87
|
+
if (!agent.readOnly) {
|
|
88
|
+
rules.push("To change an existing file, read it first and then use edit_file with an exact, unique old_string copied from read_file output (without the line-number prefix). Use write_file only for new files or full rewrites.", "Make the smallest change that solves the task and follows the existing style of the project.", "After changing code, run the relevant build, linter or tests with bash when the project has them. If the language server reports errors after your edit, fix them.");
|
|
89
|
+
}
|
|
90
|
+
rules.push("Call several tools in one turn when they are independent.", "If a tool returns an error, read it and fix your approach instead of repeating the same call.");
|
|
91
|
+
if (has("todowrite")) {
|
|
92
|
+
rules.push("For tasks with 3 or more steps, keep a todo list with todowrite: mark one step in_progress when you start it and completed as soon as it is done. Skip it for simple tasks.");
|
|
93
|
+
}
|
|
94
|
+
if (has("task")) {
|
|
95
|
+
rules.push("Use the task tool to delegate broad searches or independent sub-tasks to a subagent; it keeps your own context small.");
|
|
96
|
+
}
|
|
97
|
+
if (has("skill")) {
|
|
98
|
+
rules.push("The skill tool lists skills: specialised instructions for certain kinds of tasks. When the request matches a skill's description, load it with skill before starting and follow its instructions.");
|
|
99
|
+
}
|
|
100
|
+
if (has("websearch") || has("webfetch")) {
|
|
101
|
+
rules.push("Use websearch to find documentation and webfetch to read a page when you need information you don't have.");
|
|
102
|
+
}
|
|
103
|
+
rules.push("Files the user attaches with @path come inside <fichero> or <carpeta> tags in their message; they are already read.", "When you finish, reply with a short summary of what you changed. Do not paste whole files back.", "Reply in the same language the user writes in (usually Spanish). Be concise; use Markdown only when it helps.");
|
|
104
|
+
let agentText = "";
|
|
105
|
+
if (agent.prompt.trim() !== "") {
|
|
106
|
+
agentText = `\n\n${agent.prompt.trim()}`;
|
|
107
|
+
}
|
|
108
|
+
let isGit = "no";
|
|
109
|
+
if (fs.existsSync(path.join(root, ".git"))) {
|
|
110
|
+
isGit = "yes";
|
|
111
|
+
}
|
|
112
|
+
return `You are vipcode, an expert software engineering agent running in the user's terminal.${modeText ? `\n\n${modeText}` : ""}${agentText}
|
|
113
|
+
|
|
114
|
+
Working rules:
|
|
115
|
+
${rules.map((rule) => `- ${rule}`).join("\n")}
|
|
116
|
+
|
|
117
|
+
Environment:
|
|
118
|
+
- Working directory: ${root}
|
|
119
|
+
- Git repository: ${isGit}
|
|
120
|
+
- Platform: ${process.platform}
|
|
121
|
+
- Date: ${new Date().toISOString().slice(0, 10)}
|
|
122
|
+
- Top-level entries: ${topLevel(root)}${projectInstructions(root, options.instructions)}`;
|
|
123
|
+
}
|
package/dist/ranking.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { readAuth } from "./auth.js";
|
|
3
|
+
import { debugLog } from "./debug.js";
|
|
4
|
+
import { HOME_DIR, RANKING_PATH } from "./paths.js";
|
|
5
|
+
const WEIGHTS = {
|
|
6
|
+
coding: { code: 0.4, agents: 0.3, tools: 0.15, general: 0.15 },
|
|
7
|
+
general: { general: 1 },
|
|
8
|
+
};
|
|
9
|
+
// Nombres que cada proveedor escribe a su manera y no se parecen al de LLM Stats
|
|
10
|
+
const ALIASES = {
|
|
11
|
+
"openai-fast": "gpt-oss-20b",
|
|
12
|
+
"nemotron-3-120b-a12b": "nemotron-3-super-120b-a12b",
|
|
13
|
+
};
|
|
14
|
+
// API oficial de LLM Stats (VIPCODE_LLMSTATS_URL permite usar otra, por ejemplo en las pruebas)
|
|
15
|
+
function apiBase() {
|
|
16
|
+
return process.env.VIPCODE_LLMSTATS_URL || "https://api.zeroeval.com/stats/v1";
|
|
17
|
+
}
|
|
18
|
+
// "nvidia/z-ai/glm-5.3:free" → "glm-5.3"
|
|
19
|
+
function baseId(id) {
|
|
20
|
+
let text = String(id).toLowerCase().replace(/^@cf\//, "");
|
|
21
|
+
text = text.slice(text.lastIndexOf("/") + 1);
|
|
22
|
+
return text.replace(/(:free|-free|:cloud|-cloud)$/, "");
|
|
23
|
+
}
|
|
24
|
+
// Clave para comparar: solo letras y números, sin palabras que no cambian el modelo
|
|
25
|
+
function keyOf(id) {
|
|
26
|
+
let text = baseId(id).replace(/[:._\s]+/g, "-");
|
|
27
|
+
text = text.replace(/-(instruct|it|chat|fp8|hf|versatile)(?=-|$)/g, "");
|
|
28
|
+
text = text.replace(/^(meta-|zai-|z-ai-)/, "");
|
|
29
|
+
return text.replace(/[^a-z0-9]/g, "");
|
|
30
|
+
}
|
|
31
|
+
// Palabras del nombre sin versiones ni tamaños ("gemini-3.8-flash" → gemini, flash)
|
|
32
|
+
function wordsOf(id) {
|
|
33
|
+
return baseId(id)
|
|
34
|
+
.replace(/[:._\s]+/g, "-")
|
|
35
|
+
.split("-")
|
|
36
|
+
.filter((word) => word && word !== "latest" && !/^\d+$/.test(word) && !/^v?\d+(\.\d+)?[bm]?$/.test(word))
|
|
37
|
+
.sort()
|
|
38
|
+
.join(" ");
|
|
39
|
+
}
|
|
40
|
+
// Lo descargado con la API (null si todavía no hay clave de LLM Stats o nunca se ha descargado)
|
|
41
|
+
export function loadRankingData() {
|
|
42
|
+
try {
|
|
43
|
+
const data = JSON.parse(fs.readFileSync(RANKING_PATH, "utf8"));
|
|
44
|
+
if (Array.isArray(data.models) && data.models.length > 0) {
|
|
45
|
+
return data;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// sin descargas todavía
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
export class Ranking {
|
|
54
|
+
data;
|
|
55
|
+
metric;
|
|
56
|
+
byKey = new Map();
|
|
57
|
+
keys = [];
|
|
58
|
+
sorted = {};
|
|
59
|
+
cache = new Map();
|
|
60
|
+
constructor(data, metric = "coding") {
|
|
61
|
+
this.data = data;
|
|
62
|
+
this.metric = metric;
|
|
63
|
+
for (const model of data.models) {
|
|
64
|
+
for (const key of [keyOf(model.id), keyOf(model.name)]) {
|
|
65
|
+
if (key && !this.byKey.has(key)) {
|
|
66
|
+
this.byKey.set(key, model);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
this.keys = [...this.byKey.keys()];
|
|
71
|
+
for (const field of ["general", "code", "agents", "tools"]) {
|
|
72
|
+
this.sorted[field] = data.models
|
|
73
|
+
.map((model) => model[field])
|
|
74
|
+
.filter((value) => typeof value === "number")
|
|
75
|
+
.sort((a, b) => a - b);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Busca el modelo de LLM Stats que corresponde a un id de proveedor
|
|
79
|
+
find(id) {
|
|
80
|
+
const alias = ALIASES[baseId(id)];
|
|
81
|
+
if (alias) {
|
|
82
|
+
return this.byKey.get(keyOf(alias)) || null;
|
|
83
|
+
}
|
|
84
|
+
const base = baseId(id);
|
|
85
|
+
const withoutDate = base.replace(/-(\d{4}|\d{6}|\d{8}|\d{2}-\d{4})$/, "");
|
|
86
|
+
const tries = [keyOf(base), keyOf(withoutDate), keyOf(base.replace(/-v\d+$/, ""))];
|
|
87
|
+
for (const key of tries) {
|
|
88
|
+
if (this.byKey.has(key)) {
|
|
89
|
+
return this.byKey.get(key);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// "…-latest": el más reciente con las mismas palabras
|
|
93
|
+
if (base.endsWith("-latest")) {
|
|
94
|
+
const words = wordsOf(base);
|
|
95
|
+
const candidates = this.data.models
|
|
96
|
+
.filter((model) => wordsOf(model.id) === words)
|
|
97
|
+
.sort((a, b) => String(b.released).localeCompare(String(a.released)));
|
|
98
|
+
if (candidates[0]) {
|
|
99
|
+
return candidates[0];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Un nombre más corto que el de LLM Stats ("beam" → "beam-1.0-501b-a23b"), o más largo ("llama-4-scout-17b" → "llama-4-scout")
|
|
103
|
+
const short = keyOf(withoutDate);
|
|
104
|
+
const longer = this.keys.filter((key) => key.startsWith(short)).sort((a, b) => a.length - b.length);
|
|
105
|
+
const distinct = new Set(longer.map((key) => this.byKey.get(key)));
|
|
106
|
+
if (longer.length > 0 && (distinct.size === 1 || short.length >= 8)) {
|
|
107
|
+
return this.byKey.get(longer[0]);
|
|
108
|
+
}
|
|
109
|
+
const shorter = this.keys.filter((key) => key.length >= 8 && short.startsWith(key)).sort((a, b) => b.length - a.length);
|
|
110
|
+
if (shorter[0]) {
|
|
111
|
+
return this.byKey.get(shorter[0]);
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
percentile(field, value) {
|
|
116
|
+
const list = this.sorted[field];
|
|
117
|
+
if (!list || list.length < 2) {
|
|
118
|
+
return 0.5;
|
|
119
|
+
}
|
|
120
|
+
let below = 0;
|
|
121
|
+
while (below < list.length && list[below] < value) {
|
|
122
|
+
below++;
|
|
123
|
+
}
|
|
124
|
+
return below / (list.length - 1);
|
|
125
|
+
}
|
|
126
|
+
// Nota de 0 a 100 (null si LLM Stats no tiene datos del modelo)
|
|
127
|
+
score(id) {
|
|
128
|
+
if (this.cache.has(id)) {
|
|
129
|
+
return this.cache.get(id);
|
|
130
|
+
}
|
|
131
|
+
const model = this.find(id);
|
|
132
|
+
let result = null;
|
|
133
|
+
if (model) {
|
|
134
|
+
let sum = 0;
|
|
135
|
+
let weight = 0;
|
|
136
|
+
for (const [field, value] of Object.entries(WEIGHTS[this.metric])) {
|
|
137
|
+
const raw = model[field];
|
|
138
|
+
if (typeof raw === "number" && value) {
|
|
139
|
+
// Percentiles para que índices con escalas distintas se puedan sumar
|
|
140
|
+
sum += this.percentile(field, raw) * value;
|
|
141
|
+
weight += value;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (weight > 0) {
|
|
145
|
+
result = { score: Math.round((sum / weight) * 100), model };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
this.cache.set(id, result);
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Clave de la API de LLM Stats (vipcode auth login llm-stats, o la variable LLMSTATS_API_KEY)
|
|
153
|
+
export function rankingKey() {
|
|
154
|
+
return readAuth()["llm-stats"]?.key || process.env.LLMSTATS_API_KEY || null;
|
|
155
|
+
}
|
|
156
|
+
function pick(source, names) {
|
|
157
|
+
for (const name of names) {
|
|
158
|
+
const value = source[name];
|
|
159
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
// Un modelo tal como lo devuelve la API (los nombres de los campos pueden variar: se buscan varios)
|
|
166
|
+
function parseApiModel(item) {
|
|
167
|
+
const id = String(item?.id || item?.model_id || "");
|
|
168
|
+
if (!id) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const scores = {
|
|
172
|
+
...item,
|
|
173
|
+
...(item.top_scores || {}),
|
|
174
|
+
...(item.scores || {}),
|
|
175
|
+
...(item.indices || {}),
|
|
176
|
+
...(item.category_scores || {}),
|
|
177
|
+
};
|
|
178
|
+
return {
|
|
179
|
+
id,
|
|
180
|
+
name: String(item.name || id),
|
|
181
|
+
general: pick(scores, ["index_general", "general", "overall", "llm_stats_score", "score"]),
|
|
182
|
+
code: pick(scores, ["index_code", "code", "coding"]),
|
|
183
|
+
agents: pick(scores, ["index_agents", "agents", "agent", "agentic"]),
|
|
184
|
+
tools: pick(scores, ["index_tool_calling", "tool_calling", "tools", "tool_use"]),
|
|
185
|
+
released: item.release_date ? String(item.release_date) : null,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
// Descarga las puntuaciones actuales con la API oficial (plan gratis: 250 respuestas al día)
|
|
189
|
+
export async function refreshRanking(apiKey, signal) {
|
|
190
|
+
const models = [];
|
|
191
|
+
let cursor = "";
|
|
192
|
+
for (let page = 0; page < 20; page++) {
|
|
193
|
+
let url = `${apiBase()}/models?limit=100`;
|
|
194
|
+
if (cursor) {
|
|
195
|
+
url += `&cursor=${encodeURIComponent(cursor)}`;
|
|
196
|
+
}
|
|
197
|
+
const signals = [AbortSignal.timeout(20000)];
|
|
198
|
+
if (signal) {
|
|
199
|
+
signals.push(signal);
|
|
200
|
+
}
|
|
201
|
+
const response = await fetch(url, {
|
|
202
|
+
headers: { authorization: `Bearer ${apiKey}`, accept: "application/json" },
|
|
203
|
+
signal: AbortSignal.any(signals),
|
|
204
|
+
});
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
throw new Error(`HTTP ${response.status}`);
|
|
207
|
+
}
|
|
208
|
+
const json = await response.json();
|
|
209
|
+
const items = Array.isArray(json?.models) ? json.models : Array.isArray(json?.data) ? json.data : [];
|
|
210
|
+
for (const item of items) {
|
|
211
|
+
const model = parseApiModel(item);
|
|
212
|
+
if (model) {
|
|
213
|
+
models.push(model);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
cursor = json?.next_cursor || json?.nextCursor || "";
|
|
217
|
+
if (!cursor || items.length === 0) {
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const rated = models.filter((model) => [model.general, model.code, model.agents, model.tools].some((value) => value !== null));
|
|
222
|
+
if (rated.length < 30) {
|
|
223
|
+
throw new Error("la API no devolvió puntuaciones que vipcode sepa leer");
|
|
224
|
+
}
|
|
225
|
+
const data = { updated: new Date().toISOString().slice(0, 10), source: "https://llm-stats.com (API)", models: rated };
|
|
226
|
+
fs.mkdirSync(HOME_DIR, { recursive: true });
|
|
227
|
+
fs.writeFileSync(RANKING_PATH, JSON.stringify(data));
|
|
228
|
+
debugLog("ranking actualizado", rated.length);
|
|
229
|
+
return { count: rated.length };
|
|
230
|
+
}
|