Skip to content

Commit 08dcad2

Browse files
author
Splash Agent
committed
feat: phase C complete with wizard, self-modifier, and cross-device scripts
1 parent a809298 commit 08dcad2

15 files changed

Lines changed: 320 additions & 135 deletions

File tree

examples/cli.ts

Lines changed: 212 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ async function main() {
110110
"version", "help", "init", "setup", "config", "chat", "telegram", "slack",
111111
"whatsapp", "messenger", "discord", "runs", "tui", "dashboard", "self-improve",
112112
"providers", "skills", "memory", "maintenance", "voice", "swarm", "antigravity",
113-
"run", "doctor", "browser"
113+
"run", "doctor", "browser", "self-modify", "self-rollback"
114114
];
115115

116116
if (!KNOWN_COMMANDS.includes(cmd)) {
@@ -173,6 +173,12 @@ async function main() {
173173
case "self-improve":
174174
await runSelfImprove();
175175
return;
176+
case "self-modify":
177+
await runSelfModify(args.slice(1));
178+
return;
179+
case "self-rollback":
180+
await runSelfRollback(args.slice(1));
181+
return;
176182
case "providers":
177183
await runProviders(args.slice(1));
178184
return;
@@ -259,107 +265,142 @@ function printHelp(): void {
259265

260266
async function runInit() {
261267
console.log(renderBanner({ subtitle: "Setup" }));
262-
p.intro(pc.bgCyan(pc.black(" SPLASH INIT ")));
263-
p.log.message("Pick a provider and drop in a key. You can change this any time with `splash config`.");
268+
p.intro(pc.bgCyan(pc.black(" SPLASH INIT V2 ")));
269+
p.log.message("Let's set up your Splash environment in 8 steps.");
264270

265271
const existing = readGlobalConfig();
266272
const next: GlobalConfigShape = { ...existing };
267273
next.apiKeys = { ...(existing.apiKeys || {}) };
274+
next.providers = { ...(existing.providers || {}) };
275+
next.providers.apiKeys = next.apiKeys;
268276

277+
// Step 2: Primary Provider Choice
269278
const provider = await p.select({
270-
message: "1. Default provider:",
279+
message: "1. Primary Provider (Top 10):",
271280
options: [
272-
{ value: "openrouter", label: "OpenRouter — recommended, unlocks 300+ models" },
273-
{ value: "claude", label: "Anthropic Claude — direct API" },
274-
{ value: "openai", label: "OpenAI — GPT-4o, o3, o4-mini" },
275-
{ value: "gemini", label: "Google Gemini — 2.5 Pro/Flash" },
276-
{ value: "deepseek", label: "DeepSeek — R1, V3 (affordable)" },
277-
{ value: "ollama", label: "Ollama — local models, no key needed" },
281+
{ value: "openrouter", label: "OpenRouter (Recommended — 300+ models)" },
282+
{ value: "openai", label: "OpenAI (GPT-4o, o3)" },
283+
{ value: "anthropic", label: "Anthropic (Claude 3.5)" },
284+
{ value: "google", label: "Google Gemini" },
285+
{ value: "deepseek", label: "DeepSeek (R1, V3)" },
286+
{ value: "mistral", label: "Mistral" },
287+
{ value: "groq", label: "Groq (Ultra-fast)" },
288+
{ value: "cohere", label: "Cohere" },
289+
{ value: "nvidia", label: "Nvidia NIM" },
290+
{ value: "together", label: "Together AI" },
291+
{ value: "ollama", label: "Ollama (Local)" },
278292
],
279293
});
280294
if (p.isCancel(provider)) return abort();
281295

282296
next.defaultProvider = provider as string;
297+
next.providers.default = provider as string;
283298

299+
// Step 3: API Key Input
284300
if (provider !== "ollama") {
285301
const key = await p.password({ message: `2. Paste your ${provider} API key (input hidden):` });
286302
if (p.isCancel(key)) return abort();
287-
if (key) next.apiKeys[provider as string] = key as string;
303+
if (key) {
304+
next.apiKeys[provider as string] = key as string;
305+
next.providers.apiKeys[provider as string] = key as string;
306+
}
288307
}
289308

290-
if (provider === "openrouter") {
291-
const s = p.spinner();
292-
s.start("Fetching live OpenRouter models...");
293-
let liveModels: any[] = [];
294-
try {
295-
const res = await fetch("https://openrouter.ai/api/v1/models");
296-
const data = await res.json() as any;
297-
liveModels = data.data.sort((a: any, b: any) => a.id.localeCompare(b.id));
298-
} catch (e) {
299-
// Fallback
300-
}
301-
s.stop("Models loaded.");
302-
303-
const category = await p.select({
304-
message: "3. Model Category:",
305-
options: [
306-
{ value: "free", label: "Free — Top performing free models (DeepSeek, Llama)" },
307-
{ value: "paid", label: "Premium (Paid) — SOTA models (Claude 3.5, GPT-4o, Gemini Pro)" },
308-
{ value: "all", label: "All Live Models" },
309-
]
310-
});
311-
if (p.isCancel(category)) return abort();
312-
313-
let options: { value: string; label: string }[] = [];
314-
if (liveModels.length > 0) {
315-
const isFree = (m: any) => m.pricing?.prompt === "0" && m.pricing?.completion === "0";
316-
const filtered = category === "free" ? liveModels.filter(isFree)
317-
: category === "paid" ? liveModels.filter((m: any) => !isFree(m))
318-
: liveModels;
319-
320-
options = filtered.slice(0, 50).map((m: any) => ({
321-
value: m.id,
322-
label: `${m.name} ${isFree(m) ? pc.green("(Free)") : pc.yellow("(Paid)")} - ${m.context_length}k ctx`
323-
}));
324-
} else {
325-
// Fallback hardcoded if offline
326-
options = [
327-
{ value: "anthropic/claude-3.5-sonnet", label: "Claude 3.5 Sonnet (Premium)" },
328-
{ value: "deepseek/deepseek-r1:free", label: "DeepSeek R1 (Free)" },
329-
];
330-
}
309+
// Step 4: Default Model Selection
310+
let defaultModels: Record<string, string[]> = {
311+
openrouter: ["anthropic/claude-3.5-sonnet", "deepseek/deepseek-r1", "openai/gpt-4o", "google/gemini-2.5-pro", "mistralai/mistral-large"],
312+
openai: ["gpt-4o", "gpt-4o-mini", "o3-mini"],
313+
anthropic: ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022"],
314+
google: ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash"],
315+
deepseek: ["deepseek-chat", "deepseek-reasoner"],
316+
mistral: ["mistral-large-latest", "pixtral-large-latest", "ministral-8b-latest"],
317+
groq: ["llama3-70b-8192", "mixtral-8x7b-32768"],
318+
cohere: ["command-r-plus", "command-r"],
319+
nvidia: ["meta/llama-3.1-70b-instruct", "meta/llama-3.1-405b-instruct"],
320+
together: ["meta-llama/Llama-3-70b-chat-hf", "deepseek-ai/DeepSeek-V3"],
321+
ollama: ["llama3", "mistral", "qwen2.5"],
322+
};
331323

332-
const model = await p.select({
333-
message: "4. Default model:",
334-
options: options,
335-
});
336-
if (!p.isCancel(model)) next.defaultModel = model as string;
337-
}
324+
const modelOptions = defaultModels[provider as string].map(m => ({ value: m, label: m }));
325+
const model = await p.select({
326+
message: "3. Default model:",
327+
options: modelOptions,
328+
});
329+
if (p.isCancel(model)) return abort();
330+
next.defaultModel = model as string;
338331

339-
const safety = await p.select({
340-
message: "4. Safety level:",
332+
// Step 5: Fallback Provider Selection
333+
const fallback = await p.select({
334+
message: "4. Fallback Provider (Used on 429 Rate Limit):",
341335
options: [
342-
{ value: "standard", label: "Standard — confirms risky actions (recommended)" },
343-
{ value: "strict", label: "Strict — confirms every action" },
344-
{ value: "permissive", label: "Permissive — fully autonomous" },
345-
],
336+
{ value: "none", label: "None" },
337+
{ value: "openrouter", label: "OpenRouter" },
338+
{ value: "openai", label: "OpenAI" },
339+
{ value: "anthropic", label: "Anthropic" },
340+
{ value: "google", label: "Google Gemini" },
341+
{ value: "deepseek", label: "DeepSeek" },
342+
{ value: "groq", label: "Groq" },
343+
{ value: "ollama", label: "Ollama (Local)" },
344+
].filter(o => o.value !== provider),
346345
});
347-
if (!p.isCancel(safety)) next.safetyMode = safety as GlobalConfigShape["safetyMode"];
346+
if (p.isCancel(fallback)) return abort();
347+
348+
if (fallback !== "none") {
349+
next.providers.fallbackOrder = [fallback as string];
350+
if (fallback !== "ollama" && !next.apiKeys[fallback as string]) {
351+
const fallbackKey = await p.password({ message: `Paste your ${fallback} API key:` });
352+
if (!p.isCancel(fallbackKey) && fallbackKey) {
353+
next.apiKeys[fallback as string] = fallbackKey as string;
354+
next.providers.apiKeys[fallback as string] = fallbackKey as string;
355+
}
356+
}
357+
} else {
358+
next.providers.fallbackOrder = [];
359+
}
360+
361+
// Step 6: Workspace Setup
362+
const workspace = await p.confirm({
363+
message: `5. Set up global workspace at ~/.splash?`,
364+
initialValue: true,
365+
});
366+
if (p.isCancel(workspace)) return abort();
348367

368+
// Step 7: Theme Selection
349369
const theme = await p.select({
350-
message: "5. CLI Theme:",
370+
message: "6. CLI Theme & UI Mode:",
351371
options: [
352-
{ value: "splash", label: "Splash (Default) — Compact colored banner + minimal output" },
353-
{ value: "hydro", label: "Hydro — Blue ANSI feel, compact" },
354-
{ value: "edge", label: "EdgeNo color, no banner, just prompt" },
355-
{ value: "silent", label: "Silent — Pure prompt, silent execution" },
372+
{ value: "splash", label: "Splash — Compact banner, standard UI" },
373+
{ value: "tui", label: "TUI Dashboard — Interactive full-screen view" },
374+
{ value: "hydro", label: "HydroBlue ANSI feel" },
375+
{ value: "silent", label: "Silent — Pure execution" },
356376
],
357377
});
358378
if (!p.isCancel(theme)) {
359379
next.cli = next.cli || {};
360-
next.cli.style = theme as "splash" | "hydro" | "edge" | "silent";
380+
if (theme === "tui") {
381+
next.tui = true;
382+
next.cli.style = "splash";
383+
} else {
384+
next.tui = false;
385+
next.cli.style = theme as "splash" | "hydro" | "silent";
386+
}
361387
}
362388

389+
// Step 8: Generate Config
390+
const safety = await p.select({
391+
message: "7. Safety level:",
392+
options: [
393+
{ value: "standard", label: "Standard — confirms risky actions" },
394+
{ value: "strict", label: "Strict — confirms every action" },
395+
{ value: "permissive", label: "Permissive — fully autonomous" },
396+
],
397+
});
398+
if (!p.isCancel(safety)) {
399+
next.safetyMode = safety as GlobalConfigShape["safetyMode"];
400+
next.safety = { mode: safety as GlobalConfigShape["safetyMode"] };
401+
}
402+
403+
p.log.message("8. Generating configuration...");
363404
writeGlobalConfig(next);
364405
p.outro(pc.green(`[OK] Saved to ${globalConfigPath()}`));
365406
console.log(pc.dim(`\nTry it: ${pc.cyan("splash \"summarize this folder\"")}`));
@@ -950,6 +991,69 @@ Output ONLY a list of crisp, actionable rules you should adopt. Do not explain t
950991
console.log(getOutput(result));
951992
}
952993

994+
async function runSelfModify(args: string[]) {
995+
const isAutoApprove = args.includes("--apply");
996+
const instruction = args.filter(a => a !== "--apply").join(" ");
997+
if (!instruction) {
998+
console.error(pc.red("Usage: splash self-modify [--apply] <instruction>"));
999+
return;
1000+
}
1001+
1002+
const { SelfModifier } = await import("@alpclaw/core");
1003+
const modifier = new SelfModifier(isAutoApprove);
1004+
1005+
console.log(pc.magenta("\nSPLASH SELF-MODIFIER"));
1006+
console.log(pc.dim(`Instruction: ${instruction}`));
1007+
console.log(pc.dim(`Mode: ${isAutoApprove ? "APPLY" : "DRY-RUN"}\n`));
1008+
1009+
const alpclaw = await buildAgent();
1010+
const prompt = `You are a self-modifying engine. The user has asked you to: ${instruction}
1011+
Analyze the codebase in the current working directory, figure out which file needs changing, and output a JSON array of objects with 'file' and 'content'.
1012+
Example: [{"file": "packages/core/src/index.ts", "content": "export const a = 1;"}]
1013+
Do NOT use markdown blocks around the JSON. Output pure JSON.`;
1014+
1015+
const res = await alpclaw.createAgent({ onPhaseChange: () => {} }).run(prompt);
1016+
const out = getOutput(res);
1017+
1018+
try {
1019+
let jsonStr = out;
1020+
if (jsonStr.includes("\`\`\`json")) {
1021+
jsonStr = jsonStr.split("\`\`\`json")[1].split("\`\`\`")[0].trim();
1022+
}
1023+
const changes = JSON.parse(jsonStr);
1024+
for (const change of changes) {
1025+
if (change.file && change.content) {
1026+
const result = await modifier.applyChange(change.file, change.content, instruction);
1027+
if (!result.ok) {
1028+
console.error(pc.red(`[ERR] Failed to apply to ${change.file}: ${result.error.message}`));
1029+
} else {
1030+
console.log(pc.green(`[OK] Applied to ${change.file}`));
1031+
}
1032+
}
1033+
}
1034+
} catch (e: any) {
1035+
console.error(pc.red("[ERR] Failed to parse agent response as JSON"), e.message);
1036+
console.log(out);
1037+
}
1038+
}
1039+
1040+
async function runSelfRollback(args: string[]) {
1041+
const timestamp = args[0];
1042+
if (!timestamp) {
1043+
console.error(pc.red("Usage: splash self-rollback <timestamp>"));
1044+
return;
1045+
}
1046+
const { SelfModifier } = await import("@alpclaw/core");
1047+
const modifier = new SelfModifier(true);
1048+
const result = await modifier.rollback(timestamp);
1049+
if (!result.ok) {
1050+
console.error(pc.red(`[ERR] Rollback failed: ${result.error.message}`));
1051+
} else {
1052+
console.log(pc.green(`[OK] Rolled back to ${timestamp}`));
1053+
}
1054+
}
1055+
1056+
9531057
// ──────────────────────────────────────────────────────────────────────────
9541058
// Voice
9551059
// ──────────────────────────────────────────────────────────────────────────
@@ -1330,8 +1434,44 @@ async function runMemory(args: string[]): Promise<void> {
13301434
return;
13311435
}
13321436

1437+
if (sub === "import") {
1438+
const inFile = args[1];
1439+
if (!inFile || !fs.existsSync(inFile)) {
1440+
console.error(pc.red("Usage: splash memory import <file.json>"));
1441+
return;
1442+
}
1443+
try {
1444+
const content = fs.readFileSync(inFile, "utf-8");
1445+
const entries = JSON.parse(content);
1446+
if (!Array.isArray(entries)) throw new Error("Expected JSON array");
1447+
1448+
const newSessionId = "imported_" + Date.now();
1449+
const newFile = path.join(sessionsDir, newSessionId + ".jsonl");
1450+
if (!fs.existsSync(sessionsDir)) fs.mkdirSync(sessionsDir, { recursive: true });
1451+
1452+
const lines = entries.map(e => JSON.stringify(e)).join("\n") + "\n";
1453+
fs.writeFileSync(newFile, lines, "utf-8");
1454+
console.log(pc.green(`Imported ${entries.length} entries into session ${newSessionId}`));
1455+
} catch (e: any) {
1456+
console.error(pc.red(`Failed to import: ${e.message}`));
1457+
}
1458+
return;
1459+
}
1460+
1461+
if (sub === "clear") {
1462+
if (fs.existsSync(sessionsDir)) {
1463+
fs.rmSync(sessionsDir, { recursive: true, force: true });
1464+
}
1465+
const knowledgePath = path.join(memDir, "knowledge.jsonl");
1466+
if (fs.existsSync(knowledgePath)) {
1467+
fs.rmSync(knowledgePath, { force: true });
1468+
}
1469+
console.log(pc.green("Cleared all memory."));
1470+
return;
1471+
}
1472+
13331473
console.error(pc.red(`Unknown subcommand: memory ${sub}`));
1334-
console.log(pc.dim(" Available: list, search <query>, export [file]"));
1474+
console.log(pc.dim(" Available: list, search <query>, export [file], import <file>, clear"));
13351475
}
13361476

13371477
// ──────────────────────────────────────────────────────────────────────────

packages/config/src/global-store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface GlobalConfigShape {
2020
providers?: {
2121
default?: string;
2222
apiKeys?: Record<string, string>;
23+
fallbackOrder?: string[];
2324
};
2425
runtime?: "foreground" | "background";
2526
tui?: boolean;

packages/config/src/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const ConfigSchema = z.object({
55
default: z.string().default("openrouter"),
66
defaultModel: z.string().default("moonshotai/kimi-k2"),
77
apiKeys: z.record(z.string(), z.string()).default({}),
8+
fallbackOrder: z.array(z.string()).default([]),
89
}),
910
safety: z.object({
1011
mode: z.enum(["strict", "standard", "permissive"]).default("standard"),

0 commit comments

Comments
 (0)