@modeloslab/modelcode 0.1.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/README.md +73 -0
- package/SPEC.md +127 -0
- package/agents/code-reviewer.md +49 -0
- package/agents/debugger.md +25 -0
- package/agents/explore.md +22 -0
- package/agents/general-purpose.md +25 -0
- package/agents/plan.md +28 -0
- package/agents/researcher.md +26 -0
- package/agents/security-auditor.md +44 -0
- package/dist/BrowserWebSocketTransport-e6g854ra.mjs +8 -0
- package/dist/LaunchOptions-d24f2e73.mjs +8 -0
- package/dist/NodeWebSocketTransport-s3fsfh3j.mjs +9 -0
- package/dist/bidi-fwqajnyx.mjs +17261 -0
- package/dist/cli.mjs +1669 -0
- package/dist/devtools-fkz8mzpk.mjs +83 -0
- package/dist/fileFromPath-s8scncrt.mjs +128 -0
- package/dist/helpers-667kxskd.mjs +17 -0
- package/dist/index-4706p1xh.mjs +3238 -0
- package/dist/index-gp8nzd9n.mjs +1561 -0
- package/dist/main-0r35eyef.mjs +16229 -0
- package/dist/main-2aqyq9g6.mjs +24239 -0
- package/dist/main-5vqwebnv.mjs +54 -0
- package/dist/main-7f2pnmhh.mjs +2901 -0
- package/dist/main-7jta7ark.mjs +57 -0
- package/dist/main-8y3fe7c3.mjs +48 -0
- package/dist/main-9w13grbs.mjs +41 -0
- package/dist/main-d71btkt1.mjs +2478 -0
- package/dist/main-h8e68gyt.mjs +2819 -0
- package/dist/main-p2xnn95s.mjs +2240 -0
- package/dist/main-qfprs50h.mjs +1629 -0
- package/dist/main-tqg5vhra.mjs +19 -0
- package/dist/puppeteer-core-qdv3v3fq.mjs +1486 -0
- package/dist/tui-0r2q70wm.mjs +23768 -0
- package/package.json +66 -0
- package/skills/commit/SKILL.md +34 -0
- package/skills/debug/SKILL.md +44 -0
- package/skills/docker/SKILL.md +18 -0
- package/skills/init/SKILL.md +36 -0
- package/skills/nextjs-app-router/SKILL.md +16 -0
- package/skills/nextjs-data-fetching/SKILL.md +16 -0
- package/skills/nextjs-env-config/SKILL.md +18 -0
- package/skills/nextjs-metadata-seo/SKILL.md +17 -0
- package/skills/nextjs-middleware/SKILL.md +18 -0
- package/skills/nextjs-performance/SKILL.md +17 -0
- package/skills/nextjs-route-handler/SKILL.md +18 -0
- package/skills/nextjs-server-actions/SKILL.md +17 -0
- package/skills/nextjs-server-components/SKILL.md +18 -0
- package/skills/power-ui/SKILL.md +40 -0
- package/skills/pr/SKILL.md +38 -0
- package/skills/refactor/SKILL.md +40 -0
- package/skills/remember/SKILL.md +39 -0
- package/skills/review/SKILL.md +22 -0
- package/skills/security-review/SKILL.md +21 -0
- package/skills/simplify/SKILL.md +47 -0
- package/skills/skill-create/SKILL.md +37 -0
- package/skills/test/SKILL.md +34 -0
- package/skills/vercel-deploy/SKILL.md +16 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,1669 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
import {
|
|
4
|
+
COLUMNS,
|
|
5
|
+
MAINNET,
|
|
6
|
+
MUTATING_TOOLS,
|
|
7
|
+
addCard,
|
|
8
|
+
addEntity,
|
|
9
|
+
addJob,
|
|
10
|
+
addRelation,
|
|
11
|
+
analyzeImpact,
|
|
12
|
+
buildTurnContext,
|
|
13
|
+
chat,
|
|
14
|
+
compactionThreshold,
|
|
15
|
+
contextStatus,
|
|
16
|
+
createTransfer,
|
|
17
|
+
decryptWallet,
|
|
18
|
+
encryptWallet,
|
|
19
|
+
estimateTokens,
|
|
20
|
+
exports_external,
|
|
21
|
+
findCompactionCut,
|
|
22
|
+
fireEvent,
|
|
23
|
+
generateMnemonic,
|
|
24
|
+
getTool,
|
|
25
|
+
globMatch,
|
|
26
|
+
globalDir,
|
|
27
|
+
imageToDataUrl,
|
|
28
|
+
impactSummary,
|
|
29
|
+
importWalletHex,
|
|
30
|
+
indexCodebase,
|
|
31
|
+
indexFile,
|
|
32
|
+
isImagePath,
|
|
33
|
+
isProtectedBuiltin,
|
|
34
|
+
listJobs,
|
|
35
|
+
loadProjectContext,
|
|
36
|
+
loadSubagents,
|
|
37
|
+
lspFor,
|
|
38
|
+
moveCard,
|
|
39
|
+
newTurn,
|
|
40
|
+
postToolUse,
|
|
41
|
+
preToolUse,
|
|
42
|
+
query,
|
|
43
|
+
recordCreate,
|
|
44
|
+
recordPatch,
|
|
45
|
+
recordTurn,
|
|
46
|
+
register,
|
|
47
|
+
rememberFact,
|
|
48
|
+
removeCard,
|
|
49
|
+
removeJob,
|
|
50
|
+
renderBoard,
|
|
51
|
+
renderDiff,
|
|
52
|
+
route,
|
|
53
|
+
runProc,
|
|
54
|
+
runSubagent,
|
|
55
|
+
runTeam,
|
|
56
|
+
searchFacts,
|
|
57
|
+
searchSessions,
|
|
58
|
+
snapshot,
|
|
59
|
+
spawnProc,
|
|
60
|
+
toolSchemas,
|
|
61
|
+
userPromptSubmit,
|
|
62
|
+
validateMnemonic,
|
|
63
|
+
walletFromMnemonic,
|
|
64
|
+
which
|
|
65
|
+
} from "./main-2aqyq9g6.mjs";
|
|
66
|
+
import"./main-p2xnn95s.mjs";
|
|
67
|
+
import {
|
|
68
|
+
__require
|
|
69
|
+
} from "./main-8y3fe7c3.mjs";
|
|
70
|
+
|
|
71
|
+
// src/cli/main.ts
|
|
72
|
+
import { createInterface } from "readline/promises";
|
|
73
|
+
import { stdin, stdout } from "process";
|
|
74
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
75
|
+
|
|
76
|
+
// src/config/config.ts
|
|
77
|
+
import { homedir } from "os";
|
|
78
|
+
import { join } from "path";
|
|
79
|
+
import { mkdirSync, existsSync, readFileSync, writeFileSync } from "fs";
|
|
80
|
+
var MODELOS_API_BASE = "https://compute.modeloslab.xyz/api/v1";
|
|
81
|
+
var ConfigSchema = exports_external.object({
|
|
82
|
+
provider: exports_external.string().default("modelos"),
|
|
83
|
+
apiKey: exports_external.string().optional(),
|
|
84
|
+
baseUrl: exports_external.string().default(MODELOS_API_BASE),
|
|
85
|
+
model: exports_external.string().default("gemini-2.5-flash"),
|
|
86
|
+
subagentModel: exports_external.string().optional(),
|
|
87
|
+
maxTokens: exports_external.number().int().positive().default(8192),
|
|
88
|
+
stream: exports_external.boolean().default(true),
|
|
89
|
+
autoRoute: exports_external.boolean().default(false),
|
|
90
|
+
autoReview: exports_external.boolean().default(false),
|
|
91
|
+
autoSummary: exports_external.boolean().default(true),
|
|
92
|
+
theme: exports_external.enum(["amber", "mono", "ocean"]).default("amber"),
|
|
93
|
+
teamMemoryUrl: exports_external.string().optional(),
|
|
94
|
+
spendCapMdl: exports_external.number().default(0),
|
|
95
|
+
providers: exports_external.record(exports_external.object({ baseUrl: exports_external.string(), apiKey: exports_external.string().optional(), model: exports_external.string().optional() })).default({})
|
|
96
|
+
});
|
|
97
|
+
function globalDir2() {
|
|
98
|
+
const base = process.env.MODELCODE_HOME || homedir();
|
|
99
|
+
const d = join(base, process.env.MODELCODE_HOME ? "" : ".modelcode");
|
|
100
|
+
if (!existsSync(d))
|
|
101
|
+
mkdirSync(d, { recursive: true, mode: 448 });
|
|
102
|
+
return d;
|
|
103
|
+
}
|
|
104
|
+
function projectDir(cwd = process.cwd()) {
|
|
105
|
+
return join(cwd, ".modelcode");
|
|
106
|
+
}
|
|
107
|
+
function loadConfig(cwd = process.cwd()) {
|
|
108
|
+
let raw = {};
|
|
109
|
+
for (const p of [join(globalDir2(), "config.json"), join(projectDir(cwd), "config.json")]) {
|
|
110
|
+
try {
|
|
111
|
+
if (existsSync(p))
|
|
112
|
+
raw = { ...raw, ...JSON.parse(readFileSync(p, "utf8")) };
|
|
113
|
+
} catch {}
|
|
114
|
+
}
|
|
115
|
+
const envKey = process.env.MODELCODE_API_KEY || process.env.MODELOS_API_KEY;
|
|
116
|
+
if (envKey)
|
|
117
|
+
raw.apiKey = envKey;
|
|
118
|
+
return ConfigSchema.parse(raw);
|
|
119
|
+
}
|
|
120
|
+
function saveConfig(patch) {
|
|
121
|
+
const p = join(globalDir2(), "config.json");
|
|
122
|
+
let cur = {};
|
|
123
|
+
try {
|
|
124
|
+
if (existsSync(p))
|
|
125
|
+
cur = JSON.parse(readFileSync(p, "utf8"));
|
|
126
|
+
} catch {}
|
|
127
|
+
writeFileSync(p, JSON.stringify({ ...cur, ...patch }, null, 2), { mode: 384 });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/tools/coreTools.ts
|
|
131
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, statSync } from "fs";
|
|
132
|
+
import { resolve, relative, isAbsolute } from "path";
|
|
133
|
+
var MAX_READ = 400000;
|
|
134
|
+
function inside(cwd, p) {
|
|
135
|
+
const abs = isAbsolute(p) ? p : resolve(cwd, p);
|
|
136
|
+
const rel = relative(cwd, abs);
|
|
137
|
+
if (rel.startsWith(".."))
|
|
138
|
+
throw new Error(`path escapes the working directory: ${p}`);
|
|
139
|
+
return abs;
|
|
140
|
+
}
|
|
141
|
+
var bash = {
|
|
142
|
+
name: "bash",
|
|
143
|
+
description: "Run a shell command in the working directory; returns combined stdout+stderr. Use for builds, tests, git, file ops.",
|
|
144
|
+
permission: "ask",
|
|
145
|
+
parameters: {
|
|
146
|
+
type: "object",
|
|
147
|
+
properties: {
|
|
148
|
+
command: { type: "string", description: "the shell command to run" },
|
|
149
|
+
timeout_ms: { type: "number", description: "max runtime in ms (default 120000)" }
|
|
150
|
+
},
|
|
151
|
+
required: ["command"]
|
|
152
|
+
},
|
|
153
|
+
async run(args, ctx) {
|
|
154
|
+
const cmd = String(args.command ?? "");
|
|
155
|
+
const timeout = Number(args.timeout_ms) || 120000;
|
|
156
|
+
const r = await runProc(["bash", "-lc", cmd], { cwd: ctx.cwd, timeoutMs: timeout, onChunk: (s) => ctx.onStream?.(s) });
|
|
157
|
+
return `exit ${r.code}
|
|
158
|
+
${(r.stdout + (r.stderr ? `
|
|
159
|
+
[stderr]
|
|
160
|
+
${r.stderr}` : "")).slice(0, MAX_READ)}`;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
var readFile = {
|
|
164
|
+
name: "read",
|
|
165
|
+
description: "Read a UTF-8 text file. Returns its contents (truncated if very large).",
|
|
166
|
+
permission: "safe",
|
|
167
|
+
parameters: {
|
|
168
|
+
type: "object",
|
|
169
|
+
properties: { path: { type: "string", description: "file path (relative to cwd or absolute within it)" } },
|
|
170
|
+
required: ["path"]
|
|
171
|
+
},
|
|
172
|
+
async run(args, ctx) {
|
|
173
|
+
const p = inside(ctx.cwd, String(args.path ?? ""));
|
|
174
|
+
if (!existsSync2(p))
|
|
175
|
+
return `error: no such file: ${args.path}`;
|
|
176
|
+
if (statSync(p).size > MAX_READ * 2)
|
|
177
|
+
return `error: file too large (> ${MAX_READ * 2} bytes)`;
|
|
178
|
+
return readFileSync2(p, "utf8").slice(0, MAX_READ);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
var writeFile = {
|
|
182
|
+
name: "write",
|
|
183
|
+
description: "Create or overwrite a file with the given content. Prefer `edit` for changes to existing files.",
|
|
184
|
+
permission: "ask",
|
|
185
|
+
parameters: {
|
|
186
|
+
type: "object",
|
|
187
|
+
properties: { path: { type: "string" }, content: { type: "string" } },
|
|
188
|
+
required: ["path", "content"]
|
|
189
|
+
},
|
|
190
|
+
async run(args, ctx) {
|
|
191
|
+
const p = inside(ctx.cwd, String(args.path ?? ""));
|
|
192
|
+
writeFileSync2(p, String(args.content ?? ""));
|
|
193
|
+
return `wrote ${args.path} (${String(args.content ?? "").length} chars)`;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
var edit = {
|
|
197
|
+
name: "edit",
|
|
198
|
+
description: "Diff-based edit: replace an exact unique string with a new one (fast path, no full rewrite).",
|
|
199
|
+
permission: "ask",
|
|
200
|
+
parameters: {
|
|
201
|
+
type: "object",
|
|
202
|
+
properties: {
|
|
203
|
+
path: { type: "string" },
|
|
204
|
+
old_string: { type: "string", description: "exact text to replace (must be unique in the file)" },
|
|
205
|
+
new_string: { type: "string", description: "replacement text" },
|
|
206
|
+
replace_all: { type: "boolean", description: "replace every occurrence (default false)" }
|
|
207
|
+
},
|
|
208
|
+
required: ["path", "old_string", "new_string"]
|
|
209
|
+
},
|
|
210
|
+
async run(args, ctx) {
|
|
211
|
+
const p = inside(ctx.cwd, String(args.path ?? ""));
|
|
212
|
+
if (!existsSync2(p))
|
|
213
|
+
return `error: no such file: ${args.path}`;
|
|
214
|
+
const src = readFileSync2(p, "utf8");
|
|
215
|
+
const oldS = String(args.old_string ?? "");
|
|
216
|
+
const newS = String(args.new_string ?? "");
|
|
217
|
+
if (oldS === "")
|
|
218
|
+
return "error: old_string is empty";
|
|
219
|
+
const count = src.split(oldS).length - 1;
|
|
220
|
+
if (count === 0)
|
|
221
|
+
return "error: old_string not found";
|
|
222
|
+
if (count > 1 && !args.replace_all)
|
|
223
|
+
return `error: old_string matches ${count}x - make it unique or set replace_all`;
|
|
224
|
+
const updated = args.replace_all ? src.split(oldS).join(newS) : src.replace(oldS, newS);
|
|
225
|
+
writeFileSync2(p, updated);
|
|
226
|
+
return `edited ${args.path} (${count} replacement${count === 1 ? "" : "s"})
|
|
227
|
+
${renderDiff(oldS, newS, String(args.path))}`;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
var glob = {
|
|
231
|
+
name: "glob",
|
|
232
|
+
description: "List files matching a glob pattern (e.g. 'src/**/*.ts').",
|
|
233
|
+
permission: "safe",
|
|
234
|
+
parameters: { type: "object", properties: { pattern: { type: "string" } }, required: ["pattern"] },
|
|
235
|
+
async run(args, ctx) {
|
|
236
|
+
const matches = globMatch(ctx.cwd, String(args.pattern ?? "**/*"), 1000);
|
|
237
|
+
return matches.join(`
|
|
238
|
+
`) || "(no matches)";
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
var grep = {
|
|
242
|
+
name: "grep",
|
|
243
|
+
description: "Search file contents for a regex (ripgrep). Returns matching path:line:text.",
|
|
244
|
+
permission: "safe",
|
|
245
|
+
parameters: {
|
|
246
|
+
type: "object",
|
|
247
|
+
properties: { pattern: { type: "string" }, path: { type: "string", description: "dir/file (default cwd)" } },
|
|
248
|
+
required: ["pattern"]
|
|
249
|
+
},
|
|
250
|
+
async run(args, ctx) {
|
|
251
|
+
const pat = String(args.pattern ?? "");
|
|
252
|
+
const where = args.path ? inside(ctx.cwd, String(args.path)) : ctx.cwd;
|
|
253
|
+
const tool = which("rg") ? `rg -n --no-heading -e ${JSON.stringify(pat)} ${JSON.stringify(where)}` : `grep -rnE ${JSON.stringify(pat)} ${JSON.stringify(where)}`;
|
|
254
|
+
const r = await runProc(["bash", "-lc", `${tool} 2>/dev/null | head -200`], { cwd: ctx.cwd });
|
|
255
|
+
return r.stdout.trim() || "(no matches)";
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
function registerCoreTools() {
|
|
259
|
+
for (const t of [bash, readFile, writeFile, edit, glob, grep])
|
|
260
|
+
register(t);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/memory/tools.ts
|
|
264
|
+
var CATEGORIES = ["identity", "role", "project", "preference", "constraint", "interest", "style"];
|
|
265
|
+
var remember = {
|
|
266
|
+
name: "remember",
|
|
267
|
+
description: "Save a durable fact to long-term memory (persists across sessions). Use for user preferences, project facts, conventions, and anything worth recalling later.",
|
|
268
|
+
permission: "safe",
|
|
269
|
+
parameters: {
|
|
270
|
+
type: "object",
|
|
271
|
+
properties: {
|
|
272
|
+
text: { type: "string", description: "the fact, stated concisely" },
|
|
273
|
+
category: { type: "string", enum: CATEGORIES, description: "fact category (default project)" }
|
|
274
|
+
},
|
|
275
|
+
required: ["text"]
|
|
276
|
+
},
|
|
277
|
+
async run(args, ctx) {
|
|
278
|
+
const cat = CATEGORIES.includes(String(args.category)) ? String(args.category) : "project";
|
|
279
|
+
rememberFact(String(args.text ?? ""), cat, ctx.cwd);
|
|
280
|
+
return `remembered [${cat}]: ${String(args.text ?? "").slice(0, 120)}`;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
var recall = {
|
|
284
|
+
name: "recall",
|
|
285
|
+
description: "Search long-term memory (facts + past sessions) for something relevant to the current task.",
|
|
286
|
+
permission: "safe",
|
|
287
|
+
parameters: {
|
|
288
|
+
type: "object",
|
|
289
|
+
properties: { query: { type: "string", description: "what to look up" } },
|
|
290
|
+
required: ["query"]
|
|
291
|
+
},
|
|
292
|
+
async run(args, ctx) {
|
|
293
|
+
const q = String(args.query ?? "");
|
|
294
|
+
const facts = searchFacts(q, 8, ctx.cwd, true).map((f) => `• [${f.category}]${f.project ? " (project)" : ""} ${f.text}`);
|
|
295
|
+
const sessions = searchSessions(q, 5).map((s) => `· (${new Date(s.created_at).toLocaleDateString()}) ${s.text.slice(0, 200)}`);
|
|
296
|
+
const out = [];
|
|
297
|
+
if (facts.length)
|
|
298
|
+
out.push("FACTS:", ...facts);
|
|
299
|
+
if (sessions.length)
|
|
300
|
+
out.push("PAST SESSIONS:", ...sessions);
|
|
301
|
+
return out.length ? out.join(`
|
|
302
|
+
`) : "(nothing relevant in memory)";
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
function registerMemoryTools() {
|
|
306
|
+
register(remember);
|
|
307
|
+
register(recall);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/tools/agentTool.ts
|
|
311
|
+
function registerAgentTool(cfg, onProgress) {
|
|
312
|
+
const agents = loadSubagents();
|
|
313
|
+
const names = [...agents.keys()];
|
|
314
|
+
const agentTool = {
|
|
315
|
+
name: "agent",
|
|
316
|
+
description: "Delegate a task to a specialized subagent that runs in its own isolated context and returns its " + "conclusion. Available subagents: " + names.join(", ") + ". Use for focused work (exploration, " + "planning, review, debugging, research) to keep the main thread clean.",
|
|
317
|
+
permission: "safe",
|
|
318
|
+
parameters: {
|
|
319
|
+
type: "object",
|
|
320
|
+
properties: {
|
|
321
|
+
subagent_type: { type: "string", description: "which subagent to run", enum: names },
|
|
322
|
+
prompt: { type: "string", description: "the task/question for the subagent" }
|
|
323
|
+
},
|
|
324
|
+
required: ["subagent_type", "prompt"]
|
|
325
|
+
},
|
|
326
|
+
async run(args, ctx) {
|
|
327
|
+
const agent = agents.get(String(args.subagent_type ?? ""));
|
|
328
|
+
if (!agent)
|
|
329
|
+
return `error: unknown subagent '${args.subagent_type}'. available: ${names.join(", ")}`;
|
|
330
|
+
const { text, feeGrains } = await runSubagent(cfg, agent, String(args.prompt ?? ""), ctx, onProgress);
|
|
331
|
+
return `[${agent.name} · ${(feeGrains / 1e8).toFixed(6)} MDL · free/uncapped]
|
|
332
|
+
${text}`;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
register(agentTool);
|
|
336
|
+
const teamTool = {
|
|
337
|
+
name: "team",
|
|
338
|
+
description: "Convene a TEAM of subagents on one task — they share a channel and message each other across a " + "few rounds (coordinate, build on, and challenge each other), then return the full transcript for " + "you to synthesize. Members from: " + names.join(", ") + ".",
|
|
339
|
+
permission: "safe",
|
|
340
|
+
parameters: {
|
|
341
|
+
type: "object",
|
|
342
|
+
properties: {
|
|
343
|
+
members: { type: "array", items: { type: "string", enum: names }, description: "subagents to put on the team" },
|
|
344
|
+
prompt: { type: "string", description: "the shared task" },
|
|
345
|
+
rounds: { type: "number", description: "messaging rounds (default 2)" }
|
|
346
|
+
},
|
|
347
|
+
required: ["members", "prompt"]
|
|
348
|
+
},
|
|
349
|
+
async run(args, ctx) {
|
|
350
|
+
const members = Array.isArray(args.members) ? args.members.map(String) : [];
|
|
351
|
+
const { transcript, feeGrains } = await runTeam(cfg, members, String(args.prompt ?? ""), ctx, Number(args.rounds) || 2, onProgress);
|
|
352
|
+
return `[team · ${(feeGrains / 1e8).toFixed(6)} MDL · free/uncapped]
|
|
353
|
+
${transcript}`;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
register(teamTool);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/agent/loop.ts
|
|
360
|
+
var MAX_STEPS = 50;
|
|
361
|
+
var SYSTEM = `You are modelcode, a coding agent running on modelOS (decentralized, pay-per-use in MDL).
|
|
362
|
+
You work in the user's repository via tools: read/write/edit files, run bash, glob, grep.
|
|
363
|
+
Be concise. Prefer the diff-based 'edit' tool over rewriting whole files. Verify with bash (build/tests)
|
|
364
|
+
when relevant. Explain what you changed in one or two lines.
|
|
365
|
+
|
|
366
|
+
Memory & self-improvement: call 'remember' to save durable facts (preferences, project conventions) and
|
|
367
|
+
'recall' to look them up. After solving a non-trivial, repeatable task, use 'skill_manager' to distill it
|
|
368
|
+
into a reusable skill so it's a /command next time. Delegate focused work to subagents via 'agent', and
|
|
369
|
+
convene 'team' for multi-perspective reviews.
|
|
370
|
+
|
|
371
|
+
Tool discipline (important): call each tool ONCE for a given action — never repeat an identical tool call.
|
|
372
|
+
A tool result is authoritative: once a write/edit succeeds, trust it and move on. Do NOT claim a task is
|
|
373
|
+
done in the same message where you call the tool to do it — call the tool first, then confirm only after
|
|
374
|
+
you see its result. Keep confirmations to one short sentence.`;
|
|
375
|
+
|
|
376
|
+
class Agent {
|
|
377
|
+
cfg;
|
|
378
|
+
ctx;
|
|
379
|
+
h;
|
|
380
|
+
history;
|
|
381
|
+
constructor(cfg, ctx, h) {
|
|
382
|
+
this.cfg = cfg;
|
|
383
|
+
this.ctx = ctx;
|
|
384
|
+
this.h = h;
|
|
385
|
+
this.history = [{ role: "system", content: SYSTEM + loadProjectContext(ctx.cwd) }];
|
|
386
|
+
}
|
|
387
|
+
mode = "default";
|
|
388
|
+
get planMode() {
|
|
389
|
+
return this.mode === "plan";
|
|
390
|
+
}
|
|
391
|
+
set planMode(on) {
|
|
392
|
+
this.mode = on ? "plan" : "default";
|
|
393
|
+
}
|
|
394
|
+
lastTurnToolCount = 0;
|
|
395
|
+
abortController = null;
|
|
396
|
+
abort() {
|
|
397
|
+
this.abortController?.abort();
|
|
398
|
+
}
|
|
399
|
+
get running() {
|
|
400
|
+
return this.abortController !== null;
|
|
401
|
+
}
|
|
402
|
+
injectedContext = new Set;
|
|
403
|
+
sessionStart = Date.now();
|
|
404
|
+
note(text) {
|
|
405
|
+
this.history.push({ role: "system", content: `User steering note (apply going forward): ${text}` });
|
|
406
|
+
}
|
|
407
|
+
get messages() {
|
|
408
|
+
return [...this.history];
|
|
409
|
+
}
|
|
410
|
+
restore(saved) {
|
|
411
|
+
if (!saved.length)
|
|
412
|
+
return;
|
|
413
|
+
const head = this.history[0];
|
|
414
|
+
const rest = saved[0]?.role === "system" ? saved.slice(1) : saved;
|
|
415
|
+
this.history = [head, ...rest];
|
|
416
|
+
this.injectedContext.clear();
|
|
417
|
+
}
|
|
418
|
+
reset() {
|
|
419
|
+
this.history = [this.history[0]];
|
|
420
|
+
this.injectedContext.clear();
|
|
421
|
+
}
|
|
422
|
+
compact(summary) {
|
|
423
|
+
this.history = [this.history[0], { role: "system", content: `Summary of earlier conversation: ${summary}` }];
|
|
424
|
+
this.injectedContext.clear();
|
|
425
|
+
}
|
|
426
|
+
modelFor(userText = "") {
|
|
427
|
+
if (this.cfg.autoRoute && userText) {
|
|
428
|
+
const r = route(userText);
|
|
429
|
+
if (r)
|
|
430
|
+
return r.model;
|
|
431
|
+
}
|
|
432
|
+
return this.cfg.model;
|
|
433
|
+
}
|
|
434
|
+
contextStatus() {
|
|
435
|
+
return contextStatus(this.history, this.modelFor());
|
|
436
|
+
}
|
|
437
|
+
async autoCompact(model) {
|
|
438
|
+
if (estimateTokens(this.history) < compactionThreshold(model))
|
|
439
|
+
return false;
|
|
440
|
+
const keep = Number(process.env.MODELCODE_COMPACT_KEEP || "10");
|
|
441
|
+
const cut = findCompactionCut(this.history, keep);
|
|
442
|
+
if (cut < 0)
|
|
443
|
+
return false;
|
|
444
|
+
await fireEvent("preCompact", this.ctx.cwd).catch(() => {});
|
|
445
|
+
const head = this.history[0];
|
|
446
|
+
const middle = this.history.slice(1, cut);
|
|
447
|
+
const tail = this.history.slice(cut);
|
|
448
|
+
const rendered = middle.map((m) => {
|
|
449
|
+
const tc = m.tool_calls;
|
|
450
|
+
const body = typeof m.content === "string" ? m.content : tc ? `(tool calls: ${tc.map((c) => c.function?.name).join(", ")})` : "";
|
|
451
|
+
return `${m.role}: ${body}`.slice(0, 4000);
|
|
452
|
+
}).join(`
|
|
453
|
+
`);
|
|
454
|
+
let summary = "";
|
|
455
|
+
try {
|
|
456
|
+
const res = await chat(this.cfg, [
|
|
457
|
+
{ role: "system", content: "Summarize this conversation segment into a concise but COMPLETE brief that a coding agent can resume from: the task/goal, decisions made, files changed (with paths), key facts/values, and any open threads. Preserve specifics; omit chatter." },
|
|
458
|
+
{ role: "user", content: rendered.slice(0, 60000) }
|
|
459
|
+
], [], () => {}, this.cfg.model, 90000);
|
|
460
|
+
summary = (res.content || "").trim();
|
|
461
|
+
} catch {}
|
|
462
|
+
const note = summary ? `Summary of earlier conversation (auto-compacted to free context):
|
|
463
|
+
${summary}` : "Earlier conversation was auto-compacted to free context (summary unavailable).";
|
|
464
|
+
this.history = [head, { role: "system", content: note }, ...tail];
|
|
465
|
+
this.injectedContext.clear();
|
|
466
|
+
this.h.onCompact?.(contextStatus(this.history, model));
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
async send(userText, imagePaths = []) {
|
|
470
|
+
const promptGate = await userPromptSubmit(userText, this.ctx.cwd);
|
|
471
|
+
if (!promptGate.allowed) {
|
|
472
|
+
this.h.onToolResult("hook", `prompt blocked: ${promptGate.reason}`);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (promptGate.inject)
|
|
476
|
+
this.history.push({ role: "system", content: `[hook] ${promptGate.inject}` });
|
|
477
|
+
const modelForTurn = this.modelFor(userText);
|
|
478
|
+
await this.autoCompact(modelForTurn);
|
|
479
|
+
const turnCtx = buildTurnContext(userText, this.ctx.cwd, { injected: this.injectedContext, sessionStart: this.sessionStart });
|
|
480
|
+
if (turnCtx)
|
|
481
|
+
this.history.push({ role: "system", content: turnCtx });
|
|
482
|
+
const parts = imagePaths.map((p) => imageToDataUrl(p)).filter((u) => !!u).map((url) => ({ type: "image_url", image_url: { url } }));
|
|
483
|
+
this.history.push(parts.length ? { role: "user", content: [{ type: "text", text: userText }, ...parts] } : { role: "user", content: userText });
|
|
484
|
+
recordTurn(this.ctx.cwd, "user", userText);
|
|
485
|
+
newTurn();
|
|
486
|
+
this._doneCalls.clear();
|
|
487
|
+
const ac = this.abortController = new AbortController;
|
|
488
|
+
const signal = ac.signal;
|
|
489
|
+
let turnGrains = 0;
|
|
490
|
+
let toolCount = 0;
|
|
491
|
+
let editedCode = false;
|
|
492
|
+
if (this.cfg.autoRoute && modelForTurn !== this.cfg.model) {
|
|
493
|
+
this.h.onToolResult("router", `→ ${modelForTurn}`);
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
for (let step = 0;step < MAX_STEPS; step++) {
|
|
497
|
+
if (signal.aborted) {
|
|
498
|
+
this.h.onToolResult("interrupt", "⎋ interrupted");
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
await this.autoCompact(modelForTurn);
|
|
502
|
+
const res = await chat(this.cfg, this.history, toolSchemas(), this.h.onAssistantDelta, modelForTurn, undefined, signal);
|
|
503
|
+
turnGrains += res.feeGrains;
|
|
504
|
+
this.history.push({
|
|
505
|
+
role: "assistant",
|
|
506
|
+
content: res.toolCalls.length ? null : res.content || null,
|
|
507
|
+
...res.toolCalls.length ? { tool_calls: res.toolCalls } : {}
|
|
508
|
+
});
|
|
509
|
+
if (res.content && !res.toolCalls.length)
|
|
510
|
+
recordTurn(this.ctx.cwd, "assistant", res.content);
|
|
511
|
+
if (!res.toolCalls.length)
|
|
512
|
+
break;
|
|
513
|
+
const safeCalls = res.toolCalls.filter((c) => getTool(c.function.name)?.permission === "safe");
|
|
514
|
+
const seqCalls = res.toolCalls.filter((c) => getTool(c.function.name)?.permission !== "safe");
|
|
515
|
+
const results = new Map;
|
|
516
|
+
await Promise.all(safeCalls.map(async (c) => results.set(c.id, await this.execToolCall(c))));
|
|
517
|
+
for (const c of seqCalls)
|
|
518
|
+
results.set(c.id, await this.execToolCall(c));
|
|
519
|
+
const cap = Number(process.env.MODELCODE_MAX_TOOL_RESULT || "40000");
|
|
520
|
+
for (const call of res.toolCalls) {
|
|
521
|
+
const r = results.get(call.id) ?? { content: "error: tool did not run", edited: false };
|
|
522
|
+
if (r.edited)
|
|
523
|
+
editedCode = true;
|
|
524
|
+
let content = r.content;
|
|
525
|
+
if (content.length > cap) {
|
|
526
|
+
const head = content.slice(0, Math.floor(cap * 0.7));
|
|
527
|
+
const tail = content.slice(-Math.floor(cap * 0.2));
|
|
528
|
+
content = `${head}
|
|
529
|
+
|
|
530
|
+
…[truncated ${content.length - head.length - tail.length} chars to fit context]…
|
|
531
|
+
|
|
532
|
+
${tail}`;
|
|
533
|
+
}
|
|
534
|
+
this.history.push({ role: "tool", tool_call_id: call.id, name: call.function.name, content });
|
|
535
|
+
toolCount++;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
} catch (e) {
|
|
539
|
+
if (signal.aborted || e?.name === "AbortError")
|
|
540
|
+
this.h.onToolResult("interrupt", "⎋ interrupted");
|
|
541
|
+
else
|
|
542
|
+
throw e;
|
|
543
|
+
} finally {
|
|
544
|
+
this.abortController = null;
|
|
545
|
+
}
|
|
546
|
+
this.h.onCost(turnGrains);
|
|
547
|
+
this.lastTurnToolCount = toolCount;
|
|
548
|
+
if (toolCount >= 5 && !this._suggested) {
|
|
549
|
+
this._suggested = true;
|
|
550
|
+
this.h.onSuggestSkill?.(toolCount);
|
|
551
|
+
}
|
|
552
|
+
if (editedCode && this.cfg.autoReview) {
|
|
553
|
+
try {
|
|
554
|
+
const reviewer = loadSubagents(this.ctx.cwd).get("code-reviewer");
|
|
555
|
+
if (reviewer) {
|
|
556
|
+
const { text } = await runSubagent(this.cfg, reviewer, "Review the current working-tree diff (git diff). Report bugs + cleanups by severity with file:line.", this.ctx);
|
|
557
|
+
this.h.onReview?.(text);
|
|
558
|
+
}
|
|
559
|
+
} catch {}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
_suggested = false;
|
|
563
|
+
_doneCalls = new Map;
|
|
564
|
+
async execToolCall(call) {
|
|
565
|
+
const tool = getTool(call.function.name);
|
|
566
|
+
let args = {};
|
|
567
|
+
try {
|
|
568
|
+
args = call.function.arguments ? JSON.parse(call.function.arguments) : {};
|
|
569
|
+
} catch {}
|
|
570
|
+
const sig = `${call.function.name}:${JSON.stringify(args)}`;
|
|
571
|
+
if (this._doneCalls.has(sig)) {
|
|
572
|
+
return { content: `already done this turn (identical call) → ${this._doneCalls.get(sig).slice(0, 200)}`, edited: false };
|
|
573
|
+
}
|
|
574
|
+
if (!tool)
|
|
575
|
+
return { content: `error: unknown tool '${call.function.name}'`, edited: false };
|
|
576
|
+
this.h.onToolStart(tool.name, args);
|
|
577
|
+
let result;
|
|
578
|
+
let edited = false;
|
|
579
|
+
if (this.mode === "plan" && tool.permission !== "safe") {
|
|
580
|
+
result = "blocked: plan mode is on (read-only). Finish planning, then /mode (or /exit-plan) to allow changes.";
|
|
581
|
+
} else {
|
|
582
|
+
const gate = await preToolUse(tool.name, args, this.ctx.cwd);
|
|
583
|
+
const isEdit = MUTATING_TOOLS.has(tool.name);
|
|
584
|
+
const needsConfirm = tool.permission !== "safe" && this.mode !== "yolo" && !(this.mode === "acceptEdits" && isEdit);
|
|
585
|
+
if (!gate.allowed) {
|
|
586
|
+
result = `blocked by hook: ${gate.reason}`;
|
|
587
|
+
} else if (needsConfirm && !await this.h.confirm(tool.name, args)) {
|
|
588
|
+
result = "denied by user";
|
|
589
|
+
} else {
|
|
590
|
+
if (MUTATING_TOOLS.has(tool.name) && typeof args.path === "string")
|
|
591
|
+
snapshot(this.ctx.cwd, args.path);
|
|
592
|
+
const runCtx = { ...this.ctx, onStream: (c) => this.h.onToolStream?.(c) };
|
|
593
|
+
try {
|
|
594
|
+
result = await tool.run(args, runCtx);
|
|
595
|
+
} catch (e) {
|
|
596
|
+
result = `error: ${e.message}`;
|
|
597
|
+
}
|
|
598
|
+
if (MUTATING_TOOLS.has(tool.name) && typeof args.path === "string" && !result.startsWith("error")) {
|
|
599
|
+
try {
|
|
600
|
+
indexFile(this.ctx.cwd, args.path);
|
|
601
|
+
} catch {}
|
|
602
|
+
edited = true;
|
|
603
|
+
}
|
|
604
|
+
const post = await postToolUse(tool.name, result, this.ctx.cwd);
|
|
605
|
+
if (post)
|
|
606
|
+
result += `
|
|
607
|
+
[hook] ${post}`;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
this.h.onToolResult(tool.name, result);
|
|
611
|
+
if (!result.startsWith("error") && !result.startsWith("denied") && !result.startsWith("blocked"))
|
|
612
|
+
this._doneCalls.set(sig, result);
|
|
613
|
+
return { content: result, edited };
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/skills/manager.ts
|
|
618
|
+
import { join as join2 } from "path";
|
|
619
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, rmSync, readFileSync as readFileSync3 } from "fs";
|
|
620
|
+
var NAME_RE = /^[a-z0-9][a-z0-9-]{1,48}$/;
|
|
621
|
+
function userSkillsDir() {
|
|
622
|
+
const d = join2(globalDir(), "skills");
|
|
623
|
+
if (!existsSync3(d))
|
|
624
|
+
mkdirSync2(d, { recursive: true });
|
|
625
|
+
return d;
|
|
626
|
+
}
|
|
627
|
+
function skillDir(name) {
|
|
628
|
+
return join2(userSkillsDir(), name);
|
|
629
|
+
}
|
|
630
|
+
function writeSkill(spec, origin) {
|
|
631
|
+
if (!NAME_RE.test(spec.name))
|
|
632
|
+
throw new Error("skill name must be kebab-case (a-z, 0-9, -)");
|
|
633
|
+
if (!spec.description?.trim())
|
|
634
|
+
throw new Error("skill needs a description");
|
|
635
|
+
if (!spec.body?.trim())
|
|
636
|
+
throw new Error("skill needs instructions (body)");
|
|
637
|
+
const dir = skillDir(spec.name);
|
|
638
|
+
const existed = existsSync3(join2(dir, "SKILL.md"));
|
|
639
|
+
mkdirSync2(dir, { recursive: true });
|
|
640
|
+
const md = `---
|
|
641
|
+
name: ${spec.name}
|
|
642
|
+
description: ${spec.description.trim()}
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
${spec.body.trim()}
|
|
646
|
+
`;
|
|
647
|
+
writeFileSync3(join2(dir, "SKILL.md"), md);
|
|
648
|
+
existed ? recordPatch(spec.name) : recordCreate(spec.name, origin);
|
|
649
|
+
return `${existed ? "updated" : "created"} skill /${spec.name}`;
|
|
650
|
+
}
|
|
651
|
+
function deleteSkill(name) {
|
|
652
|
+
if (isProtectedBuiltin(name))
|
|
653
|
+
throw new Error(`'${name}' is a protected builtin and can't be deleted`);
|
|
654
|
+
const dir = skillDir(name);
|
|
655
|
+
if (!existsSync3(dir))
|
|
656
|
+
throw new Error(`no user skill named '${name}'`);
|
|
657
|
+
rmSync(dir, { recursive: true, force: true });
|
|
658
|
+
return `deleted skill /${name}`;
|
|
659
|
+
}
|
|
660
|
+
var skillManager = {
|
|
661
|
+
name: "skill_manager",
|
|
662
|
+
description: "Create, update, or delete a reusable skill (a SKILL.md callable as /<name>). Use `create` to " + "distill a successful multi-step approach into a repeatable procedure for next time.",
|
|
663
|
+
permission: "ask",
|
|
664
|
+
parameters: {
|
|
665
|
+
type: "object",
|
|
666
|
+
properties: {
|
|
667
|
+
action: { type: "string", enum: ["create", "update", "delete"] },
|
|
668
|
+
name: { type: "string", description: "kebab-case skill name" },
|
|
669
|
+
description: { type: "string" },
|
|
670
|
+
body: { type: "string", description: "step-by-step instructions (markdown)" }
|
|
671
|
+
},
|
|
672
|
+
required: ["action", "name"]
|
|
673
|
+
},
|
|
674
|
+
async run(args) {
|
|
675
|
+
const action = String(args.action);
|
|
676
|
+
const name = String(args.name ?? "");
|
|
677
|
+
if (action === "delete")
|
|
678
|
+
return deleteSkill(name);
|
|
679
|
+
return writeSkill({ name, description: String(args.description ?? ""), body: String(args.body ?? "") }, "agent");
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
function registerSkillManager() {
|
|
683
|
+
register(skillManager);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/cli/mentions.ts
|
|
687
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
688
|
+
import { join as join3 } from "path";
|
|
689
|
+
function expandFileMentions(line, cwd) {
|
|
690
|
+
const mentions = [...line.matchAll(/(?:^|\s)@([\w./\-]+)/g)].map((m) => m[1]).filter(Boolean);
|
|
691
|
+
if (!mentions.length)
|
|
692
|
+
return line;
|
|
693
|
+
const blocks = [];
|
|
694
|
+
for (const rel of mentions) {
|
|
695
|
+
if (isImagePath(rel))
|
|
696
|
+
continue;
|
|
697
|
+
try {
|
|
698
|
+
const p = join3(cwd, rel);
|
|
699
|
+
if (existsSync4(p))
|
|
700
|
+
blocks.push(`
|
|
701
|
+
|
|
702
|
+
--- @${rel} ---
|
|
703
|
+
${readFileSync4(p, "utf8").slice(0, 50000)}`);
|
|
704
|
+
} catch {}
|
|
705
|
+
}
|
|
706
|
+
return blocks.length ? line + blocks.join("") : line;
|
|
707
|
+
}
|
|
708
|
+
function imageMentions(line, cwd) {
|
|
709
|
+
return [...line.matchAll(/(?:^|\s)@([\w./\-]+)/g)].map((m) => m[1]).filter(Boolean).filter(isImagePath).map((rel) => join3(cwd, rel)).filter((p) => existsSync4(p));
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/wallet/credits.ts
|
|
713
|
+
var COMPUTE = process.env.MODELOS_COMPUTE || "https://compute.modeloslab.xyz";
|
|
714
|
+
async function creditBalance(address) {
|
|
715
|
+
try {
|
|
716
|
+
const r = await fetch(`${COMPUTE}/api/v1/credits/balance?address=${encodeURIComponent(address)}`, { signal: AbortSignal.timeout(6000) });
|
|
717
|
+
if (!r.ok)
|
|
718
|
+
return null;
|
|
719
|
+
const j = await r.json();
|
|
720
|
+
const grains = Number(j?.balance_grains ?? 0);
|
|
721
|
+
return { grains, mdl: grains / 1e8 };
|
|
722
|
+
} catch {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async function claimDeposit(apiKey, address, txid) {
|
|
727
|
+
try {
|
|
728
|
+
const r = await fetch(`${COMPUTE}/api/v1/credits/claim`, {
|
|
729
|
+
method: "POST",
|
|
730
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
731
|
+
body: JSON.stringify({ walletAddr: address, txid }),
|
|
732
|
+
signal: AbortSignal.timeout(15000)
|
|
733
|
+
});
|
|
734
|
+
const j = await r.json().catch(() => ({}));
|
|
735
|
+
if (!r.ok)
|
|
736
|
+
return { ok: false, message: j.error || `claim failed (HTTP ${r.status})` };
|
|
737
|
+
return { ok: true, message: `credited ${(Number(j.credited_grains ?? 0) / 1e8).toFixed(6)} MDL` };
|
|
738
|
+
} catch (e) {
|
|
739
|
+
return { ok: false, message: e.message };
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
var FUND_INSTRUCTIONS = "Fund credits in one command: `modelcode credits fund <mdl>` (signs from your CLI wallet, broadcasts, " + "and claims automatically). Or send MDL manually to the deposit address and run `credits claim <addr> <txid>`.";
|
|
743
|
+
var EXPLORER = process.env.MODELOS_EXPLORER || "https://explorer.modeloslab.xyz";
|
|
744
|
+
async function registerApiKey(walletAddr, label = "modelcode CLI") {
|
|
745
|
+
const rnd = crypto.getRandomValues(new Uint8Array(24));
|
|
746
|
+
const apiKey = "mdlk_" + Array.from(rnd).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
747
|
+
const r = await fetch(`${COMPUTE}/api/v1/keys/register`, {
|
|
748
|
+
method: "POST",
|
|
749
|
+
headers: { "Content-Type": "application/json" },
|
|
750
|
+
body: JSON.stringify({ apiKey, walletAddr, label }),
|
|
751
|
+
signal: AbortSignal.timeout(1e4)
|
|
752
|
+
});
|
|
753
|
+
const j = await r.json().catch(() => ({}));
|
|
754
|
+
if (!r.ok || j.error)
|
|
755
|
+
throw new Error(j.error || `key registration failed (HTTP ${r.status})`);
|
|
756
|
+
return { apiKey, depositAddr: j.depositAddr ?? "" };
|
|
757
|
+
}
|
|
758
|
+
async function accountInfo(address) {
|
|
759
|
+
try {
|
|
760
|
+
const r = await fetch(`${COMPUTE}/api/v1/credits/balance?address=${encodeURIComponent(address)}`, { signal: AbortSignal.timeout(6000) });
|
|
761
|
+
if (!r.ok)
|
|
762
|
+
return null;
|
|
763
|
+
const j = await r.json();
|
|
764
|
+
if (!j.deposit_addr)
|
|
765
|
+
return null;
|
|
766
|
+
return { grains: Number(j.balance_grains ?? 0), depositAddr: j.deposit_addr };
|
|
767
|
+
} catch {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
async function fetchUtxos(address) {
|
|
772
|
+
const r = await fetch(`${EXPLORER}/api/v2/utxo/${address}`, { signal: AbortSignal.timeout(8000) });
|
|
773
|
+
if (!r.ok)
|
|
774
|
+
return [];
|
|
775
|
+
return (await r.json()).map((u) => ({ txid: u.txid, vout: u.vout, value: BigInt(u.value || "0") }));
|
|
776
|
+
}
|
|
777
|
+
async function broadcast(hex) {
|
|
778
|
+
const r = await fetch(`${COMPUTE}/api/chain/broadcast`, {
|
|
779
|
+
method: "POST",
|
|
780
|
+
headers: { "Content-Type": "application/json" },
|
|
781
|
+
body: JSON.stringify({ hex }),
|
|
782
|
+
signal: AbortSignal.timeout(15000)
|
|
783
|
+
});
|
|
784
|
+
const j = await r.json().catch(() => ({}));
|
|
785
|
+
if (!r.ok || j.error)
|
|
786
|
+
throw new Error(j.error || `broadcast failed (HTTP ${r.status})`);
|
|
787
|
+
if (!j.txid)
|
|
788
|
+
throw new Error("broadcast returned no txid");
|
|
789
|
+
return j.txid;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// src/tools/moreTools.ts
|
|
793
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
|
|
794
|
+
import { resolve as resolve2 } from "path";
|
|
795
|
+
function stripHtml(html) {
|
|
796
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/&[a-z]+;/g, " ").replace(/\s+/g, " ").trim();
|
|
797
|
+
}
|
|
798
|
+
function assertPublicUrl(raw) {
|
|
799
|
+
let u;
|
|
800
|
+
try {
|
|
801
|
+
u = new URL(raw);
|
|
802
|
+
} catch {
|
|
803
|
+
throw new Error(`invalid URL: ${raw}`);
|
|
804
|
+
}
|
|
805
|
+
if (u.protocol !== "http:" && u.protocol !== "https:")
|
|
806
|
+
throw new Error(`blocked scheme: ${u.protocol}`);
|
|
807
|
+
const h = u.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
808
|
+
const blocked = h === "localhost" || h.endsWith(".localhost") || h === "metadata.google.internal" || /^(0|127|10)\./.test(h) || /^169\.254\./.test(h) || /^192\.168\./.test(h) || /^172\.(1[6-9]|2\d|3[01])\./.test(h) || /^(::1|fe80:|fc00:|fd[0-9a-f]{2}:)/.test(h) || h === "0.0.0.0" || h === "::";
|
|
809
|
+
if (blocked)
|
|
810
|
+
throw new Error(`blocked host (SSRF guard): ${u.hostname}`);
|
|
811
|
+
return u;
|
|
812
|
+
}
|
|
813
|
+
var webFetch = {
|
|
814
|
+
name: "web_fetch",
|
|
815
|
+
description: "Fetch a URL and return its readable text content.",
|
|
816
|
+
permission: "ask",
|
|
817
|
+
parameters: { type: "object", properties: { url: { type: "string" } }, required: ["url"] },
|
|
818
|
+
async run(args) {
|
|
819
|
+
try {
|
|
820
|
+
const u = assertPublicUrl(String(args.url));
|
|
821
|
+
const r = await fetch(u, { signal: AbortSignal.timeout(15000), headers: { "User-Agent": "modelcode" } });
|
|
822
|
+
const ct = r.headers.get("content-type") || "";
|
|
823
|
+
const body = await r.text();
|
|
824
|
+
return (ct.includes("html") ? stripHtml(body) : body).slice(0, 20000);
|
|
825
|
+
} catch (e) {
|
|
826
|
+
return `error: ${e.message}`;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
var webSearch = {
|
|
831
|
+
name: "web_search",
|
|
832
|
+
description: "Search the web (DuckDuckGo) and return top result titles + URLs.",
|
|
833
|
+
permission: "ask",
|
|
834
|
+
parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] },
|
|
835
|
+
async run(args) {
|
|
836
|
+
try {
|
|
837
|
+
const r = await fetch(`https://duckduckgo.com/html/?q=${encodeURIComponent(String(args.query))}`, { signal: AbortSignal.timeout(15000), headers: { "User-Agent": "Mozilla/5.0 modelcode" } });
|
|
838
|
+
const html = await r.text();
|
|
839
|
+
const cleanUrl = (href) => {
|
|
840
|
+
const u = href.replace(/^.*uddg=/, "").replace(/&(amp;)?rut=.*$/i, "");
|
|
841
|
+
try {
|
|
842
|
+
return decodeURIComponent(u);
|
|
843
|
+
} catch {
|
|
844
|
+
return u;
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
const hits = [...html.matchAll(/result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g)].slice(0, 8).map((m) => `${stripHtml(m[2] ?? "")} — ${cleanUrl(m[1] ?? "")}`);
|
|
848
|
+
return hits.join(`
|
|
849
|
+
`) || "(no results)";
|
|
850
|
+
} catch (e) {
|
|
851
|
+
return `error: ${e.message}`;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
var notebookEdit = {
|
|
856
|
+
name: "notebook_edit",
|
|
857
|
+
description: "Edit a Jupyter .ipynb cell: replace the source of the cell at the given index.",
|
|
858
|
+
permission: "ask",
|
|
859
|
+
parameters: { type: "object", properties: { path: { type: "string" }, cell: { type: "number" }, source: { type: "string" } }, required: ["path", "cell", "source"] },
|
|
860
|
+
async run(args, ctx) {
|
|
861
|
+
const p = resolve2(ctx.cwd, String(args.path));
|
|
862
|
+
if (!existsSync5(p))
|
|
863
|
+
return `error: no such notebook: ${args.path}`;
|
|
864
|
+
const nb = JSON.parse(readFileSync5(p, "utf8"));
|
|
865
|
+
const i = Number(args.cell);
|
|
866
|
+
if (!nb.cells?.[i])
|
|
867
|
+
return `error: cell ${i} out of range (${nb.cells?.length ?? 0} cells)`;
|
|
868
|
+
nb.cells[i].source = String(args.source).split(/(?<=\n)/);
|
|
869
|
+
writeFileSync4(p, JSON.stringify(nb, null, 1));
|
|
870
|
+
return `edited cell ${i} of ${args.path}`;
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
var sshExec = {
|
|
874
|
+
name: "ssh_exec",
|
|
875
|
+
description: "Run a command on a remote host over SSH (uses your ssh config/keys). host like user@server.",
|
|
876
|
+
permission: "ask",
|
|
877
|
+
parameters: { type: "object", properties: { host: { type: "string" }, command: { type: "string" } }, required: ["host", "command"] },
|
|
878
|
+
async run(args) {
|
|
879
|
+
const r = await runProc(["ssh", "-o", "BatchMode=yes", String(args.host), String(args.command)]);
|
|
880
|
+
return `exit ${r.code}
|
|
881
|
+
${(r.stdout + (r.stderr ? `
|
|
882
|
+
[stderr]
|
|
883
|
+
${r.stderr}` : "")).slice(0, 20000)}`;
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
var lsp = {
|
|
887
|
+
name: "lsp",
|
|
888
|
+
description: "Language-server queries: 'definition' / 'references' / 'hover' at a file position, or 'diagnostics' for a file. Real LSP (precise), not text search.",
|
|
889
|
+
permission: "safe",
|
|
890
|
+
parameters: {
|
|
891
|
+
type: "object",
|
|
892
|
+
properties: {
|
|
893
|
+
action: { type: "string", enum: ["definition", "references", "hover", "diagnostics"] },
|
|
894
|
+
path: { type: "string" },
|
|
895
|
+
line: { type: "number", description: "0-based line (for definition/references/hover)" },
|
|
896
|
+
character: { type: "number", description: "0-based column" }
|
|
897
|
+
},
|
|
898
|
+
required: ["action", "path"]
|
|
899
|
+
},
|
|
900
|
+
async run(args, ctx) {
|
|
901
|
+
const file = String(args.path);
|
|
902
|
+
const action = String(args.action);
|
|
903
|
+
const srv = lspFor(file, ctx.cwd);
|
|
904
|
+
if (!srv) {
|
|
905
|
+
const cmds = [
|
|
906
|
+
existsSync5(resolve2(ctx.cwd, "tsconfig.json")) && "npx -y typescript tsc --noEmit",
|
|
907
|
+
existsSync5(resolve2(ctx.cwd, "go.mod")) && "go vet ./...",
|
|
908
|
+
existsSync5(resolve2(ctx.cwd, "Cargo.toml")) && "cargo check -q"
|
|
909
|
+
].filter(Boolean);
|
|
910
|
+
if (action === "diagnostics" && cmds.length) {
|
|
911
|
+
const r = await runProc(["bash", "-lc", `${cmds[0]} 2>&1 | head -120`], { cwd: ctx.cwd });
|
|
912
|
+
return `(no language server installed; shelled typecheck)
|
|
913
|
+
${r.stdout.trim() || "no diagnostics"}`;
|
|
914
|
+
}
|
|
915
|
+
return "no language server for this file type (install e.g. typescript-language-server, pyright, gopls)";
|
|
916
|
+
}
|
|
917
|
+
const ln = Number(args.line) || 0, ch = Number(args.character) || 0;
|
|
918
|
+
try {
|
|
919
|
+
if (action === "diagnostics") {
|
|
920
|
+
const d = await srv.diags(file);
|
|
921
|
+
return d.length ? d.map((x) => `${file}:${x.range.start.line + 1}:${x.range.start.character + 1} [${x.severity === 1 ? "error" : "warn"}] ${x.message}`).join(`
|
|
922
|
+
`) : "no diagnostics";
|
|
923
|
+
}
|
|
924
|
+
if (action === "hover") {
|
|
925
|
+
const h = await srv.hover(file, ln, ch);
|
|
926
|
+
const c = h?.contents;
|
|
927
|
+
return (typeof c === "string" ? c : c?.value ?? JSON.stringify(c)) || "(no hover info)";
|
|
928
|
+
}
|
|
929
|
+
const locs = action === "definition" ? await srv.definition(file, ln, ch) : await srv.references(file, ln, ch);
|
|
930
|
+
const arr = Array.isArray(locs) ? locs : locs ? [locs] : [];
|
|
931
|
+
return arr.length ? arr.map((l) => `${decodeURIComponent((l.uri || "").replace("file://", ""))}:${(l.range?.start?.line ?? 0) + 1}:${(l.range?.start?.character ?? 0) + 1}`).join(`
|
|
932
|
+
`) : "(none found)";
|
|
933
|
+
} catch (e) {
|
|
934
|
+
return `lsp error: ${e.message}`;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
var browserExtract = {
|
|
939
|
+
name: "browser",
|
|
940
|
+
description: "Open a page and extract its main readable content (baseline; interactive automation is on the roadmap).",
|
|
941
|
+
permission: "ask",
|
|
942
|
+
parameters: { type: "object", properties: { url: { type: "string" } }, required: ["url"] },
|
|
943
|
+
run: webFetch.run
|
|
944
|
+
};
|
|
945
|
+
var kgQueryTool = {
|
|
946
|
+
name: "kg_query",
|
|
947
|
+
description: "Query the knowledge graph (entities + relationships) — semantic memory AND the codebase graph (files/symbols).",
|
|
948
|
+
permission: "safe",
|
|
949
|
+
parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] },
|
|
950
|
+
async run(args) {
|
|
951
|
+
const hits = query(String(args.query), 8);
|
|
952
|
+
if (!hits.length)
|
|
953
|
+
return "(nothing in the graph)";
|
|
954
|
+
return hits.map((h) => `• ${h.entity.kind}:${h.entity.name}${h.entity.data ? ` (${h.entity.data})` : ""}` + (h.relations.length ? `
|
|
955
|
+
→ ${h.relations.map((r) => `${r.rel} ${r.dst}`).join(", ")}` : "")).join(`
|
|
956
|
+
`);
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
var kgIndexTool = {
|
|
960
|
+
name: "kg_index",
|
|
961
|
+
description: "Index this repository into the knowledge graph (files + symbols + contains edges) for structural recall.",
|
|
962
|
+
permission: "safe",
|
|
963
|
+
parameters: { type: "object", properties: {} },
|
|
964
|
+
async run(_args, ctx) {
|
|
965
|
+
const r = await indexCodebase(ctx.cwd);
|
|
966
|
+
return `indexed ${r.files} files, ${r.symbols} symbols into the knowledge graph`;
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
var kgRemember = {
|
|
970
|
+
name: "kg_relate",
|
|
971
|
+
description: "Add an entity or a relationship to the knowledge graph (e.g. decision/component facts).",
|
|
972
|
+
permission: "safe",
|
|
973
|
+
parameters: { type: "object", properties: { kind: { type: "string" }, name: { type: "string" }, data: { type: "string" }, rel: { type: "string", description: "optional relationship" }, to_kind: { type: "string" }, to_name: { type: "string" } }, required: ["kind", "name"] },
|
|
974
|
+
async run(args) {
|
|
975
|
+
const id = addEntity(String(args.kind), String(args.name), String(args.data ?? ""));
|
|
976
|
+
if (args.rel && args.to_kind && args.to_name) {
|
|
977
|
+
const d = addEntity(String(args.to_kind), String(args.to_name));
|
|
978
|
+
addRelation(id, String(args.rel), d);
|
|
979
|
+
}
|
|
980
|
+
return `added ${id}`;
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
var kgImpactTool = {
|
|
984
|
+
name: "kg_impact",
|
|
985
|
+
description: "Impact analysis BEFORE changing a symbol: shows where it's declared + which files reference it (callers/usages). Call this before refactoring/renaming/changing a function/class/type so you know the blast radius.",
|
|
986
|
+
permission: "safe",
|
|
987
|
+
parameters: { type: "object", properties: { symbol: { type: "string", description: "the function/class/type/const name to analyze" } }, required: ["symbol"] },
|
|
988
|
+
async run(args, ctx) {
|
|
989
|
+
return impactSummary(analyzeImpact(ctx.cwd, String(args.symbol ?? "").trim()));
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
function registerMoreTools() {
|
|
993
|
+
for (const t of [webFetch, webSearch, notebookEdit, sshExec, lsp, browserExtract, kgQueryTool, kgIndexTool, kgRemember, kgImpactTool])
|
|
994
|
+
register(t);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// src/tools/browser.ts
|
|
998
|
+
import { existsSync as existsSync6 } from "fs";
|
|
999
|
+
var CHROME_CANDIDATES = [
|
|
1000
|
+
process.env.MODELCODE_CHROME,
|
|
1001
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
1002
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
1003
|
+
"/usr/bin/google-chrome",
|
|
1004
|
+
"/usr/bin/chromium",
|
|
1005
|
+
"/usr/bin/chromium-browser"
|
|
1006
|
+
].filter(Boolean);
|
|
1007
|
+
function chromePath() {
|
|
1008
|
+
return CHROME_CANDIDATES.find((p) => existsSync6(p)) ?? null;
|
|
1009
|
+
}
|
|
1010
|
+
var _browser = null;
|
|
1011
|
+
var _page = null;
|
|
1012
|
+
async function page() {
|
|
1013
|
+
if (_page)
|
|
1014
|
+
return _page;
|
|
1015
|
+
const exe = chromePath();
|
|
1016
|
+
if (!exe)
|
|
1017
|
+
throw new Error("no Chrome/Chromium found — set MODELCODE_CHROME to its path");
|
|
1018
|
+
const puppeteer = (await import("./puppeteer-core-qdv3v3fq.mjs")).default;
|
|
1019
|
+
_browser = await puppeteer.launch({ executablePath: exe, headless: true, args: ["--no-sandbox"] });
|
|
1020
|
+
_page = await _browser.newPage();
|
|
1021
|
+
return _page;
|
|
1022
|
+
}
|
|
1023
|
+
async function closeBrowser() {
|
|
1024
|
+
try {
|
|
1025
|
+
await _browser?.close();
|
|
1026
|
+
} catch {} finally {
|
|
1027
|
+
_browser = null;
|
|
1028
|
+
_page = null;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
var browserTool = {
|
|
1032
|
+
name: "browser_action",
|
|
1033
|
+
description: "Drive a headless browser: navigate to a URL, extract readable text, click/type a CSS selector, or screenshot. Use for JS-rendered pages, flows, and scraping.",
|
|
1034
|
+
permission: "ask",
|
|
1035
|
+
parameters: {
|
|
1036
|
+
type: "object",
|
|
1037
|
+
properties: {
|
|
1038
|
+
action: { type: "string", enum: ["navigate", "extract", "click", "type", "screenshot"] },
|
|
1039
|
+
url: { type: "string", description: "for navigate" },
|
|
1040
|
+
selector: { type: "string", description: "CSS selector for click/type/extract scope" },
|
|
1041
|
+
text: { type: "string", description: "text to type" },
|
|
1042
|
+
path: { type: "string", description: "screenshot output path" }
|
|
1043
|
+
},
|
|
1044
|
+
required: ["action"]
|
|
1045
|
+
},
|
|
1046
|
+
async run(args, ctx) {
|
|
1047
|
+
try {
|
|
1048
|
+
const p = await page();
|
|
1049
|
+
switch (String(args.action)) {
|
|
1050
|
+
case "navigate":
|
|
1051
|
+
await p.goto(String(args.url), { waitUntil: "networkidle2", timeout: 30000 });
|
|
1052
|
+
return `navigated to ${args.url} — title: ${await p.title()}`;
|
|
1053
|
+
case "extract": {
|
|
1054
|
+
const sel = args.selector ? String(args.selector) : "body";
|
|
1055
|
+
const txt = await p.$eval(sel, (el) => el.innerText).catch(() => "");
|
|
1056
|
+
return txt.slice(0, 20000) || "(no text)";
|
|
1057
|
+
}
|
|
1058
|
+
case "click":
|
|
1059
|
+
await p.click(String(args.selector));
|
|
1060
|
+
return `clicked ${args.selector}`;
|
|
1061
|
+
case "type":
|
|
1062
|
+
await p.type(String(args.selector), String(args.text ?? ""));
|
|
1063
|
+
return `typed into ${args.selector}`;
|
|
1064
|
+
case "screenshot": {
|
|
1065
|
+
const out = args.path ? `${ctx.cwd}/${args.path}` : `${ctx.cwd}/screenshot-${Date.now()}.png`;
|
|
1066
|
+
await p.screenshot({ path: out });
|
|
1067
|
+
return `saved screenshot ${out}`;
|
|
1068
|
+
}
|
|
1069
|
+
default:
|
|
1070
|
+
return `unknown action '${args.action}'`;
|
|
1071
|
+
}
|
|
1072
|
+
} catch (e) {
|
|
1073
|
+
return `browser error: ${e.message}`;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
function registerBrowserTool() {
|
|
1078
|
+
register(browserTool);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// src/lsp/client.ts
|
|
1082
|
+
import { join as join4, extname } from "path";
|
|
1083
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
1084
|
+
import { pathToFileURL } from "url";
|
|
1085
|
+
class LspServer {
|
|
1086
|
+
cwd;
|
|
1087
|
+
proc;
|
|
1088
|
+
id = 1;
|
|
1089
|
+
pending = new Map;
|
|
1090
|
+
diagnostics = new Map;
|
|
1091
|
+
buf = Buffer.alloc(0);
|
|
1092
|
+
opened = new Set;
|
|
1093
|
+
ready;
|
|
1094
|
+
constructor(cmd, cwd) {
|
|
1095
|
+
this.cwd = cwd;
|
|
1096
|
+
const parts = cmd.split(" ");
|
|
1097
|
+
this.proc = spawnProc(parts, { cwd });
|
|
1098
|
+
this.readLoop();
|
|
1099
|
+
this.ready = this.initialize();
|
|
1100
|
+
}
|
|
1101
|
+
send(msg) {
|
|
1102
|
+
const body = Buffer.from(JSON.stringify(msg), "utf8");
|
|
1103
|
+
const header = Buffer.from(`Content-Length: ${body.length}\r
|
|
1104
|
+
\r
|
|
1105
|
+
`, "ascii");
|
|
1106
|
+
this.proc.stdin?.write(Buffer.concat([header, body]));
|
|
1107
|
+
}
|
|
1108
|
+
request(method, params) {
|
|
1109
|
+
const id = this.id++;
|
|
1110
|
+
this.send({ jsonrpc: "2.0", id, method, params });
|
|
1111
|
+
return new Promise((resolve3, reject) => {
|
|
1112
|
+
this.pending.set(id, { resolve: resolve3, reject });
|
|
1113
|
+
setTimeout(() => {
|
|
1114
|
+
if (this.pending.delete(id))
|
|
1115
|
+
reject(new Error(`LSP ${method} timed out`));
|
|
1116
|
+
}, 20000);
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
notify(method, params) {
|
|
1120
|
+
this.send({ jsonrpc: "2.0", method, params });
|
|
1121
|
+
}
|
|
1122
|
+
readLoop() {
|
|
1123
|
+
this.proc.stdout?.on("data", (value) => {
|
|
1124
|
+
this.buf = Buffer.concat([this.buf, value]);
|
|
1125
|
+
for (;; ) {
|
|
1126
|
+
const sep = this.buf.indexOf(`\r
|
|
1127
|
+
\r
|
|
1128
|
+
`);
|
|
1129
|
+
if (sep === -1)
|
|
1130
|
+
break;
|
|
1131
|
+
const header = this.buf.subarray(0, sep).toString("ascii");
|
|
1132
|
+
const m = header.match(/Content-Length:\s*(\d+)/i);
|
|
1133
|
+
if (!m) {
|
|
1134
|
+
this.buf = this.buf.subarray(sep + 4);
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
const len = Number(m[1]);
|
|
1138
|
+
const start = sep + 4;
|
|
1139
|
+
if (this.buf.length < start + len)
|
|
1140
|
+
break;
|
|
1141
|
+
const body = this.buf.subarray(start, start + len).toString("utf8");
|
|
1142
|
+
this.buf = this.buf.subarray(start + len);
|
|
1143
|
+
try {
|
|
1144
|
+
this.dispatch(JSON.parse(body));
|
|
1145
|
+
} catch {}
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
dispatch(msg) {
|
|
1150
|
+
if (msg.id !== undefined && this.pending.has(msg.id)) {
|
|
1151
|
+
const p = this.pending.get(msg.id);
|
|
1152
|
+
this.pending.delete(msg.id);
|
|
1153
|
+
msg.error ? p.reject(new Error(msg.error.message)) : p.resolve(msg.result);
|
|
1154
|
+
} else if (msg.method === "textDocument/publishDiagnostics") {
|
|
1155
|
+
this.diagnostics.set(msg.params.uri, msg.params.diagnostics ?? []);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
async initialize() {
|
|
1159
|
+
await this.request("initialize", {
|
|
1160
|
+
processId: process.pid,
|
|
1161
|
+
rootUri: pathToFileURL(this.cwd).href,
|
|
1162
|
+
capabilities: { textDocument: { publishDiagnostics: {}, definition: {}, references: {}, hover: {} } }
|
|
1163
|
+
});
|
|
1164
|
+
this.notify("initialized", {});
|
|
1165
|
+
}
|
|
1166
|
+
uri(file) {
|
|
1167
|
+
return pathToFileURL(join4(this.cwd, file)).href;
|
|
1168
|
+
}
|
|
1169
|
+
async openDoc(file) {
|
|
1170
|
+
const uri = this.uri(file);
|
|
1171
|
+
if (!this.opened.has(uri)) {
|
|
1172
|
+
const text = readFileSync6(join4(this.cwd, file), "utf8");
|
|
1173
|
+
const languageId = extname(file).replace(/^\./, "");
|
|
1174
|
+
this.notify("textDocument/didOpen", { textDocument: { uri, languageId, version: 1, text } });
|
|
1175
|
+
this.opened.add(uri);
|
|
1176
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
1177
|
+
}
|
|
1178
|
+
return uri;
|
|
1179
|
+
}
|
|
1180
|
+
async definition(file, line, character) {
|
|
1181
|
+
await this.ready;
|
|
1182
|
+
const uri = await this.openDoc(file);
|
|
1183
|
+
return this.request("textDocument/definition", { textDocument: { uri }, position: { line, character } });
|
|
1184
|
+
}
|
|
1185
|
+
async references(file, line, character) {
|
|
1186
|
+
await this.ready;
|
|
1187
|
+
const uri = await this.openDoc(file);
|
|
1188
|
+
return this.request("textDocument/references", { textDocument: { uri }, position: { line, character }, context: { includeDeclaration: true } });
|
|
1189
|
+
}
|
|
1190
|
+
async hover(file, line, character) {
|
|
1191
|
+
await this.ready;
|
|
1192
|
+
const uri = await this.openDoc(file);
|
|
1193
|
+
return this.request("textDocument/hover", { textDocument: { uri }, position: { line, character } });
|
|
1194
|
+
}
|
|
1195
|
+
async diags(file) {
|
|
1196
|
+
await this.ready;
|
|
1197
|
+
const uri = await this.openDoc(file);
|
|
1198
|
+
return this.diagnostics.get(uri) ?? [];
|
|
1199
|
+
}
|
|
1200
|
+
close() {
|
|
1201
|
+
try {
|
|
1202
|
+
this.proc.kill();
|
|
1203
|
+
} catch {}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
var _servers = new Map;
|
|
1207
|
+
function closeAllLsp() {
|
|
1208
|
+
for (const s of _servers.values())
|
|
1209
|
+
s.close();
|
|
1210
|
+
_servers.clear();
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// src/tools/taskTools.ts
|
|
1214
|
+
var kanban = {
|
|
1215
|
+
name: "kanban",
|
|
1216
|
+
description: "Track multi-step work on a kanban board: add/move/list/remove cards across todo→doing→review→done. Resumable across sessions.",
|
|
1217
|
+
permission: "safe",
|
|
1218
|
+
parameters: {
|
|
1219
|
+
type: "object",
|
|
1220
|
+
properties: {
|
|
1221
|
+
action: { type: "string", enum: ["add", "move", "list", "remove"] },
|
|
1222
|
+
title: { type: "string" },
|
|
1223
|
+
body: { type: "string" },
|
|
1224
|
+
id: { type: "number" },
|
|
1225
|
+
column: { type: "string", enum: COLUMNS }
|
|
1226
|
+
},
|
|
1227
|
+
required: ["action"]
|
|
1228
|
+
},
|
|
1229
|
+
async run(args, ctx) {
|
|
1230
|
+
switch (String(args.action)) {
|
|
1231
|
+
case "add": {
|
|
1232
|
+
const c = addCard(ctx.cwd, String(args.title ?? "untitled"), String(args.body ?? ""));
|
|
1233
|
+
return `added #${c.id} "${c.title}" → todo`;
|
|
1234
|
+
}
|
|
1235
|
+
case "move": {
|
|
1236
|
+
if (!args.id || !args.column)
|
|
1237
|
+
return "need id + column";
|
|
1238
|
+
moveCard(Number(args.id), String(args.column));
|
|
1239
|
+
return `moved #${args.id} → ${args.column}`;
|
|
1240
|
+
}
|
|
1241
|
+
case "remove": {
|
|
1242
|
+
if (!args.id)
|
|
1243
|
+
return "need id";
|
|
1244
|
+
removeCard(Number(args.id));
|
|
1245
|
+
return `removed #${args.id}`;
|
|
1246
|
+
}
|
|
1247
|
+
default:
|
|
1248
|
+
return renderBoard(ctx.cwd) || "(board empty)";
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
var schedule = {
|
|
1253
|
+
name: "schedule",
|
|
1254
|
+
description: "Schedule a task: run a prompt later/repeatedly. schedule is interval seconds ('300') or a 5-field cron string ('0 9 * * 1'). Use to set up background or recurring work.",
|
|
1255
|
+
permission: "ask",
|
|
1256
|
+
parameters: {
|
|
1257
|
+
type: "object",
|
|
1258
|
+
properties: {
|
|
1259
|
+
action: { type: "string", enum: ["add", "list", "remove"] },
|
|
1260
|
+
name: { type: "string" },
|
|
1261
|
+
schedule: { type: "string" },
|
|
1262
|
+
prompt: { type: "string" },
|
|
1263
|
+
id: { type: "number" }
|
|
1264
|
+
},
|
|
1265
|
+
required: ["action"]
|
|
1266
|
+
},
|
|
1267
|
+
async run(args, ctx) {
|
|
1268
|
+
switch (String(args.action)) {
|
|
1269
|
+
case "add": {
|
|
1270
|
+
if (!args.schedule || !args.prompt)
|
|
1271
|
+
return "need schedule + prompt";
|
|
1272
|
+
const j = addJob(String(args.name ?? "job"), String(args.schedule), String(args.prompt), ctx.cwd);
|
|
1273
|
+
return `scheduled #${j.id} "${j.name}" — next run ${new Date(j.next_run).toLocaleString()}`;
|
|
1274
|
+
}
|
|
1275
|
+
case "remove": {
|
|
1276
|
+
if (!args.id)
|
|
1277
|
+
return "need id";
|
|
1278
|
+
removeJob(Number(args.id));
|
|
1279
|
+
return `removed job #${args.id}`;
|
|
1280
|
+
}
|
|
1281
|
+
default: {
|
|
1282
|
+
const js = listJobs();
|
|
1283
|
+
return js.length ? js.map((j) => `#${j.id} ${j.name} [${j.schedule}] next ${new Date(j.next_run).toLocaleString()} ${j.last_status ? "· " + j.last_status : ""}`).join(`
|
|
1284
|
+
`) : "(no scheduled jobs)";
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
function registerTaskTools() {
|
|
1290
|
+
register(kanban);
|
|
1291
|
+
register(schedule);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// src/tips/tips.ts
|
|
1295
|
+
var FREQUENCY = Math.max(1, Number(process.env.MODELCODE_TIP_FREQUENCY || "1"));
|
|
1296
|
+
|
|
1297
|
+
// src/wallet/store.ts
|
|
1298
|
+
import { join as join5 } from "path";
|
|
1299
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
1300
|
+
function walletPath() {
|
|
1301
|
+
return join5(globalDir(), "wallet.json");
|
|
1302
|
+
}
|
|
1303
|
+
function walletExists() {
|
|
1304
|
+
return existsSync8(walletPath());
|
|
1305
|
+
}
|
|
1306
|
+
function walletAddress() {
|
|
1307
|
+
try {
|
|
1308
|
+
return JSON.parse(readFileSync7(walletPath(), "utf8")).address;
|
|
1309
|
+
} catch {
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
async function createNew(password) {
|
|
1314
|
+
const mnemonic = generateMnemonic(128);
|
|
1315
|
+
const w = walletFromMnemonic(mnemonic, MAINNET);
|
|
1316
|
+
writeFileSync5(walletPath(), JSON.stringify(await encryptWallet(w, password), null, 2), { mode: 384 });
|
|
1317
|
+
return { address: w.address, mnemonic };
|
|
1318
|
+
}
|
|
1319
|
+
async function importWallet(secret, password) {
|
|
1320
|
+
const s = secret.trim();
|
|
1321
|
+
let w;
|
|
1322
|
+
if (/^(0x)?[0-9a-fA-F]{64}$/.test(s))
|
|
1323
|
+
w = importWalletHex(s, MAINNET);
|
|
1324
|
+
else if (validateMnemonic(s))
|
|
1325
|
+
w = walletFromMnemonic(s, MAINNET);
|
|
1326
|
+
else
|
|
1327
|
+
throw new Error("not a valid BIP-39 phrase or 64-char private-key hex");
|
|
1328
|
+
writeFileSync5(walletPath(), JSON.stringify(await encryptWallet(w, password), null, 2), { mode: 384 });
|
|
1329
|
+
return { address: w.address };
|
|
1330
|
+
}
|
|
1331
|
+
async function loadWallet(password) {
|
|
1332
|
+
if (!walletExists())
|
|
1333
|
+
throw new Error("no wallet — run `modelcode wallet new` or `wallet import`");
|
|
1334
|
+
const enc = JSON.parse(readFileSync7(walletPath(), "utf8"));
|
|
1335
|
+
return decryptWallet(enc, password, MAINNET);
|
|
1336
|
+
}
|
|
1337
|
+
// src/cli/main.ts
|
|
1338
|
+
var C = { dim: "\x1B[2m", green: "\x1B[32m", amber: "\x1B[33m", cyan: "\x1B[36m", reset: "\x1B[0m", bold: "\x1B[1m" };
|
|
1339
|
+
var grainsToMdl = (g) => (g / 1e8).toFixed(6);
|
|
1340
|
+
async function login() {
|
|
1341
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
1342
|
+
const ok = await interactiveLogin(rl, loadConfig());
|
|
1343
|
+
rl.close();
|
|
1344
|
+
if (!ok)
|
|
1345
|
+
process.exit(1);
|
|
1346
|
+
}
|
|
1347
|
+
async function interactiveLogin(rl, cfg) {
|
|
1348
|
+
stdout.write(`How would you like to start?
|
|
1349
|
+
` + ` ${C.cyan}1${C.reset}) paste an existing API key (mdlk_\u2026)
|
|
1350
|
+
` + ` ${C.cyan}2${C.reset}) create a wallet + API key ${C.dim}(fund the wallet to buy credits)${C.reset}
|
|
1351
|
+
` + ` ${C.cyan}3${C.reset}) skip for now
|
|
1352
|
+
`);
|
|
1353
|
+
const choice = (await rl.question(`${C.green}choose 1-3\u203A${C.reset} `)).trim();
|
|
1354
|
+
if (choice === "1" || choice.startsWith("mdlk_")) {
|
|
1355
|
+
const key = choice.startsWith("mdlk_") ? choice : (await rl.question("API key (mdlk_\u2026): ")).trim();
|
|
1356
|
+
if (!key.startsWith("mdlk_")) {
|
|
1357
|
+
stdout.write(`${C.amber}that doesn't look like an mdlk_ key${C.reset}
|
|
1358
|
+
`);
|
|
1359
|
+
return false;
|
|
1360
|
+
}
|
|
1361
|
+
saveConfig({ apiKey: key });
|
|
1362
|
+
cfg.apiKey = key;
|
|
1363
|
+
stdout.write(`${C.green}\u2713 logged in${C.reset} (key saved to ~/.modelcode/config.json)
|
|
1364
|
+
`);
|
|
1365
|
+
return true;
|
|
1366
|
+
}
|
|
1367
|
+
if (choice === "2") {
|
|
1368
|
+
if (walletExists()) {
|
|
1369
|
+
stdout.write(`${C.dim}a wallet already exists; registering a key for it\u2026${C.reset}
|
|
1370
|
+
`);
|
|
1371
|
+
}
|
|
1372
|
+
let address = walletAddress() ?? "";
|
|
1373
|
+
if (!address) {
|
|
1374
|
+
const pw = (await rl.question("set a wallet password: ")).trim();
|
|
1375
|
+
if (!pw) {
|
|
1376
|
+
stdout.write(`${C.amber}password required${C.reset}
|
|
1377
|
+
`);
|
|
1378
|
+
return false;
|
|
1379
|
+
}
|
|
1380
|
+
const r = await createNew(pw);
|
|
1381
|
+
address = r.address;
|
|
1382
|
+
stdout.write(`${C.green}\u2713 wallet created${C.reset} ${address}
|
|
1383
|
+
` + `${C.amber}RECOVERY PHRASE (write it down, shown once):${C.reset}
|
|
1384
|
+
${r.mnemonic}
|
|
1385
|
+
`);
|
|
1386
|
+
}
|
|
1387
|
+
try {
|
|
1388
|
+
const { apiKey, depositAddr } = await registerApiKey(address);
|
|
1389
|
+
saveConfig({ apiKey });
|
|
1390
|
+
cfg.apiKey = apiKey;
|
|
1391
|
+
stdout.write(`${C.green}\u2713 API key created + saved${C.reset}
|
|
1392
|
+
` + `${C.dim}To buy credits: send MDL to your wallet, then run ${C.reset}modelcode credits fund <mdl>${C.dim} ` + `(it funds the API bank ${depositAddr ? depositAddr.slice(0, 16) + "\u2026" : ""} and claims credits).${C.reset}
|
|
1393
|
+
`);
|
|
1394
|
+
return true;
|
|
1395
|
+
} catch (e) {
|
|
1396
|
+
stdout.write(`${C.amber}key registration failed: ${e.message}${C.reset}
|
|
1397
|
+
`);
|
|
1398
|
+
return false;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
stdout.write(`${C.dim}(skipped \u2014 /help works; paste a key anytime to run prompts)${C.reset}
|
|
1402
|
+
`);
|
|
1403
|
+
return false;
|
|
1404
|
+
}
|
|
1405
|
+
async function maybeAutoKey(address) {
|
|
1406
|
+
const cfg = loadConfig();
|
|
1407
|
+
if (cfg.apiKey) {
|
|
1408
|
+
stdout.write(`${C.dim}(an API key is already configured; keeping it)${C.reset}
|
|
1409
|
+
`);
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
1413
|
+
const ans = (await rl.question("Generate an API key for this wallet now (use the internal wallet for inference)? [Y/n] ")).trim().toLowerCase();
|
|
1414
|
+
rl.close();
|
|
1415
|
+
if (ans === "n" || ans === "no")
|
|
1416
|
+
return;
|
|
1417
|
+
try {
|
|
1418
|
+
const { apiKey, depositAddr } = await registerApiKey(address);
|
|
1419
|
+
saveConfig({ apiKey });
|
|
1420
|
+
stdout.write(`${C.green}API key created & saved${C.reset} (${apiKey.slice(0, 12)}\u2026)
|
|
1421
|
+
` + `deposit address: ${depositAddr}
|
|
1422
|
+
fund it in one command: ${C.cyan}modelcode credits fund <mdl>${C.reset}
|
|
1423
|
+
`);
|
|
1424
|
+
} catch (e) {
|
|
1425
|
+
stdout.write(`${C.amber}could not auto-create key: ${e.message}${C.reset}
|
|
1426
|
+
`);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
async function password(prompt = "Wallet password: ") {
|
|
1430
|
+
if (process.env.MODELCODE_WALLET_PASSWORD)
|
|
1431
|
+
return process.env.MODELCODE_WALLET_PASSWORD;
|
|
1432
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
1433
|
+
const p = await rl.question(prompt);
|
|
1434
|
+
rl.close();
|
|
1435
|
+
return p;
|
|
1436
|
+
}
|
|
1437
|
+
async function walletCmd(sub, arg) {
|
|
1438
|
+
if (sub === "new") {
|
|
1439
|
+
if (walletExists()) {
|
|
1440
|
+
stdout.write(`a wallet already exists (~/.modelcode/wallet.json). Remove it first to replace.
|
|
1441
|
+
`);
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
const pw = await password("Set a password to encrypt the wallet: ");
|
|
1445
|
+
const { address, mnemonic } = await createNew(pw);
|
|
1446
|
+
stdout.write(`${C.green}wallet created${C.reset}
|
|
1447
|
+
address: ${address}
|
|
1448
|
+
|
|
1449
|
+
${C.amber}RECOVERY PHRASE (write it down, shown once):${C.reset}
|
|
1450
|
+
${mnemonic}
|
|
1451
|
+
`);
|
|
1452
|
+
await maybeAutoKey(address);
|
|
1453
|
+
} else if (sub === "import") {
|
|
1454
|
+
if (!arg) {
|
|
1455
|
+
stdout.write(`usage: modelcode wallet import "<12/24-word phrase>" | <privkey-hex>
|
|
1456
|
+
`);
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
const pw = await password("Set a password to encrypt the wallet: ");
|
|
1460
|
+
const { address } = await importWallet(arg, pw);
|
|
1461
|
+
stdout.write(`${C.green}wallet imported${C.reset}
|
|
1462
|
+
address: ${address}
|
|
1463
|
+
`);
|
|
1464
|
+
await maybeAutoKey(address);
|
|
1465
|
+
} else if (sub === "address") {
|
|
1466
|
+
stdout.write((walletAddress() ?? "no wallet \u2014 `modelcode wallet new`") + `
|
|
1467
|
+
`);
|
|
1468
|
+
} else if (sub === "balance") {
|
|
1469
|
+
const addr = walletAddress();
|
|
1470
|
+
if (!addr) {
|
|
1471
|
+
stdout.write(`no wallet
|
|
1472
|
+
`);
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
const utxos = await fetchUtxos(addr);
|
|
1476
|
+
const total = utxos.reduce((s, u) => s + u.value, 0n);
|
|
1477
|
+
stdout.write(`on-chain: ${(Number(total) / 1e8).toFixed(6)} MDL across ${utxos.length} UTXO(s)
|
|
1478
|
+
address: ${addr}
|
|
1479
|
+
`);
|
|
1480
|
+
} else if (sub === "export") {
|
|
1481
|
+
const pw = await password();
|
|
1482
|
+
try {
|
|
1483
|
+
const w = await loadWallet(pw);
|
|
1484
|
+
stdout.write(`${C.amber}KEEP SECRET${C.reset}
|
|
1485
|
+
address: ${w.address}
|
|
1486
|
+
private key: ${Buffer.from(w.privateKey).toString("hex")}
|
|
1487
|
+
${w.mnemonic ? "phrase: " + w.mnemonic + `
|
|
1488
|
+
` : ""}`);
|
|
1489
|
+
} catch (e) {
|
|
1490
|
+
stdout.write(`${C.amber}${e.message}${C.reset}
|
|
1491
|
+
`);
|
|
1492
|
+
}
|
|
1493
|
+
} else {
|
|
1494
|
+
stdout.write(`wallet commands: new | import <secret> | address | balance | export
|
|
1495
|
+
`);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
async function fundCmd(cfg, mdlStr) {
|
|
1499
|
+
const mdl = Number(mdlStr);
|
|
1500
|
+
if (!cfg.apiKey) {
|
|
1501
|
+
stdout.write("login first (`modelcode login`)\n");
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (!walletExists()) {
|
|
1505
|
+
stdout.write("no wallet \u2014 `modelcode wallet new` or `wallet import` first\n");
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
if (!(mdl > 0)) {
|
|
1509
|
+
stdout.write(`usage: modelcode credits fund <mdl>
|
|
1510
|
+
`);
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
const pw = await password();
|
|
1514
|
+
const w = await loadWallet(pw);
|
|
1515
|
+
const info = await accountInfo(w.address);
|
|
1516
|
+
if (!info) {
|
|
1517
|
+
stdout.write(`could not reach the credits API / get the deposit address
|
|
1518
|
+
`);
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
const utxos = await fetchUtxos(w.address);
|
|
1522
|
+
if (!utxos.length) {
|
|
1523
|
+
stdout.write(`wallet ${w.address} has no funds. Send MDL to it first.
|
|
1524
|
+
`);
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
const amount = BigInt(Math.round(mdl * 1e8));
|
|
1528
|
+
stdout.write(`${C.dim}signing transfer of ${mdl} MDL \u2192 bank ${info.depositAddr.slice(0, 16)}\u2026${C.reset}
|
|
1529
|
+
`);
|
|
1530
|
+
const built = createTransfer(w, utxos, info.depositAddr, amount);
|
|
1531
|
+
const txid = await broadcast(built.hex);
|
|
1532
|
+
stdout.write(`${C.green}broadcast${C.reset} txid ${txid}
|
|
1533
|
+
`);
|
|
1534
|
+
const deadline = Date.now() + 6 * 60000;
|
|
1535
|
+
stdout.write(`${C.dim}waiting for confirmation, then claiming as credits\u2026${C.reset}
|
|
1536
|
+
`);
|
|
1537
|
+
for (let i = 0;; i++) {
|
|
1538
|
+
const r = await claimDeposit(cfg.apiKey, w.address, txid);
|
|
1539
|
+
if (r.ok) {
|
|
1540
|
+
stdout.write(`${C.green}\u2713 ${r.message}${C.reset}
|
|
1541
|
+
`);
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
if (!/confirmation/i.test(r.message)) {
|
|
1545
|
+
stdout.write(`${C.amber}\u2717 ${r.message}${C.reset}
|
|
1546
|
+
`);
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
if (Date.now() >= deadline) {
|
|
1550
|
+
stdout.write(`${C.amber}deposit broadcast but not yet confirmed \u2014 finish later with:${C.reset}
|
|
1551
|
+
` + ` modelcode credits claim ${w.address} ${txid}
|
|
1552
|
+
`);
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
if (i === 0)
|
|
1556
|
+
stdout.write(`${C.dim} ${r.message} \u2014 retrying\u2026${C.reset}
|
|
1557
|
+
`);
|
|
1558
|
+
await new Promise((res) => setTimeout(res, 12000));
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
async function main() {
|
|
1562
|
+
const [cmd, sub, arg] = process.argv.slice(2);
|
|
1563
|
+
const cfg = loadConfig();
|
|
1564
|
+
if (cmd === "login")
|
|
1565
|
+
return login();
|
|
1566
|
+
if (cmd === "config") {
|
|
1567
|
+
stdout.write(JSON.stringify({ ...cfg, apiKey: cfg.apiKey ? "mdlk_\u2026(set)" : undefined }, null, 2) + `
|
|
1568
|
+
`);
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
if (cmd === "wallet")
|
|
1572
|
+
return walletCmd(sub ?? "", arg);
|
|
1573
|
+
async function launchTui(resume) {
|
|
1574
|
+
if (!cfg.apiKey) {
|
|
1575
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
1576
|
+
await interactiveLogin(rl, cfg);
|
|
1577
|
+
rl.close();
|
|
1578
|
+
}
|
|
1579
|
+
if (!cfg.apiKey)
|
|
1580
|
+
return;
|
|
1581
|
+
const { runTui } = await import("./tui-0r2q70wm.mjs");
|
|
1582
|
+
return runTui(cfg, resume);
|
|
1583
|
+
}
|
|
1584
|
+
if (cmd === "tui")
|
|
1585
|
+
return launchTui("none");
|
|
1586
|
+
if (cmd === "credits") {
|
|
1587
|
+
if (sub === "fund")
|
|
1588
|
+
return void await fundCmd(cfg, arg ?? "");
|
|
1589
|
+
if (sub === "claim") {
|
|
1590
|
+
const claimAddr = arg, claimTxid = process.argv[5];
|
|
1591
|
+
if (!claimAddr || !claimTxid) {
|
|
1592
|
+
stdout.write(`usage: modelcode credits claim <address> <txid>
|
|
1593
|
+
`);
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
if (!cfg.apiKey) {
|
|
1597
|
+
stdout.write("No API key. Run `modelcode login` first.\n");
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
const r = await claimDeposit(cfg.apiKey, claimAddr, claimTxid);
|
|
1601
|
+
stdout.write(`${r.ok ? C.green + "\u2713 " : C.amber + "\u2717 "}${r.message}${C.reset}
|
|
1602
|
+
`);
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
const addr = walletAddress();
|
|
1606
|
+
const b = addr ? await creditBalance(addr) : null;
|
|
1607
|
+
stdout.write(b ? `credits: ${b.mdl.toFixed(6)} MDL (wallet ${addr})
|
|
1608
|
+
` : `${FUND_INSTRUCTIONS}
|
|
1609
|
+
`);
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
if (cmd === "-p" || cmd === "--print" || cmd === "run" && sub) {
|
|
1613
|
+
if (!cfg.apiKey) {
|
|
1614
|
+
stdout.write("No API key. Run `modelcode login` first.\n");
|
|
1615
|
+
process.exit(1);
|
|
1616
|
+
}
|
|
1617
|
+
const promptArg = (cmd === "run" ? process.argv.slice(3) : process.argv.slice(3)).join(" ").trim();
|
|
1618
|
+
const prompt = promptArg || (!stdin.isTTY ? readFileSync8(0, "utf8").trim() : "");
|
|
1619
|
+
if (!prompt) {
|
|
1620
|
+
stdout.write(`usage: modelcode -p "<prompt>"
|
|
1621
|
+
`);
|
|
1622
|
+
process.exit(1);
|
|
1623
|
+
}
|
|
1624
|
+
return runOnce(cfg, prompt);
|
|
1625
|
+
}
|
|
1626
|
+
const resumeMode = cmd === "--continue" || cmd === "-c" ? "continue" : cmd === "--resume" || cmd === "-r" ? "resume" : "none";
|
|
1627
|
+
return launchTui(resumeMode);
|
|
1628
|
+
}
|
|
1629
|
+
async function runOnce(cfg, prompt) {
|
|
1630
|
+
const ctx = { cwd: process.cwd() };
|
|
1631
|
+
registerCoreTools();
|
|
1632
|
+
registerMemoryTools();
|
|
1633
|
+
registerAgentTool(cfg);
|
|
1634
|
+
registerSkillManager();
|
|
1635
|
+
registerMoreTools();
|
|
1636
|
+
registerBrowserTool();
|
|
1637
|
+
registerTaskTools();
|
|
1638
|
+
let cost = 0;
|
|
1639
|
+
const agent = new Agent(cfg, ctx, {
|
|
1640
|
+
onAssistantDelta: (t) => stdout.write(t),
|
|
1641
|
+
onToolStart: (n, a) => stdout.write(`
|
|
1642
|
+
${C.dim}\u2699 ${n} ${JSON.stringify(a).slice(0, 100)}${C.reset}
|
|
1643
|
+
`),
|
|
1644
|
+
onToolResult: () => {},
|
|
1645
|
+
onCost: (g) => {
|
|
1646
|
+
cost += g;
|
|
1647
|
+
},
|
|
1648
|
+
confirm: async () => true
|
|
1649
|
+
});
|
|
1650
|
+
try {
|
|
1651
|
+
await agent.send(expandFileMentions(prompt, ctx.cwd), imageMentions(prompt, ctx.cwd));
|
|
1652
|
+
} catch (e) {
|
|
1653
|
+
stdout.write(`
|
|
1654
|
+
error: ${e.message}
|
|
1655
|
+
`);
|
|
1656
|
+
process.exitCode = 1;
|
|
1657
|
+
} finally {
|
|
1658
|
+
stdout.write(`
|
|
1659
|
+
${C.dim}\u2014 ${grainsToMdl(cost)} MDL${C.reset}
|
|
1660
|
+
`);
|
|
1661
|
+
await closeBrowser();
|
|
1662
|
+
closeAllLsp();
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
main().catch((e) => {
|
|
1666
|
+
stdout.write(`fatal: ${e?.message ?? e}
|
|
1667
|
+
`);
|
|
1668
|
+
process.exit(1);
|
|
1669
|
+
});
|