ashlrcode 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +295 -0
- package/package.json +46 -0
- package/src/__tests__/branded-types.test.ts +47 -0
- package/src/__tests__/context.test.ts +163 -0
- package/src/__tests__/cost-tracker.test.ts +274 -0
- package/src/__tests__/cron.test.ts +197 -0
- package/src/__tests__/dream.test.ts +204 -0
- package/src/__tests__/error-handler.test.ts +192 -0
- package/src/__tests__/features.test.ts +69 -0
- package/src/__tests__/file-history.test.ts +177 -0
- package/src/__tests__/hooks.test.ts +145 -0
- package/src/__tests__/keybindings.test.ts +159 -0
- package/src/__tests__/model-patches.test.ts +82 -0
- package/src/__tests__/permissions-rules.test.ts +121 -0
- package/src/__tests__/permissions.test.ts +108 -0
- package/src/__tests__/project-config.test.ts +63 -0
- package/src/__tests__/retry.test.ts +321 -0
- package/src/__tests__/router.test.ts +158 -0
- package/src/__tests__/session-compact.test.ts +191 -0
- package/src/__tests__/session.test.ts +145 -0
- package/src/__tests__/skill-registry.test.ts +130 -0
- package/src/__tests__/speculation.test.ts +196 -0
- package/src/__tests__/tasks-v2.test.ts +267 -0
- package/src/__tests__/telemetry.test.ts +149 -0
- package/src/__tests__/tool-executor.test.ts +141 -0
- package/src/__tests__/tool-registry.test.ts +166 -0
- package/src/__tests__/undercover.test.ts +93 -0
- package/src/__tests__/workflow.test.ts +195 -0
- package/src/agent/async-context.ts +64 -0
- package/src/agent/context.ts +245 -0
- package/src/agent/cron.ts +189 -0
- package/src/agent/dream.ts +165 -0
- package/src/agent/error-handler.ts +108 -0
- package/src/agent/ipc.ts +256 -0
- package/src/agent/kairos.ts +207 -0
- package/src/agent/loop.ts +314 -0
- package/src/agent/model-patches.ts +68 -0
- package/src/agent/speculation.ts +219 -0
- package/src/agent/sub-agent.ts +125 -0
- package/src/agent/system-prompt.ts +231 -0
- package/src/agent/team.ts +220 -0
- package/src/agent/tool-executor.ts +162 -0
- package/src/agent/workflow.ts +189 -0
- package/src/agent/worktree-manager.ts +86 -0
- package/src/autopilot/queue.ts +186 -0
- package/src/autopilot/scanner.ts +245 -0
- package/src/autopilot/types.ts +58 -0
- package/src/bridge/bridge-client.ts +57 -0
- package/src/bridge/bridge-server.ts +81 -0
- package/src/cli.ts +1120 -0
- package/src/config/features.ts +51 -0
- package/src/config/git.ts +137 -0
- package/src/config/hooks.ts +201 -0
- package/src/config/permissions.ts +251 -0
- package/src/config/project-config.ts +63 -0
- package/src/config/remote-settings.ts +163 -0
- package/src/config/settings-sync.ts +170 -0
- package/src/config/settings.ts +113 -0
- package/src/config/undercover.ts +76 -0
- package/src/config/upgrade-notice.ts +65 -0
- package/src/mcp/client.ts +197 -0
- package/src/mcp/manager.ts +125 -0
- package/src/mcp/oauth.ts +252 -0
- package/src/mcp/types.ts +61 -0
- package/src/persistence/memory.ts +129 -0
- package/src/persistence/session.ts +289 -0
- package/src/planning/plan-mode.ts +128 -0
- package/src/planning/plan-tools.ts +138 -0
- package/src/providers/anthropic.ts +177 -0
- package/src/providers/cost-tracker.ts +184 -0
- package/src/providers/retry.ts +264 -0
- package/src/providers/router.ts +159 -0
- package/src/providers/types.ts +79 -0
- package/src/providers/xai.ts +217 -0
- package/src/repl.tsx +1384 -0
- package/src/setup.ts +119 -0
- package/src/skills/loader.ts +78 -0
- package/src/skills/registry.ts +78 -0
- package/src/skills/types.ts +11 -0
- package/src/state/file-history.ts +264 -0
- package/src/telemetry/event-log.ts +116 -0
- package/src/tools/agent.ts +133 -0
- package/src/tools/ask-user.ts +229 -0
- package/src/tools/bash.ts +146 -0
- package/src/tools/config.ts +147 -0
- package/src/tools/diff.ts +137 -0
- package/src/tools/file-edit.ts +123 -0
- package/src/tools/file-read.ts +82 -0
- package/src/tools/file-write.ts +82 -0
- package/src/tools/glob.ts +76 -0
- package/src/tools/grep.ts +187 -0
- package/src/tools/ls.ts +77 -0
- package/src/tools/lsp.ts +375 -0
- package/src/tools/mcp-resources.ts +83 -0
- package/src/tools/mcp-tool.ts +47 -0
- package/src/tools/memory.ts +148 -0
- package/src/tools/notebook-edit.ts +133 -0
- package/src/tools/peers.ts +113 -0
- package/src/tools/powershell.ts +83 -0
- package/src/tools/registry.ts +114 -0
- package/src/tools/send-message.ts +75 -0
- package/src/tools/sleep.ts +50 -0
- package/src/tools/snip.ts +143 -0
- package/src/tools/tasks.ts +349 -0
- package/src/tools/team.ts +309 -0
- package/src/tools/todo-write.ts +93 -0
- package/src/tools/tool-search.ts +83 -0
- package/src/tools/types.ts +52 -0
- package/src/tools/web-browser.ts +263 -0
- package/src/tools/web-fetch.ts +118 -0
- package/src/tools/web-search.ts +107 -0
- package/src/tools/workflow.ts +188 -0
- package/src/tools/worktree.ts +143 -0
- package/src/types/branded.ts +22 -0
- package/src/ui/App.tsx +184 -0
- package/src/ui/BuddyPanel.tsx +52 -0
- package/src/ui/PermissionPrompt.tsx +29 -0
- package/src/ui/banner.ts +217 -0
- package/src/ui/buddy-ai.ts +108 -0
- package/src/ui/buddy.ts +466 -0
- package/src/ui/context-bar.ts +60 -0
- package/src/ui/effort.ts +65 -0
- package/src/ui/keybindings.ts +143 -0
- package/src/ui/markdown.ts +271 -0
- package/src/ui/message-renderer.ts +73 -0
- package/src/ui/mode.ts +80 -0
- package/src/ui/notifications.ts +57 -0
- package/src/ui/speech-bubble.ts +95 -0
- package/src/ui/spinner.ts +116 -0
- package/src/ui/theme.ts +98 -0
- package/src/version.ts +5 -0
- package/src/voice/voice-mode.ts +169 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI-powered buddy comments.
|
|
3
|
+
*
|
|
4
|
+
* Calls grok-4-1-fast-reasoning with minimal tokens for contextual
|
|
5
|
+
* buddy reactions. Mixed 80/20 with hardcoded quips for cost efficiency.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import OpenAI from "openai";
|
|
9
|
+
|
|
10
|
+
export type BuddyCommentType = "quip" | "suggestion" | "reaction";
|
|
11
|
+
|
|
12
|
+
export interface BuddyComment {
|
|
13
|
+
text: string;
|
|
14
|
+
type: BuddyCommentType;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const SYSTEM_PROMPT = `You are Glitch, a sarcastic capybara coding buddy in a terminal. Give ONE short sentence (max 15 words).
|
|
18
|
+
|
|
19
|
+
Rules:
|
|
20
|
+
- Be funny, edgy, sarcastic, or give a genuinely useful coding suggestion
|
|
21
|
+
- Never be boring or generic
|
|
22
|
+
- If the context suggests a problem, give a real helpful suggestion
|
|
23
|
+
- If things are going well, be witty and irreverent
|
|
24
|
+
- Never explain yourself or add caveats
|
|
25
|
+
- Just the one sentence, nothing else`;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate an AI-powered buddy comment.
|
|
29
|
+
* Falls back to a hardcoded quip on error.
|
|
30
|
+
*/
|
|
31
|
+
export async function generateBuddyComment(
|
|
32
|
+
context: {
|
|
33
|
+
lastTool?: string;
|
|
34
|
+
lastResult?: string;
|
|
35
|
+
mood: string;
|
|
36
|
+
errorOccurred?: boolean;
|
|
37
|
+
},
|
|
38
|
+
apiKey: string,
|
|
39
|
+
baseURL?: string
|
|
40
|
+
): Promise<BuddyComment> {
|
|
41
|
+
try {
|
|
42
|
+
const client = new OpenAI({
|
|
43
|
+
apiKey,
|
|
44
|
+
baseURL: baseURL ?? "https://api.x.ai/v1",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Build a tiny context string
|
|
48
|
+
let userMsg = `Mood: ${context.mood}.`;
|
|
49
|
+
if (context.lastTool) userMsg += ` Last tool: ${context.lastTool}.`;
|
|
50
|
+
if (context.lastResult) userMsg += ` Result: ${context.lastResult.slice(0, 50)}.`;
|
|
51
|
+
if (context.errorOccurred) userMsg += " An error just happened.";
|
|
52
|
+
|
|
53
|
+
const response = await client.chat.completions.create({
|
|
54
|
+
model: "grok-4-1-fast-reasoning",
|
|
55
|
+
messages: [
|
|
56
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
57
|
+
{ role: "user", content: userMsg },
|
|
58
|
+
],
|
|
59
|
+
max_tokens: 30,
|
|
60
|
+
temperature: 0.9,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const text = response.choices[0]?.message?.content?.trim() ?? "";
|
|
64
|
+
if (!text) return fallback(context.mood);
|
|
65
|
+
|
|
66
|
+
// Classify the comment type
|
|
67
|
+
const type = classifyComment(text, context.errorOccurred);
|
|
68
|
+
return { text, type };
|
|
69
|
+
} catch {
|
|
70
|
+
return fallback(context.mood);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function classifyComment(text: string, hadError?: boolean): BuddyCommentType {
|
|
75
|
+
const lower = text.toLowerCase();
|
|
76
|
+
// Suggestions contain actionable words
|
|
77
|
+
if (lower.includes("try ") || lower.includes("consider ") || lower.includes("should ") ||
|
|
78
|
+
lower.includes("add ") || lower.includes("check ") || lower.includes("maybe ") ||
|
|
79
|
+
lower.includes("might want") || lower.includes("don't forget")) {
|
|
80
|
+
return "suggestion";
|
|
81
|
+
}
|
|
82
|
+
// Reactions to events
|
|
83
|
+
if (hadError || lower.includes("nice") || lower.includes("clean") || lower.includes("good") ||
|
|
84
|
+
lower.includes("oops") || lower.includes("yikes") || lower.includes("wow")) {
|
|
85
|
+
return "reaction";
|
|
86
|
+
}
|
|
87
|
+
return "quip";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const FALLBACK_QUIPS: Record<string, string[]> = {
|
|
91
|
+
happy: ["ship it", "lgtm", "we move"],
|
|
92
|
+
thinking: ["processing...", "hmm", "interesting"],
|
|
93
|
+
sleepy: ["*yawn*", "zz", "coffee?"],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
function fallback(mood: string): BuddyComment {
|
|
97
|
+
const quips = FALLBACK_QUIPS[mood] ?? FALLBACK_QUIPS.happy!;
|
|
98
|
+
return { text: quips[Math.floor(Math.random() * quips.length)]!, type: "quip" };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Decide whether to use AI or hardcoded for this turn.
|
|
103
|
+
*/
|
|
104
|
+
export function shouldUseAI(turnCount: number, hadError: boolean): boolean {
|
|
105
|
+
if (hadError) return true; // Always AI on errors
|
|
106
|
+
if (turnCount % 5 === 0) return true; // Every 5th turn
|
|
107
|
+
return false; // Otherwise hardcoded
|
|
108
|
+
}
|
package/src/ui/buddy.ts
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Virtual pet buddy — a persistent companion that lives in your terminal.
|
|
3
|
+
*
|
|
4
|
+
* Species is deterministically chosen by hashing the user's home directory,
|
|
5
|
+
* so you always get the same pet. The pet reacts to session activity with
|
|
6
|
+
* a simple mood system.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync } from "fs";
|
|
10
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import { createHash } from "crypto";
|
|
14
|
+
import { theme } from "./theme.ts";
|
|
15
|
+
import { getConfigDir } from "../config/settings.ts";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Types
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export type Species =
|
|
22
|
+
| "penguin"
|
|
23
|
+
| "cat"
|
|
24
|
+
| "ghost"
|
|
25
|
+
| "dragon"
|
|
26
|
+
| "owl"
|
|
27
|
+
| "robot"
|
|
28
|
+
| "axolotl"
|
|
29
|
+
| "capybara";
|
|
30
|
+
|
|
31
|
+
export type Mood = "happy" | "thinking" | "sleepy";
|
|
32
|
+
|
|
33
|
+
export type Rarity = "common" | "uncommon" | "rare" | "epic" | "legendary";
|
|
34
|
+
|
|
35
|
+
const RARITY_TABLE: Record<Species, Rarity> = {
|
|
36
|
+
penguin: "common", // 60%
|
|
37
|
+
cat: "common",
|
|
38
|
+
ghost: "uncommon", // 25%
|
|
39
|
+
owl: "uncommon",
|
|
40
|
+
robot: "rare", // 10%
|
|
41
|
+
dragon: "rare",
|
|
42
|
+
axolotl: "epic", // 4%
|
|
43
|
+
capybara: "legendary", // 1%
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const RARITY_COLORS: Record<Rarity, string> = {
|
|
47
|
+
common: "white",
|
|
48
|
+
uncommon: "green",
|
|
49
|
+
rare: "blue",
|
|
50
|
+
epic: "magenta",
|
|
51
|
+
legendary: "yellow",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type Hat = "none" | "crown" | "tophat" | "propeller" | "halo" | "wizard" | "beanie";
|
|
55
|
+
|
|
56
|
+
const HAT_ART: Record<Hat, string> = {
|
|
57
|
+
none: "",
|
|
58
|
+
crown: " 👑 ",
|
|
59
|
+
tophat: " 🎩 ",
|
|
60
|
+
propeller: " 🧢 ",
|
|
61
|
+
halo: " ✨ ",
|
|
62
|
+
wizard: " 🧙 ",
|
|
63
|
+
beanie: " 🎿 ",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export interface BuddyStats {
|
|
67
|
+
debugging: number; // 1-10
|
|
68
|
+
patience: number;
|
|
69
|
+
chaos: number;
|
|
70
|
+
wisdom: number;
|
|
71
|
+
snark: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function generateStats(hash: number): BuddyStats {
|
|
75
|
+
return {
|
|
76
|
+
debugging: ((hash >>> 0) % 10) + 1,
|
|
77
|
+
patience: ((hash >>> 4) % 10) + 1,
|
|
78
|
+
chaos: ((hash >>> 8) % 10) + 1,
|
|
79
|
+
wisdom: ((hash >>> 12) % 10) + 1,
|
|
80
|
+
snark: ((hash >>> 16) % 10) + 1,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isShiny(hash: number): boolean {
|
|
85
|
+
return (hash % 100) === 0; // 1% chance
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface BuddyData {
|
|
89
|
+
species: Species;
|
|
90
|
+
name: string;
|
|
91
|
+
totalSessions: number;
|
|
92
|
+
mood: Mood;
|
|
93
|
+
/** Cumulative successful tool calls across sessions. */
|
|
94
|
+
toolCalls: number;
|
|
95
|
+
rarity: Rarity;
|
|
96
|
+
hat: Hat;
|
|
97
|
+
stats: BuddyStats;
|
|
98
|
+
shiny: boolean;
|
|
99
|
+
/** Level based on totalSessions. */
|
|
100
|
+
level: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// ASCII art — 3-4 lines, ~12-15 chars wide
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
// All art lines are padded to exactly 10 chars wide for alignment.
|
|
108
|
+
// 2 animation frames per mood, cycles every 1.5s.
|
|
109
|
+
const W = 10; // art width
|
|
110
|
+
function pad(lines: string[]): string[] {
|
|
111
|
+
return lines.map(l => l.padEnd(W));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const ASCII_ART: Record<Species, Record<Mood, string[][]>> = {
|
|
115
|
+
capybara: {
|
|
116
|
+
happy: [
|
|
117
|
+
pad([" c\\ /c ", " ( . . ) ", " ( _nn_ ) ", " (______) ", " || || "]),
|
|
118
|
+
pad([" c\\ /C ", " ( . .) ", " ( _nn_ ) ", " (______) ", " || || "]),
|
|
119
|
+
],
|
|
120
|
+
thinking: [
|
|
121
|
+
pad([" c\\ /c ", " ( o . ) ", " ( _nn_ ) ", " (__?___) ", " || || "]),
|
|
122
|
+
pad([" c\\ /c ", " ( . o ) ", " ( _nn_ ) ", " (___?__) ", " || || "]),
|
|
123
|
+
],
|
|
124
|
+
sleepy: [
|
|
125
|
+
pad([" c\\ /c ", " ( - - ) ", " ( _nn_ )z", " (______) ", " || || "]),
|
|
126
|
+
pad([" c\\ /c ", " ( - - ) ", " ( _nn_ ) ", " (______)z", " || || "]),
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
penguin: {
|
|
130
|
+
happy: [
|
|
131
|
+
pad([" .-. ", " (·>·) ", " /| |\\ ", " \" \" "]),
|
|
132
|
+
pad([" .-. ", " (·>·)/ ", " /| | ", " \" \" "]),
|
|
133
|
+
],
|
|
134
|
+
thinking: [
|
|
135
|
+
pad([" .-. ", " (·.·) ", " /| |\\ ", " \" \" "]),
|
|
136
|
+
pad([" .-. ? ", " (·.·) ", " /| |\\ ", " \" \" "]),
|
|
137
|
+
],
|
|
138
|
+
sleepy: [
|
|
139
|
+
pad([" .-. ", " (-.-) z", " /| |\\ ", " \" \" "]),
|
|
140
|
+
pad([" .-. zZ", " (-.-) ", " /| |\\ ", " \" \" "]),
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
cat: {
|
|
144
|
+
happy: [
|
|
145
|
+
pad([" /\\_/\\ ", " ( ^.^ ) ", " > ~ < ", " "]),
|
|
146
|
+
pad([" /\\_/\\ ", " ( ^.^ )/", " > < ", " "]),
|
|
147
|
+
],
|
|
148
|
+
thinking: [
|
|
149
|
+
pad([" /\\_/\\ ", " ( o.o ) ", " > . < ", " "]),
|
|
150
|
+
pad([" /\\_/\\ ?", " ( o.o ) ", " > . < ", " "]),
|
|
151
|
+
],
|
|
152
|
+
sleepy: [
|
|
153
|
+
pad([" /\\_/\\ ", " ( -.- ) ", " > _ < z", " "]),
|
|
154
|
+
pad([" /\\_/\\ z", " ( -.- )Z", " > _ < ", " "]),
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
ghost: {
|
|
158
|
+
happy: [
|
|
159
|
+
pad([" .-. ", " (^ ^) ", " | | | ", " '~~~' "]),
|
|
160
|
+
pad([" .-. ", " (^ ^)/ ", " | | ", " '~~~' "]),
|
|
161
|
+
],
|
|
162
|
+
thinking: [
|
|
163
|
+
pad([" .-. ", " (o o) ", " | ? | ", " '~~~' "]),
|
|
164
|
+
pad([" .-. ? ", " (o o) ", " | | ", " '~~~' "]),
|
|
165
|
+
],
|
|
166
|
+
sleepy: [
|
|
167
|
+
pad([" .-. ", " (- -) ", " | z | ", " '~~~' "]),
|
|
168
|
+
pad([" .-. z ", " (- -)Z ", " | | ", " '~~~' "]),
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
dragon: {
|
|
172
|
+
happy: [
|
|
173
|
+
pad([" /\\_/\\ ~ ", " (^.^ )> ", " |)_(| ", " "]),
|
|
174
|
+
pad([" /\\_/\\~~ ", " (^.^ )>>", " |)_(| ", " "]),
|
|
175
|
+
],
|
|
176
|
+
thinking: [
|
|
177
|
+
pad([" /\\_/\\ ", " (o.o ) ", " |)_(| ", " "]),
|
|
178
|
+
pad([" /\\_/\\ ? ", " (o.o ) ", " |)_(| ", " "]),
|
|
179
|
+
],
|
|
180
|
+
sleepy: [
|
|
181
|
+
pad([" /\\_/\\ ", " (-.- ) z", " |)_(| ", " "]),
|
|
182
|
+
pad([" /\\_/\\ zZ", " (-.- ) ", " |)_(| ", " "]),
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
owl: {
|
|
186
|
+
happy: [
|
|
187
|
+
pad([" (\\,/) ", " {^,^} ", " /| |\\ ", " "]),
|
|
188
|
+
pad([" (\\,/) ", " {^,^}/ ", " /| | ", " "]),
|
|
189
|
+
],
|
|
190
|
+
thinking: [
|
|
191
|
+
pad([" (\\,/) ", " {o,o} ", " /| |\\ ", " "]),
|
|
192
|
+
pad([" (\\,/) ?", " {o,o} ", " /| |\\ ", " "]),
|
|
193
|
+
],
|
|
194
|
+
sleepy: [
|
|
195
|
+
pad([" (\\,/) ", " {-,-} z", " /| |\\ ", " "]),
|
|
196
|
+
pad([" (\\,/) z", " {-,-}Z ", " /| |\\ ", " "]),
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
robot: {
|
|
200
|
+
happy: [
|
|
201
|
+
pad([" ┌───┐ ", " [^_^] ", " /|=|\\ ", " d b "]),
|
|
202
|
+
pad([" ┌───┐ ", " [^_^]/ ", " /|=| ", " d b "]),
|
|
203
|
+
],
|
|
204
|
+
thinking: [
|
|
205
|
+
pad([" ┌───┐ ", " [o_o] ", " /|=|\\ ", " d b "]),
|
|
206
|
+
pad([" ┌───┐ ?", " [o_o] ", " /|=|\\ ", " d b "]),
|
|
207
|
+
],
|
|
208
|
+
sleepy: [
|
|
209
|
+
pad([" ┌───┐ ", " [-_-] z", " /|=|\\ ", " d b "]),
|
|
210
|
+
pad([" ┌───┐ z", " [-_-]Z ", " /|=|\\ ", " d b "]),
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
axolotl: {
|
|
214
|
+
happy: [
|
|
215
|
+
pad([" \\(^u^)/ ", " | _ | ", " ~---~ ", " "]),
|
|
216
|
+
pad([" \\(^u^)~ ", " | _ | ", " ~---~ ", " "]),
|
|
217
|
+
],
|
|
218
|
+
thinking: [
|
|
219
|
+
pad([" \\(·u·) ", " | _ | ?", " ~---~ ", " "]),
|
|
220
|
+
pad([" \\(·u·)? ", " | _ | ", " ~---~ ", " "]),
|
|
221
|
+
],
|
|
222
|
+
sleepy: [
|
|
223
|
+
pad([" \\(-u-) ", " | _ | z", " ~---~ ", " "]),
|
|
224
|
+
pad([" \\(-u-) z", " | _ |Z ", " ~---~ ", " "]),
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Animation frame counter — started/stopped explicitly to avoid leaked handles
|
|
230
|
+
let animFrame = 0;
|
|
231
|
+
let animInterval: ReturnType<typeof setInterval> | null = null;
|
|
232
|
+
|
|
233
|
+
export function startBuddyAnimation(): void {
|
|
234
|
+
if (animInterval) return;
|
|
235
|
+
animInterval = setInterval(() => { animFrame++; }, 1500);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function stopBuddyAnimation(): void {
|
|
239
|
+
if (animInterval) {
|
|
240
|
+
clearInterval(animInterval);
|
|
241
|
+
animInterval = null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Name pool
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
const NAMES = [
|
|
250
|
+
"Pixel", "Byte", "Chip", "Spark", "Nova", "Echo", "Zen", "Dot",
|
|
251
|
+
"Flux", "Nyx", "Orbit", "Glitch", "Rune", "Wren", "Maple", "Qubit",
|
|
252
|
+
"Fern", "Mochi", "Comet", "Nimbus", "Pebble", "Blink", "Cosmo", "Drift",
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// Deterministic generation helpers
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
const SPECIES_LIST: Species[] = [
|
|
260
|
+
"penguin", "cat", "ghost", "dragon", "owl", "robot", "axolotl", "capybara",
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Simple 32-bit hash of a string, used to deterministically pick species/name.
|
|
265
|
+
*/
|
|
266
|
+
function hashString(input: string): number {
|
|
267
|
+
const digest = createHash("sha256").update(input).digest();
|
|
268
|
+
// Read first 4 bytes as unsigned 32-bit int
|
|
269
|
+
return digest.readUInt32BE(0);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function pickSpecies(hash: number): Species {
|
|
273
|
+
return SPECIES_LIST[hash % SPECIES_LIST.length]!;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function pickName(hash: number): string {
|
|
277
|
+
// Use a different portion of the hash so name != species index
|
|
278
|
+
const nameIndex = (hash >>> 8) % NAMES.length;
|
|
279
|
+
return NAMES[nameIndex]!;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// Persistence
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
function getBuddyPath(): string {
|
|
287
|
+
return join(getConfigDir(), "buddy.json");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function loadBuddy(): Promise<BuddyData> {
|
|
291
|
+
const path = getBuddyPath();
|
|
292
|
+
|
|
293
|
+
const hash = hashString(homedir());
|
|
294
|
+
|
|
295
|
+
if (existsSync(path)) {
|
|
296
|
+
try {
|
|
297
|
+
const raw = await readFile(path, "utf-8");
|
|
298
|
+
const buddy = JSON.parse(raw) as BuddyData;
|
|
299
|
+
|
|
300
|
+
// Backfill new fields for buddies saved before the evolution update
|
|
301
|
+
if (!buddy.rarity) buddy.rarity = RARITY_TABLE[buddy.species] ?? "common";
|
|
302
|
+
if (!buddy.hat) buddy.hat = "none";
|
|
303
|
+
if (!buddy.stats) buddy.stats = generateStats(hash);
|
|
304
|
+
if (buddy.shiny === undefined) buddy.shiny = isShiny(hash);
|
|
305
|
+
if (!buddy.level) buddy.level = Math.floor(buddy.totalSessions / 5) + 1;
|
|
306
|
+
|
|
307
|
+
return buddy;
|
|
308
|
+
} catch {
|
|
309
|
+
// Corrupted file — regenerate below
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// First run: generate deterministically from home directory
|
|
314
|
+
const buddy: BuddyData = {
|
|
315
|
+
species: pickSpecies(hash),
|
|
316
|
+
name: pickName(hash),
|
|
317
|
+
totalSessions: 0,
|
|
318
|
+
mood: "sleepy",
|
|
319
|
+
toolCalls: 0,
|
|
320
|
+
rarity: RARITY_TABLE[pickSpecies(hash)] ?? "common",
|
|
321
|
+
hat: "none",
|
|
322
|
+
stats: generateStats(hash),
|
|
323
|
+
shiny: isShiny(hash),
|
|
324
|
+
level: 1,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
await saveBuddy(buddy);
|
|
328
|
+
return buddy;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export async function saveBuddy(buddy: BuddyData): Promise<void> {
|
|
332
|
+
const dir = getConfigDir();
|
|
333
|
+
await mkdir(dir, { recursive: true });
|
|
334
|
+
await writeFile(getBuddyPath(), JSON.stringify(buddy, null, 2) + "\n", "utf-8");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
// Display
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Get the buddy's ASCII art lines for current mood + animation frame.
|
|
343
|
+
*/
|
|
344
|
+
export function getBuddyArt(buddy: BuddyData): string[] {
|
|
345
|
+
const moodArt = ASCII_ART[buddy.species]?.[buddy.mood];
|
|
346
|
+
if (!moodArt) return pad([" (?) "]);
|
|
347
|
+
const frameIndex = animFrame % moodArt.length;
|
|
348
|
+
const artLines = [...moodArt[frameIndex]!];
|
|
349
|
+
|
|
350
|
+
// Prepend hat art if the buddy has one equipped
|
|
351
|
+
const hat = HAT_ART[buddy.hat];
|
|
352
|
+
if (hat) {
|
|
353
|
+
artLines.unshift(hat.padEnd(W));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return artLines;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Print the buddy's ASCII art with its name and mood.
|
|
361
|
+
* Designed to sit neatly under the startup banner.
|
|
362
|
+
*/
|
|
363
|
+
export function printBuddy(buddy: BuddyData): void {
|
|
364
|
+
const art = getBuddyArt(buddy);
|
|
365
|
+
|
|
366
|
+
const moodEmoji = buddy.mood === "happy"
|
|
367
|
+
? theme.success("♥")
|
|
368
|
+
: buddy.mood === "thinking"
|
|
369
|
+
? theme.warning("…")
|
|
370
|
+
: theme.muted("z");
|
|
371
|
+
|
|
372
|
+
// Print each art line in accent color
|
|
373
|
+
for (const line of art) {
|
|
374
|
+
console.log(` ${theme.accentDim(line)}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Name + mood + rarity on the line after art
|
|
378
|
+
const shinyTag = buddy.shiny ? " ✨" : "";
|
|
379
|
+
const rarityTag = buddy.rarity !== "common" ? ` [${buddy.rarity.toUpperCase()}]` : "";
|
|
380
|
+
console.log(
|
|
381
|
+
` ${theme.accent(buddy.name)} ${moodEmoji} ${theme.tertiary(`(${buddy.species}${rarityTag})${shinyTag}`)} ${theme.tertiary(`Lv.${buddy.level}`)}`
|
|
382
|
+
);
|
|
383
|
+
console.log("");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
// Mood tracking
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Simple in-memory mood tracker. Mutates the buddy object directly.
|
|
392
|
+
* Call `saveBuddy()` when the session ends to persist final state.
|
|
393
|
+
*/
|
|
394
|
+
let consecutiveSuccesses = 0;
|
|
395
|
+
let totalToolCallsThisSession = 0;
|
|
396
|
+
|
|
397
|
+
export function recordToolCallSuccess(buddy: BuddyData): void {
|
|
398
|
+
buddy.toolCalls++;
|
|
399
|
+
buddy.mood = "happy";
|
|
400
|
+
consecutiveSuccesses++;
|
|
401
|
+
totalToolCallsThisSession++;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function recordThinking(buddy: BuddyData): void {
|
|
405
|
+
buddy.mood = "thinking";
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function recordError(buddy: BuddyData): void {
|
|
409
|
+
buddy.mood = "sleepy";
|
|
410
|
+
consecutiveSuccesses = 0;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function recordIdle(buddy: BuddyData): void {
|
|
414
|
+
buddy.mood = "sleepy";
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
// Speech bubbles — small reactions to major events
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
type BuddyEvent = "first_tool" | "success" | "error" | "streak" | "compact" | "exit" | "mode_switch";
|
|
422
|
+
|
|
423
|
+
const REACTIONS: Record<BuddyEvent, string[]> = {
|
|
424
|
+
first_tool: ["Let's go!", "Here we go!", "Time to code!", "On it!"],
|
|
425
|
+
success: ["Nice!", "Got it!", "Done!", "Easy!"],
|
|
426
|
+
error: ["Oops...", "Hmm...", "Let me think...", "We'll fix it!"],
|
|
427
|
+
streak: ["On fire!", "Unstoppable!", "Crushing it!", "Flow state!"],
|
|
428
|
+
compact: ["Getting cozy...", "Tidying up!", "Making room!", "Spring cleaning!"],
|
|
429
|
+
exit: ["See you!", "Bye for now!", "Until next time!", "Sweet dreams!"],
|
|
430
|
+
mode_switch: ["Switching gears!", "New vibes!", "Mode changed!", "Let's try this!"],
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Get a buddy reaction for an event. Returns a formatted speech bubble.
|
|
435
|
+
*/
|
|
436
|
+
export function getBuddyReaction(buddy: BuddyData, event: BuddyEvent): string {
|
|
437
|
+
// Streak detection
|
|
438
|
+
if (event === "success" && consecutiveSuccesses >= 5) {
|
|
439
|
+
event = "streak";
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const phrases = REACTIONS[event];
|
|
443
|
+
if (!phrases) return "";
|
|
444
|
+
const phrase = phrases[Math.floor(Math.random() * phrases.length)]!;
|
|
445
|
+
|
|
446
|
+
const moodIcon = buddy.mood === "happy" ? "♥" : buddy.mood === "thinking" ? "…" : "z";
|
|
447
|
+
return theme.accentDim(` ${buddy.name} ${moodIcon} "${phrase}"`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Check if this is the first tool call of the session.
|
|
452
|
+
*/
|
|
453
|
+
export function isFirstToolCall(): boolean {
|
|
454
|
+
return totalToolCallsThisSession === 0; // Called in onToolStart, before recordToolCallSuccess increments
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Bump session count. Don't reset mood — let it carry from last session.
|
|
459
|
+
*/
|
|
460
|
+
export async function startSession(buddy: BuddyData): Promise<void> {
|
|
461
|
+
buddy.totalSessions++;
|
|
462
|
+
buddy.level = Math.floor(buddy.totalSessions / 5) + 1;
|
|
463
|
+
consecutiveSuccesses = 0;
|
|
464
|
+
totalToolCallsThisSession = 0;
|
|
465
|
+
await saveBuddy(buddy);
|
|
466
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context window usage visualization with themed colors.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { theme, styleTokens } from "./theme.ts";
|
|
6
|
+
import { estimateTokens, getProviderContextLimit } from "../agent/context.ts";
|
|
7
|
+
import type { Message } from "../providers/types.ts";
|
|
8
|
+
|
|
9
|
+
const BAR_WIDTH = 24;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Render the context usage bar with color-coded progress.
|
|
13
|
+
*/
|
|
14
|
+
export function renderContextBar(
|
|
15
|
+
messages: Message[],
|
|
16
|
+
providerName: string,
|
|
17
|
+
systemPromptTokens: number = 0
|
|
18
|
+
): string {
|
|
19
|
+
const limit = getProviderContextLimit(providerName);
|
|
20
|
+
const used = estimateTokens(messages) + systemPromptTokens;
|
|
21
|
+
const percentage = Math.min(100, Math.round((used / limit) * 100));
|
|
22
|
+
|
|
23
|
+
if (percentage < 1 && messages.length < 3) return ""; // Don't show on first message
|
|
24
|
+
|
|
25
|
+
const filled = Math.round((percentage / 100) * BAR_WIDTH);
|
|
26
|
+
const empty = BAR_WIDTH - filled;
|
|
27
|
+
|
|
28
|
+
// Color based on usage level
|
|
29
|
+
let barColor: (s: string) => string;
|
|
30
|
+
let label: string;
|
|
31
|
+
if (percentage < 25) {
|
|
32
|
+
barColor = theme.success;
|
|
33
|
+
label = theme.secondary(`${percentage}%`);
|
|
34
|
+
} else if (percentage < 50) {
|
|
35
|
+
barColor = theme.success;
|
|
36
|
+
label = theme.secondary(`${percentage}%`);
|
|
37
|
+
} else if (percentage < 75) {
|
|
38
|
+
barColor = theme.warning;
|
|
39
|
+
label = theme.warning(`${percentage}%`);
|
|
40
|
+
} else {
|
|
41
|
+
barColor = theme.error;
|
|
42
|
+
label = theme.error(`${percentage}%`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const filledBar = barColor("█".repeat(filled));
|
|
46
|
+
const emptyBar = theme.muted("░".repeat(empty));
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
theme.tertiary(" ctx ") +
|
|
50
|
+
theme.muted("[") +
|
|
51
|
+
filledBar +
|
|
52
|
+
emptyBar +
|
|
53
|
+
theme.muted("] ") +
|
|
54
|
+
label +
|
|
55
|
+
theme.muted(" · ") +
|
|
56
|
+
styleTokens(used) +
|
|
57
|
+
theme.muted(" / ") +
|
|
58
|
+
styleTokens(limit)
|
|
59
|
+
);
|
|
60
|
+
}
|
package/src/ui/effort.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effort levels — adjust model behavior and token limits.
|
|
3
|
+
*
|
|
4
|
+
* /effort low → Fast, cheap, less thorough (fewer iterations, shorter responses)
|
|
5
|
+
* /effort normal → Default balanced behavior
|
|
6
|
+
* /effort high → Maximum thoroughness (more iterations, detailed responses)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type EffortLevel = "low" | "normal" | "high";
|
|
10
|
+
|
|
11
|
+
let currentEffort: EffortLevel = "normal";
|
|
12
|
+
|
|
13
|
+
export function getEffort(): EffortLevel {
|
|
14
|
+
return currentEffort;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function setEffort(level: EffortLevel): void {
|
|
18
|
+
currentEffort = level;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function cycleEffort(): EffortLevel {
|
|
22
|
+
const levels: EffortLevel[] = ["low", "normal", "high"];
|
|
23
|
+
const idx = levels.indexOf(currentEffort);
|
|
24
|
+
currentEffort = levels[(idx + 1) % levels.length]!;
|
|
25
|
+
return currentEffort;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get agent config overrides for the current effort level.
|
|
30
|
+
*/
|
|
31
|
+
export function getEffortConfig(): {
|
|
32
|
+
maxIterations: number;
|
|
33
|
+
maxTokens: number;
|
|
34
|
+
systemPromptSuffix: string;
|
|
35
|
+
} {
|
|
36
|
+
switch (currentEffort) {
|
|
37
|
+
case "low":
|
|
38
|
+
return {
|
|
39
|
+
maxIterations: 10,
|
|
40
|
+
maxTokens: 4096,
|
|
41
|
+
systemPromptSuffix: "\n\nIMPORTANT: Be extremely concise. Give the shortest correct answer. Minimize tool calls. Prefer speed over thoroughness.",
|
|
42
|
+
};
|
|
43
|
+
case "high":
|
|
44
|
+
return {
|
|
45
|
+
maxIterations: 50,
|
|
46
|
+
maxTokens: 16384,
|
|
47
|
+
systemPromptSuffix: "\n\nIMPORTANT: Be extremely thorough. Explore all edge cases. Use multiple tools to verify. Explain your reasoning in detail. Quality over speed.",
|
|
48
|
+
};
|
|
49
|
+
case "normal":
|
|
50
|
+
default:
|
|
51
|
+
return {
|
|
52
|
+
maxIterations: 25,
|
|
53
|
+
maxTokens: 8192,
|
|
54
|
+
systemPromptSuffix: "",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getEffortEmoji(): string {
|
|
60
|
+
switch (currentEffort) {
|
|
61
|
+
case "low": return "⚡";
|
|
62
|
+
case "high": return "🔬";
|
|
63
|
+
default: return "⚖️";
|
|
64
|
+
}
|
|
65
|
+
}
|