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
package/src/cli.ts
ADDED
|
@@ -0,0 +1,1120 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AshlrCode (ac) — Multi-provider AI coding agent CLI.
|
|
5
|
+
*
|
|
6
|
+
* Entry point: sets up providers, tools, sessions, plan mode, context
|
|
7
|
+
* management, and runs the interactive REPL.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFile } from "fs/promises";
|
|
11
|
+
import { resolve, join } from "path";
|
|
12
|
+
import { existsSync } from "fs";
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
import { createInterface } from "readline";
|
|
15
|
+
|
|
16
|
+
import { ProviderRouter } from "./providers/router.ts";
|
|
17
|
+
import { ToolRegistry } from "./tools/registry.ts";
|
|
18
|
+
import { runAgentLoop } from "./agent/loop.ts";
|
|
19
|
+
import { loadSettings } from "./config/settings.ts";
|
|
20
|
+
import { initRemoteSettings, startPolling, loadCachedSettings, stopPolling } from "./config/remote-settings.ts";
|
|
21
|
+
import { Session, listSessions, resumeSession, getLastSessionForCwd, forkSession } from "./persistence/session.ts";
|
|
22
|
+
import {
|
|
23
|
+
needsCompaction,
|
|
24
|
+
autoCompact,
|
|
25
|
+
snipCompact,
|
|
26
|
+
estimateTokens,
|
|
27
|
+
getProviderContextLimit,
|
|
28
|
+
} from "./agent/context.ts";
|
|
29
|
+
import {
|
|
30
|
+
isPlanMode,
|
|
31
|
+
getPlanModePrompt,
|
|
32
|
+
getPlanState,
|
|
33
|
+
exitPlanMode,
|
|
34
|
+
} from "./planning/plan-mode.ts";
|
|
35
|
+
import type { Message } from "./providers/types.ts";
|
|
36
|
+
import type { ToolContext } from "./tools/types.ts";
|
|
37
|
+
|
|
38
|
+
// Tools
|
|
39
|
+
import { bashTool } from "./tools/bash.ts";
|
|
40
|
+
import { fileReadTool } from "./tools/file-read.ts";
|
|
41
|
+
import { fileWriteTool } from "./tools/file-write.ts";
|
|
42
|
+
import { fileEditTool } from "./tools/file-edit.ts";
|
|
43
|
+
import { globTool } from "./tools/glob.ts";
|
|
44
|
+
import { grepTool } from "./tools/grep.ts";
|
|
45
|
+
import { askUserTool } from "./tools/ask-user.ts";
|
|
46
|
+
import { webFetchTool } from "./tools/web-fetch.ts";
|
|
47
|
+
import { enterPlanTool, exitPlanTool, planWriteTool } from "./planning/plan-tools.ts";
|
|
48
|
+
import { agentTool, initAgentTool } from "./tools/agent.ts";
|
|
49
|
+
import { taskCreateTool, taskUpdateTool, taskListTool, taskGetTool } from "./tools/tasks.ts";
|
|
50
|
+
import { loadMemories, formatMemoriesForPrompt } from "./persistence/memory.ts";
|
|
51
|
+
import {
|
|
52
|
+
loadPermissions,
|
|
53
|
+
checkPermission,
|
|
54
|
+
recordPermission,
|
|
55
|
+
allowForSession,
|
|
56
|
+
setBypassMode,
|
|
57
|
+
setAutoAcceptEdits,
|
|
58
|
+
isBypassMode,
|
|
59
|
+
setRules,
|
|
60
|
+
} from "./config/permissions.ts";
|
|
61
|
+
import { Spinner, getToolPhrase } from "./ui/spinner.ts";
|
|
62
|
+
import { renderMarkdownDelta, flushMarkdown, resetMarkdown } from "./ui/markdown.ts";
|
|
63
|
+
import { printBanner, printTurnSeparator, printInputLine, printStatusLine } from "./ui/banner.ts";
|
|
64
|
+
import { getCurrentMode, setMode, cycleMode, getPromptForMode, type Mode } from "./ui/mode.ts";
|
|
65
|
+
import { renderContextBar } from "./ui/context-bar.ts";
|
|
66
|
+
import { loadBuddy, printBuddy, saveBuddy, startSession, recordToolCallSuccess, recordThinking, recordError, getBuddyReaction, isFirstToolCall, type BuddyData } from "./ui/buddy.ts";
|
|
67
|
+
import { theme, styleCost, styleTokens } from "./ui/theme.ts";
|
|
68
|
+
import { lsTool } from "./tools/ls.ts";
|
|
69
|
+
import { configTool } from "./tools/config.ts";
|
|
70
|
+
import { enterWorktreeTool, exitWorktreeTool } from "./tools/worktree.ts";
|
|
71
|
+
import { webSearchTool } from "./tools/web-search.ts";
|
|
72
|
+
import { toolSearchTool, initToolSearch } from "./tools/tool-search.ts";
|
|
73
|
+
import { powershellTool } from "./tools/powershell.ts";
|
|
74
|
+
import { getGitContext, formatGitPrompt } from "./config/git.ts";
|
|
75
|
+
import { loadHooksFromSettings } from "./config/hooks.ts";
|
|
76
|
+
import { fileHistory } from "./state/file-history.ts";
|
|
77
|
+
import { memorySaveTool, memoryListTool, memoryDeleteTool } from "./tools/memory.ts";
|
|
78
|
+
import { notebookEditTool } from "./tools/notebook-edit.ts";
|
|
79
|
+
import { sendMessageTool } from "./tools/send-message.ts";
|
|
80
|
+
import { sleepTool } from "./tools/sleep.ts";
|
|
81
|
+
import { todoWriteTool } from "./tools/todo-write.ts";
|
|
82
|
+
import { diffTool } from "./tools/diff.ts";
|
|
83
|
+
import { snipTool, initSnipTool } from "./tools/snip.ts";
|
|
84
|
+
import { lspTool, shutdownLSP } from "./tools/lsp.ts";
|
|
85
|
+
import { webBrowserTool, shutdownBrowser } from "./tools/web-browser.ts";
|
|
86
|
+
import { feature } from "./config/features.ts";
|
|
87
|
+
import { teamCreateTool, teamDeleteTool, teamListTool, teamDispatchTool, initTeamTools } from "./tools/team.ts";
|
|
88
|
+
import { workflowTool } from "./tools/workflow.ts";
|
|
89
|
+
import { listPeersTool } from "./tools/peers.ts";
|
|
90
|
+
import { MCPManager } from "./mcp/manager.ts";
|
|
91
|
+
import { createMCPTool } from "./tools/mcp-tool.ts";
|
|
92
|
+
import { listMcpResourcesTool, setMCPManager } from "./tools/mcp-resources.ts";
|
|
93
|
+
import { initTasks } from "./tools/tasks.ts";
|
|
94
|
+
import { loadSkills } from "./skills/loader.ts";
|
|
95
|
+
import { SkillRegistry } from "./skills/registry.ts";
|
|
96
|
+
import { categorizeError } from "./agent/error-handler.ts";
|
|
97
|
+
import { buildSystemPrompt } from "./agent/system-prompt.ts";
|
|
98
|
+
import { runSetupWizard, needsSetup } from "./setup.ts";
|
|
99
|
+
import { loadProjectConfig } from "./config/project-config.ts";
|
|
100
|
+
import { startInkRepl } from "./repl.tsx";
|
|
101
|
+
import { VERSION } from "./version.ts";
|
|
102
|
+
let maxCostUSD = Infinity;
|
|
103
|
+
|
|
104
|
+
interface AppState {
|
|
105
|
+
router: ProviderRouter;
|
|
106
|
+
registry: ToolRegistry;
|
|
107
|
+
toolContext: ToolContext;
|
|
108
|
+
session: Session;
|
|
109
|
+
history: Message[];
|
|
110
|
+
baseSystemPrompt: string;
|
|
111
|
+
skillRegistry: SkillRegistry;
|
|
112
|
+
buddy: BuddyData;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function main() {
|
|
116
|
+
const args = process.argv.slice(2);
|
|
117
|
+
|
|
118
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
119
|
+
console.log(`AshlrCode v${VERSION}`);
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
124
|
+
printHelp();
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Load settings and permissions
|
|
129
|
+
let settings = await loadSettings();
|
|
130
|
+
await loadPermissions();
|
|
131
|
+
|
|
132
|
+
// Wire up permission rules from settings
|
|
133
|
+
if (settings.permissionRules) {
|
|
134
|
+
setRules(settings.permissionRules);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Initialize remote managed settings
|
|
138
|
+
const remoteUrl = process.env.AC_REMOTE_SETTINGS_URL ?? settings.remoteSettingsUrl;
|
|
139
|
+
if (remoteUrl) {
|
|
140
|
+
initRemoteSettings(remoteUrl, settings.providers.primary.apiKey);
|
|
141
|
+
await loadCachedSettings();
|
|
142
|
+
startPolling();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Parse mode flags
|
|
146
|
+
const dangerouslySkipPermissions = args.includes("--dangerously-skip-permissions") || args.includes("--yolo");
|
|
147
|
+
const autoAcceptEditsFlag = args.includes("--auto-accept-edits");
|
|
148
|
+
const printMode = args.includes("--print");
|
|
149
|
+
const maxCostArg = getArg(args, "--max-cost");
|
|
150
|
+
maxCostUSD = maxCostArg ? parseFloat(maxCostArg) : Infinity;
|
|
151
|
+
|
|
152
|
+
if (dangerouslySkipPermissions) {
|
|
153
|
+
setBypassMode(true);
|
|
154
|
+
}
|
|
155
|
+
if (autoAcceptEditsFlag) {
|
|
156
|
+
setAutoAcceptEdits(true);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (needsSetup(settings)) {
|
|
160
|
+
const newSettings = await runSetupWizard();
|
|
161
|
+
settings = newSettings;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Initialize provider router
|
|
165
|
+
const router = new ProviderRouter(settings.providers);
|
|
166
|
+
|
|
167
|
+
// Initialize tool registry
|
|
168
|
+
const registry = new ToolRegistry();
|
|
169
|
+
registry.register(bashTool);
|
|
170
|
+
registry.register(fileReadTool);
|
|
171
|
+
registry.register(fileWriteTool);
|
|
172
|
+
registry.register(fileEditTool);
|
|
173
|
+
registry.register(globTool);
|
|
174
|
+
registry.register(grepTool);
|
|
175
|
+
registry.register(askUserTool);
|
|
176
|
+
registry.register(webFetchTool);
|
|
177
|
+
registry.register(enterPlanTool);
|
|
178
|
+
registry.register(exitPlanTool);
|
|
179
|
+
registry.register(planWriteTool);
|
|
180
|
+
registry.register(agentTool);
|
|
181
|
+
registry.register(taskCreateTool);
|
|
182
|
+
registry.register(taskUpdateTool);
|
|
183
|
+
registry.register(taskListTool);
|
|
184
|
+
registry.register(taskGetTool);
|
|
185
|
+
registry.register(lsTool);
|
|
186
|
+
registry.register(configTool);
|
|
187
|
+
registry.register(enterWorktreeTool);
|
|
188
|
+
registry.register(exitWorktreeTool);
|
|
189
|
+
registry.register(webSearchTool);
|
|
190
|
+
registry.register(toolSearchTool);
|
|
191
|
+
registry.register(memorySaveTool);
|
|
192
|
+
registry.register(memoryListTool);
|
|
193
|
+
registry.register(memoryDeleteTool);
|
|
194
|
+
registry.register(notebookEditTool);
|
|
195
|
+
registry.register(sendMessageTool);
|
|
196
|
+
registry.register(sleepTool);
|
|
197
|
+
registry.register(todoWriteTool);
|
|
198
|
+
registry.register(diffTool);
|
|
199
|
+
registry.register(snipTool);
|
|
200
|
+
registry.register(lspTool);
|
|
201
|
+
registry.register(teamCreateTool);
|
|
202
|
+
registry.register(teamDeleteTool);
|
|
203
|
+
registry.register(teamListTool);
|
|
204
|
+
registry.register(teamDispatchTool);
|
|
205
|
+
registry.register(workflowTool);
|
|
206
|
+
registry.register(listPeersTool);
|
|
207
|
+
if (process.platform === "win32") {
|
|
208
|
+
registry.register(powershellTool);
|
|
209
|
+
}
|
|
210
|
+
if (feature("BROWSER_TOOL")) {
|
|
211
|
+
registry.register(webBrowserTool);
|
|
212
|
+
}
|
|
213
|
+
initToolSearch(registry);
|
|
214
|
+
|
|
215
|
+
// Set up hooks from settings — toolHooks (new format) takes priority, falls back to hooks (legacy)
|
|
216
|
+
if (settings.toolHooks) {
|
|
217
|
+
const hooksConfig = loadHooksFromSettings(settings.toolHooks);
|
|
218
|
+
registry.setHooks(hooksConfig);
|
|
219
|
+
} else if (settings.hooks) {
|
|
220
|
+
registry.setHooks(settings.hooks);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Connect MCP servers in background (don't block startup)
|
|
224
|
+
const mcpManager = new MCPManager();
|
|
225
|
+
if (settings.mcpServers && Object.keys(settings.mcpServers).length > 0) {
|
|
226
|
+
setMCPManager(mcpManager);
|
|
227
|
+
registry.register(listMcpResourcesTool);
|
|
228
|
+
mcpManager.connectAll(settings.mcpServers).then(() => {
|
|
229
|
+
for (const { serverName, tool } of mcpManager.getAllTools()) {
|
|
230
|
+
registry.register(createMCPTool(serverName, tool, mcpManager));
|
|
231
|
+
}
|
|
232
|
+
}).catch(() => {});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Load system prompt via builder (knowledge files, memory, git context)
|
|
236
|
+
const rawSystemPrompt = await loadSystemPrompt();
|
|
237
|
+
const cwd = process.cwd();
|
|
238
|
+
|
|
239
|
+
// Build prompt using SystemPromptBuilder — but skip addToolDescriptions()
|
|
240
|
+
// because tools are already sent via the API's `tools` parameter.
|
|
241
|
+
const { SystemPromptBuilder } = await import("./agent/system-prompt.ts");
|
|
242
|
+
const promptBuilder = new SystemPromptBuilder();
|
|
243
|
+
promptBuilder.addCoreInstructions(rawSystemPrompt);
|
|
244
|
+
promptBuilder.addPermissionContext(getCurrentMode());
|
|
245
|
+
|
|
246
|
+
if (isPlanMode()) {
|
|
247
|
+
promptBuilder.addPlanMode();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
await promptBuilder.addKnowledgeFiles(cwd);
|
|
251
|
+
await promptBuilder.addMemoryFiles();
|
|
252
|
+
|
|
253
|
+
// Append legacy memories (from persistence/memory.ts) for backward compat
|
|
254
|
+
const memories = await loadMemories(cwd);
|
|
255
|
+
if (memories.length > 0) {
|
|
256
|
+
promptBuilder.addSection("legacy-memories", formatMemoriesForPrompt(memories), 45);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Git context via builder method (richer than legacy formatGitPrompt)
|
|
260
|
+
await promptBuilder.addGitContext(cwd);
|
|
261
|
+
|
|
262
|
+
const assembled = promptBuilder.build(8000);
|
|
263
|
+
let baseSystemPrompt = assembled.text;
|
|
264
|
+
|
|
265
|
+
// Initialize agent tool with router/registry references
|
|
266
|
+
initAgentTool(router, registry, baseSystemPrompt);
|
|
267
|
+
initTeamTools(router, registry, baseSystemPrompt);
|
|
268
|
+
|
|
269
|
+
// Tool context
|
|
270
|
+
const toolContext: ToolContext = {
|
|
271
|
+
cwd,
|
|
272
|
+
requestPermission: async (tool, description) => {
|
|
273
|
+
return await askPermission(tool, description);
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// Session handling
|
|
278
|
+
let session: Session;
|
|
279
|
+
let history: Message[] = [];
|
|
280
|
+
|
|
281
|
+
const resumeId = getArg(args, "--resume");
|
|
282
|
+
const forkId = getArg(args, "--fork-session");
|
|
283
|
+
const continueFlag = args.includes("--continue") || args.includes("-c");
|
|
284
|
+
|
|
285
|
+
if (resumeId) {
|
|
286
|
+
const resumed = await resumeSession(resumeId);
|
|
287
|
+
if (resumed) {
|
|
288
|
+
session = resumed.session;
|
|
289
|
+
history = resumed.messages;
|
|
290
|
+
console.log(chalk.dim(`Resumed session ${resumeId} (${history.length} messages)`));
|
|
291
|
+
} else {
|
|
292
|
+
console.error(chalk.red(`Session ${resumeId} not found`));
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
} else if (continueFlag) {
|
|
296
|
+
const lastId = await getLastSessionForCwd(cwd);
|
|
297
|
+
if (lastId) {
|
|
298
|
+
const resumed = await resumeSession(lastId);
|
|
299
|
+
if (resumed) {
|
|
300
|
+
session = resumed.session;
|
|
301
|
+
history = resumed.messages;
|
|
302
|
+
console.log(chalk.dim(`Continued session ${lastId} (${history.length} messages)`));
|
|
303
|
+
} else {
|
|
304
|
+
session = new Session();
|
|
305
|
+
await session.init(router.currentProvider.name, router.currentProvider.config.model);
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
session = new Session();
|
|
309
|
+
await session.init(router.currentProvider.name, router.currentProvider.config.model);
|
|
310
|
+
}
|
|
311
|
+
} else if (forkId) {
|
|
312
|
+
const forked = await forkSession(forkId, router.currentProvider.name, router.currentProvider.config.model);
|
|
313
|
+
if (forked) {
|
|
314
|
+
session = forked.session;
|
|
315
|
+
history = forked.messages;
|
|
316
|
+
console.log(chalk.dim(`Forked session ${forkId} → ${session.id} (${history.length} messages)`));
|
|
317
|
+
} else {
|
|
318
|
+
console.error(chalk.red(`Session ${forkId} not found`));
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
session = new Session();
|
|
323
|
+
await session.init(router.currentProvider.name, router.currentProvider.config.model);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Initialize task persistence
|
|
327
|
+
await initTasks(session.id);
|
|
328
|
+
|
|
329
|
+
// Load skills
|
|
330
|
+
const skillRegistry = new SkillRegistry();
|
|
331
|
+
const skills = await loadSkills(cwd);
|
|
332
|
+
skillRegistry.registerAll(skills);
|
|
333
|
+
// Skills loaded silently — use /skills to list them
|
|
334
|
+
|
|
335
|
+
// Load buddy (don't reset mood — let it carry from last session)
|
|
336
|
+
const buddy = await loadBuddy();
|
|
337
|
+
await startSession(buddy);
|
|
338
|
+
|
|
339
|
+
const state: AppState = {
|
|
340
|
+
router,
|
|
341
|
+
registry,
|
|
342
|
+
toolContext,
|
|
343
|
+
session,
|
|
344
|
+
history,
|
|
345
|
+
baseSystemPrompt,
|
|
346
|
+
skillRegistry,
|
|
347
|
+
buddy,
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// Wire SnipTool with history accessors
|
|
351
|
+
initSnipTool(
|
|
352
|
+
() => state.history,
|
|
353
|
+
(msgs) => { state.history.length = 0; state.history.push(...msgs); },
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// Set initial mode based on flags
|
|
357
|
+
if (dangerouslySkipPermissions) setMode("yolo");
|
|
358
|
+
else if (autoAcceptEditsFlag) setMode("accept-edits");
|
|
359
|
+
|
|
360
|
+
// Header (suppress in print mode)
|
|
361
|
+
if (!printMode) {
|
|
362
|
+
const startMode = dangerouslySkipPermissions ? "yolo" : autoAcceptEditsFlag ? "accept-edits" : undefined;
|
|
363
|
+
printBanner(VERSION, router.currentProvider.name, router.currentProvider.config.model, startMode);
|
|
364
|
+
printBuddy(buddy);
|
|
365
|
+
console.log(theme.tertiary(` ${cwd}`));
|
|
366
|
+
if (buddy.totalSessions <= 1) {
|
|
367
|
+
// First-time quick-start
|
|
368
|
+
console.log(theme.accent("\n Welcome! Here are some things to try:"));
|
|
369
|
+
console.log(theme.secondary(` "fix the login bug"`) + theme.tertiary(` — describe any task`));
|
|
370
|
+
console.log(theme.secondary(` /explore`) + theme.tertiary(` — analyze this codebase`));
|
|
371
|
+
console.log(theme.secondary(` /commit`) + theme.tertiary(` — commit your changes`));
|
|
372
|
+
console.log(theme.secondary(` /buddy`) + theme.tertiary(` — meet ${buddy.name}!`));
|
|
373
|
+
console.log(theme.tertiary(`\n Shift+Tab switches modes. /help for all commands.\n`));
|
|
374
|
+
} else {
|
|
375
|
+
console.log(theme.tertiary(` Shift+Tab to switch modes. /help for commands. Ctrl+C to exit.\n`));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Graceful Ctrl+C — only for non-interactive paths (--print, single-shot)
|
|
380
|
+
// In Ink mode, repl.tsx handleExit() manages cleanup + process.exit
|
|
381
|
+
// Only register SIGINT for non-Ink paths. Ink handles its own exit.
|
|
382
|
+
const isNonInteractive = printMode || args.some(a => !a.startsWith("-") && !a.startsWith("--"));
|
|
383
|
+
if (isNonInteractive) process.on("SIGINT", async () => {
|
|
384
|
+
try {
|
|
385
|
+
if (state.history.length > 0) {
|
|
386
|
+
await state.session.appendMessages(state.history.slice(-2));
|
|
387
|
+
}
|
|
388
|
+
state.buddy.mood = "sleepy";
|
|
389
|
+
await saveBuddy(state.buddy);
|
|
390
|
+
} catch {}
|
|
391
|
+
if (!printMode) {
|
|
392
|
+
console.log(chalk.dim(`\n${router.getCostSummary()}`));
|
|
393
|
+
}
|
|
394
|
+
mcpManager.disconnectAll().catch(() => {});
|
|
395
|
+
shutdownLSP().catch(() => {});
|
|
396
|
+
shutdownBrowser().catch(() => {});
|
|
397
|
+
stopPolling();
|
|
398
|
+
process.exit(0);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Check for inline command
|
|
402
|
+
const inlineMessage = args
|
|
403
|
+
.filter((a) => !a.startsWith("-") && !a.startsWith("--"))
|
|
404
|
+
.join(" ");
|
|
405
|
+
|
|
406
|
+
if (inlineMessage) {
|
|
407
|
+
await runTurn(inlineMessage, state, printMode);
|
|
408
|
+
if (!printMode) {
|
|
409
|
+
console.log(chalk.dim(`\n${router.getCostSummary()}`));
|
|
410
|
+
}
|
|
411
|
+
process.exit(0);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Interactive REPL — use Ink for proper cursor positioning
|
|
415
|
+
startInkRepl(state, maxCostUSD);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function getPrompt(): string {
|
|
419
|
+
return getPromptForMode();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function handleCommand(
|
|
423
|
+
input: string,
|
|
424
|
+
state: AppState,
|
|
425
|
+
rl: ReturnType<typeof createInterface>
|
|
426
|
+
): Promise<void> {
|
|
427
|
+
const [cmd, ...rest] = input.split(" ");
|
|
428
|
+
const arg = rest.join(" ").trim();
|
|
429
|
+
|
|
430
|
+
switch (cmd) {
|
|
431
|
+
case "/quit":
|
|
432
|
+
case "/exit":
|
|
433
|
+
case "/q":
|
|
434
|
+
console.log(chalk.dim(state.router.getCostSummary()));
|
|
435
|
+
process.exit(0);
|
|
436
|
+
|
|
437
|
+
case "/cost":
|
|
438
|
+
console.log(theme.secondary(state.router.getCostSummary()));
|
|
439
|
+
console.log(
|
|
440
|
+
theme.tertiary(
|
|
441
|
+
`Context: ~${estimateTokens(state.history).toLocaleString()} tokens, ${state.history.length} messages`
|
|
442
|
+
)
|
|
443
|
+
);
|
|
444
|
+
// Cost nudge
|
|
445
|
+
if (state.router.costs.totalCostUSD > 5) {
|
|
446
|
+
console.log(theme.warning(`\n 💡 Expensive session ($${state.router.costs.totalCostUSD.toFixed(2)}). Consider:`));
|
|
447
|
+
console.log(theme.tertiary(` /model grok-3 — 80% cheaper for exploration`));
|
|
448
|
+
console.log(theme.tertiary(` /compact — reduce context size`));
|
|
449
|
+
console.log(theme.tertiary(` /effort fast — shorter responses`));
|
|
450
|
+
} else if (state.router.costs.totalCostUSD > 1) {
|
|
451
|
+
console.log(theme.tertiary(`\n 💡 Tip: /model grok-3 for cheaper exploration tasks`));
|
|
452
|
+
}
|
|
453
|
+
break;
|
|
454
|
+
|
|
455
|
+
case "/clear":
|
|
456
|
+
state.history.length = 0;
|
|
457
|
+
if (isPlanMode()) exitPlanMode();
|
|
458
|
+
console.log(chalk.dim("Conversation cleared."));
|
|
459
|
+
break;
|
|
460
|
+
|
|
461
|
+
case "/plan":
|
|
462
|
+
if (isPlanMode()) {
|
|
463
|
+
const planState = getPlanState();
|
|
464
|
+
console.log(chalk.magenta("Plan mode is active."));
|
|
465
|
+
console.log(chalk.dim(`Plan file: ${planState.planFilePath}`));
|
|
466
|
+
console.log(chalk.dim(`Started: ${planState.startedAt}`));
|
|
467
|
+
} else {
|
|
468
|
+
console.log(
|
|
469
|
+
chalk.dim(
|
|
470
|
+
"Plan mode is not active. The model can enter plan mode by calling EnterPlan."
|
|
471
|
+
)
|
|
472
|
+
);
|
|
473
|
+
console.log(
|
|
474
|
+
chalk.dim(
|
|
475
|
+
'Tip: Ask the model to "plan first" and it will use plan mode.'
|
|
476
|
+
)
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
break;
|
|
480
|
+
|
|
481
|
+
case "/sessions": {
|
|
482
|
+
const sessions = await listSessions();
|
|
483
|
+
if (sessions.length === 0) {
|
|
484
|
+
console.log(chalk.dim("No saved sessions."));
|
|
485
|
+
} else {
|
|
486
|
+
console.log(chalk.bold("Recent sessions:"));
|
|
487
|
+
for (const s of sessions) {
|
|
488
|
+
const age = timeSince(new Date(s.updatedAt));
|
|
489
|
+
const title = s.title ?? s.cwd.split("/").pop() ?? s.id;
|
|
490
|
+
const current = s.id === state.session.id ? chalk.cyan(" (current)") : "";
|
|
491
|
+
console.log(
|
|
492
|
+
` ${chalk.bold(s.id)}${current} — ${title} (${s.messageCount} msgs, ${age} ago)`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
console.log(chalk.dim("\nResume with: ac --resume <id>"));
|
|
496
|
+
}
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
case "/model":
|
|
501
|
+
if (arg) {
|
|
502
|
+
// Model switching
|
|
503
|
+
const models: Record<string, string> = {
|
|
504
|
+
"grok-fast": "grok-4-1-fast-reasoning",
|
|
505
|
+
"grok-4": "grok-4-0314",
|
|
506
|
+
"grok-3": "grok-3-fast",
|
|
507
|
+
"sonnet": "claude-sonnet-4-6-20250514",
|
|
508
|
+
"opus": "claude-opus-4-6-20250514",
|
|
509
|
+
"haiku": "claude-haiku-4-5-20251001",
|
|
510
|
+
};
|
|
511
|
+
const resolved = models[arg] ?? arg;
|
|
512
|
+
state.router.currentProvider.config.model = resolved;
|
|
513
|
+
console.log(chalk.dim(`Switched to model: ${resolved}`));
|
|
514
|
+
} else {
|
|
515
|
+
console.log(chalk.bold("Current:"));
|
|
516
|
+
console.log(chalk.dim(` Provider: ${state.router.currentProvider.name}`));
|
|
517
|
+
console.log(chalk.dim(` Model: ${state.router.currentProvider.config.model}`));
|
|
518
|
+
console.log(chalk.bold("\nAliases:"));
|
|
519
|
+
console.log(chalk.dim(" grok-fast → grok-4-1-fast-reasoning"));
|
|
520
|
+
console.log(chalk.dim(" grok-4 → grok-4-0314"));
|
|
521
|
+
console.log(chalk.dim(" grok-3 → grok-3-fast"));
|
|
522
|
+
console.log(chalk.dim(" sonnet → claude-sonnet-4-6-20250514"));
|
|
523
|
+
console.log(chalk.dim(" opus → claude-opus-4-6-20250514"));
|
|
524
|
+
console.log(chalk.dim("\nUsage: /model <alias or model-id>"));
|
|
525
|
+
}
|
|
526
|
+
break;
|
|
527
|
+
|
|
528
|
+
case "/compact": {
|
|
529
|
+
const before = estimateTokens(state.history);
|
|
530
|
+
state.history = snipCompact(state.history);
|
|
531
|
+
state.history = await autoCompact(state.history, state.router);
|
|
532
|
+
const after = estimateTokens(state.history);
|
|
533
|
+
console.log(
|
|
534
|
+
chalk.dim(
|
|
535
|
+
`Compacted: ${before.toLocaleString()} → ${after.toLocaleString()} tokens`
|
|
536
|
+
)
|
|
537
|
+
);
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
case "/history": {
|
|
542
|
+
if (state.history.length === 0) {
|
|
543
|
+
console.log(chalk.dim("No messages yet."));
|
|
544
|
+
} else {
|
|
545
|
+
let turnNum = 0;
|
|
546
|
+
for (const msg of state.history) {
|
|
547
|
+
if (msg.role === "user" && typeof msg.content === "string") {
|
|
548
|
+
turnNum++;
|
|
549
|
+
const preview = msg.content.length > 80 ? msg.content.slice(0, 77) + "..." : msg.content;
|
|
550
|
+
console.log(chalk.cyan(` ${turnNum}. `) + preview);
|
|
551
|
+
} else if (msg.role === "assistant" && typeof msg.content === "string") {
|
|
552
|
+
const preview = msg.content.length > 80 ? msg.content.slice(0, 77) + "..." : msg.content;
|
|
553
|
+
console.log(chalk.dim(` → ${preview}`));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
case "/undo": {
|
|
561
|
+
// Remove last user + assistant turn
|
|
562
|
+
if (state.history.length < 2) {
|
|
563
|
+
console.log(chalk.dim("Nothing to undo."));
|
|
564
|
+
} else {
|
|
565
|
+
// Find and remove the last user message and everything after
|
|
566
|
+
let lastUserIdx = -1;
|
|
567
|
+
for (let i = state.history.length - 1; i >= 0; i--) {
|
|
568
|
+
if (state.history[i]!.role === "user") {
|
|
569
|
+
lastUserIdx = i;
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (lastUserIdx >= 0) {
|
|
574
|
+
const removed = state.history.length - lastUserIdx;
|
|
575
|
+
state.history.splice(lastUserIdx);
|
|
576
|
+
console.log(chalk.dim(`Undid last turn (removed ${removed} messages).`));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
case "/restore": {
|
|
583
|
+
if (!arg) {
|
|
584
|
+
const snapshots = fileHistory.getSnapshotFiles();
|
|
585
|
+
if (snapshots.length === 0) {
|
|
586
|
+
console.log(chalk.dim("No file snapshots available."));
|
|
587
|
+
} else {
|
|
588
|
+
console.log(chalk.bold("Files with snapshots:"));
|
|
589
|
+
for (const s of snapshots) {
|
|
590
|
+
console.log(chalk.dim(` ${s.path} (${s.count} snapshot(s))`));
|
|
591
|
+
}
|
|
592
|
+
console.log(chalk.dim("\nUsage: /restore <file-path>"));
|
|
593
|
+
}
|
|
594
|
+
} else {
|
|
595
|
+
const restored = await fileHistory.restore(arg);
|
|
596
|
+
if (restored) {
|
|
597
|
+
console.log(chalk.green(`Restored: ${arg}`));
|
|
598
|
+
} else {
|
|
599
|
+
console.log(chalk.red(`No snapshot found for: ${arg}`));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
case "/memory": {
|
|
606
|
+
const mems = await loadMemories(process.cwd());
|
|
607
|
+
if (mems.length === 0) {
|
|
608
|
+
console.log(chalk.dim("No memories for this project. The model can save memories using MemorySave."));
|
|
609
|
+
} else {
|
|
610
|
+
console.log(chalk.bold(`${mems.length} project memories:`));
|
|
611
|
+
for (const m of mems) {
|
|
612
|
+
console.log(chalk.dim(` ${chalk.bold(m.name)} (${m.type}): ${m.description || m.content.slice(0, 60)}`));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
case "/skills": {
|
|
619
|
+
const allSkills = state.skillRegistry.getAll();
|
|
620
|
+
if (allSkills.length === 0) {
|
|
621
|
+
console.log(chalk.dim("No skills loaded. Add .md files to ~/.ashlrcode/skills/"));
|
|
622
|
+
} else {
|
|
623
|
+
console.log(chalk.bold(`${allSkills.length} skills:`));
|
|
624
|
+
for (const s of allSkills) {
|
|
625
|
+
console.log(chalk.dim(` ${chalk.bold(s.trigger)} — ${s.description}`));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
case "/tools": {
|
|
632
|
+
const tools = state.registry.getAll();
|
|
633
|
+
console.log(chalk.bold(`${tools.length} tools registered:`));
|
|
634
|
+
for (const tool of tools) {
|
|
635
|
+
const flags = [
|
|
636
|
+
tool.isReadOnly() ? chalk.green("read-only") : chalk.yellow("write"),
|
|
637
|
+
tool.isConcurrencySafe() ? "parallel" : "serial",
|
|
638
|
+
].join(", ");
|
|
639
|
+
console.log(chalk.dim(` ${chalk.bold(tool.name)} (${flags})`));
|
|
640
|
+
}
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
case "/diff": {
|
|
645
|
+
const proc = Bun.spawn(["git", "diff", "--stat"], {
|
|
646
|
+
cwd: process.cwd(),
|
|
647
|
+
stdout: "pipe",
|
|
648
|
+
stderr: "pipe",
|
|
649
|
+
});
|
|
650
|
+
const stdout = await new Response(proc.stdout).text();
|
|
651
|
+
if (stdout.trim()) {
|
|
652
|
+
console.log(stdout);
|
|
653
|
+
} else {
|
|
654
|
+
console.log(chalk.dim("No uncommitted changes."));
|
|
655
|
+
}
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
case "/git": {
|
|
660
|
+
const gitInfo = await getGitContext(process.cwd());
|
|
661
|
+
if (!gitInfo.isRepo) {
|
|
662
|
+
console.log(chalk.dim("Not a git repository."));
|
|
663
|
+
} else {
|
|
664
|
+
console.log(chalk.bold("Git:"));
|
|
665
|
+
console.log(chalk.dim(` Branch: ${gitInfo.branch}`));
|
|
666
|
+
console.log(chalk.dim(` Remote: ${gitInfo.remoteUrl ?? "none"}`));
|
|
667
|
+
const changes = gitInfo.status?.split("\n").filter(Boolean).length ?? 0;
|
|
668
|
+
console.log(chalk.dim(` Changes: ${changes > 0 ? changes : "clean"}`));
|
|
669
|
+
}
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
case "/buddy": {
|
|
674
|
+
if (arg === "name" || arg?.startsWith("name ")) {
|
|
675
|
+
const newName = arg.replace("name", "").trim();
|
|
676
|
+
if (newName) {
|
|
677
|
+
state.buddy.name = newName;
|
|
678
|
+
await saveBuddy(state.buddy);
|
|
679
|
+
console.log(theme.success(` Buddy renamed to ${newName}!`));
|
|
680
|
+
} else {
|
|
681
|
+
console.log(theme.tertiary(" Usage: /buddy name <new-name>"));
|
|
682
|
+
}
|
|
683
|
+
} else {
|
|
684
|
+
printBuddy(state.buddy);
|
|
685
|
+
const b = state.buddy;
|
|
686
|
+
const shinyStr = b.shiny ? " ✨ SHINY" : "";
|
|
687
|
+
console.log(theme.primary(` ${b.name} the ${b.species}${shinyStr}`));
|
|
688
|
+
console.log(theme.primary(` Rarity: ${b.rarity.toUpperCase()} · Level ${b.level} · Hat: ${b.hat}`));
|
|
689
|
+
console.log(theme.primary(` Stats: 🐛${b.stats.debugging} 🧘${b.stats.patience} 🌀${b.stats.chaos} 🦉${b.stats.wisdom} 😏${b.stats.snark}`));
|
|
690
|
+
console.log(theme.primary(` Mood: ${b.mood}`));
|
|
691
|
+
console.log(theme.tertiary(` Sessions: ${b.totalSessions} · Tool calls: ${b.toolCalls}`));
|
|
692
|
+
console.log(theme.tertiary(`\n Rename: /buddy name <new-name>`));
|
|
693
|
+
}
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
case "/effort": {
|
|
698
|
+
const levels = ["fast", "balanced", "thorough"];
|
|
699
|
+
if (!arg) {
|
|
700
|
+
console.log(theme.primary("Effort: " + (state.router.currentProvider.config.maxTokens === 4096 ? "fast" : state.router.currentProvider.config.maxTokens === 16384 ? "thorough" : "balanced")));
|
|
701
|
+
console.log(theme.tertiary(" fast — shorter responses, fewer tokens"));
|
|
702
|
+
console.log(theme.tertiary(" balanced — default behavior"));
|
|
703
|
+
console.log(theme.tertiary(" thorough — deeper analysis, more tokens"));
|
|
704
|
+
console.log(theme.tertiary(" Usage: /effort <level>"));
|
|
705
|
+
} else if (levels.includes(arg)) {
|
|
706
|
+
const tokenMap: Record<string, number> = { fast: 4096, balanced: 8192, thorough: 16384 };
|
|
707
|
+
state.router.currentProvider.config.maxTokens = tokenMap[arg]!;
|
|
708
|
+
console.log(theme.success(` Effort set to: ${arg} (${tokenMap[arg]} max tokens)`));
|
|
709
|
+
} else {
|
|
710
|
+
console.log(theme.error(` Unknown effort level. Choose: ${levels.join(", ")}`));
|
|
711
|
+
}
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
case "/btw": {
|
|
716
|
+
if (!arg) {
|
|
717
|
+
console.log(theme.tertiary(" Ask a quick side question: /btw <question>"));
|
|
718
|
+
} else {
|
|
719
|
+
await runTurn(`[Side question — answer briefly, don't change the main task] ${arg}`, state);
|
|
720
|
+
}
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
case "/status": {
|
|
725
|
+
const taskList = await import("./tools/tasks.ts");
|
|
726
|
+
console.log(theme.primary("Session: ") + theme.accent(state.session.id));
|
|
727
|
+
console.log(theme.primary("Provider: ") + theme.accent(state.router.currentProvider.name + ":" + state.router.currentProvider.config.model));
|
|
728
|
+
console.log(theme.primary("Messages: ") + theme.tokens(String(state.history.length)));
|
|
729
|
+
console.log(theme.primary("Cost: ") + styleCost(state.router.costs.totalCostUSD));
|
|
730
|
+
const ctxLimit = getProviderContextLimit(state.router.currentProvider.name);
|
|
731
|
+
const ctxUsed = estimateTokens(state.history);
|
|
732
|
+
console.log(theme.primary("Context: ") + styleTokens(ctxUsed) + theme.tertiary(" / ") + styleTokens(ctxLimit) + theme.tertiary(` (${Math.round((ctxUsed / ctxLimit) * 100)}%)`));
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
case "/help":
|
|
737
|
+
printCommands();
|
|
738
|
+
break;
|
|
739
|
+
|
|
740
|
+
default:
|
|
741
|
+
console.log(theme.tertiary(`Unknown command: ${cmd}. Type /help for available commands.`));
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async function runTurn(input: string, state: AppState, printMode = false): Promise<void> {
|
|
746
|
+
const spinner = printMode ? null : new Spinner("Thinking");
|
|
747
|
+
let firstTextReceived = false;
|
|
748
|
+
|
|
749
|
+
try {
|
|
750
|
+
// Ultrathink: if user includes "ultrathink" in message, use max tokens for this turn
|
|
751
|
+
const isUltrathink = input.toLowerCase().includes("ultrathink");
|
|
752
|
+
const savedMaxTokens = state.router.currentProvider.config.maxTokens;
|
|
753
|
+
if (isUltrathink) {
|
|
754
|
+
state.router.currentProvider.config.maxTokens = 32768;
|
|
755
|
+
if (!printMode) console.log(theme.accent(" ⚡ Ultrathink mode — deep reasoning enabled\n"));
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Check cost budget
|
|
759
|
+
if (maxCostUSD < Infinity && state.router.costs.totalCostUSD >= maxCostUSD) {
|
|
760
|
+
console.error(chalk.yellow(`\n Cost limit reached ($${state.router.costs.totalCostUSD.toFixed(4)} >= $${maxCostUSD}). Use --max-cost to increase.`));
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Build system prompt (base + plan mode if active)
|
|
765
|
+
const systemPrompt =
|
|
766
|
+
state.baseSystemPrompt + getPlanModePrompt();
|
|
767
|
+
|
|
768
|
+
// Check if context needs compaction before this turn
|
|
769
|
+
const systemTokens = Math.ceil(systemPrompt.length / 4);
|
|
770
|
+
const contextLimit = getProviderContextLimit(state.router.currentProvider.name);
|
|
771
|
+
|
|
772
|
+
// Warn at 50% and 75% of context limit
|
|
773
|
+
const currentTokens = estimateTokens(state.history) + systemTokens;
|
|
774
|
+
if (!printMode && currentTokens > contextLimit * 0.85) {
|
|
775
|
+
console.log(theme.error(` ⚠ Context at ${Math.round((currentTokens / contextLimit) * 100)}% — approaching limit!`));
|
|
776
|
+
console.log(theme.tertiary(` 💡 Run /compact to shrink context, or start fresh with ac --continue`));
|
|
777
|
+
} else if (!printMode && currentTokens > contextLimit * 0.75) {
|
|
778
|
+
console.log(theme.warning(` ⚠ Context at ${Math.round((currentTokens / contextLimit) * 100)}% of ${contextLimit.toLocaleString()} token limit`));
|
|
779
|
+
} else if (!printMode && currentTokens > contextLimit * 0.5) {
|
|
780
|
+
console.log(theme.tertiary(` Context at ${Math.round((currentTokens / contextLimit) * 100)}% of limit`));
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (needsCompaction(state.history, systemTokens, { maxContextTokens: contextLimit })) {
|
|
784
|
+
if (!printMode) console.log(chalk.dim(" [compacting context...]"));
|
|
785
|
+
state.history = snipCompact(state.history);
|
|
786
|
+
state.history = await autoCompact(state.history, state.router);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Start spinner (not in print mode)
|
|
790
|
+
spinner?.start();
|
|
791
|
+
resetMarkdown();
|
|
792
|
+
|
|
793
|
+
// Auto-title session from first message
|
|
794
|
+
if (state.history.length === 0) {
|
|
795
|
+
const title = input.length > 60 ? input.slice(0, 57) + "..." : input;
|
|
796
|
+
await state.session.setTitle(title);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Capture message count AFTER compaction (not before)
|
|
800
|
+
const preTurnMessageCount = state.history.length;
|
|
801
|
+
|
|
802
|
+
// Echo user input as a styled message (so it stays visible during output)
|
|
803
|
+
if (!printMode) {
|
|
804
|
+
console.log("\n" + theme.accent(" ❯ ") + theme.primary(input.length > 100 ? input.slice(0, 97) + "..." : input));
|
|
805
|
+
console.log("");
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const result = await runAgentLoop(input, state.history, {
|
|
809
|
+
systemPrompt,
|
|
810
|
+
router: state.router,
|
|
811
|
+
toolRegistry: state.registry,
|
|
812
|
+
toolContext: state.toolContext,
|
|
813
|
+
readOnly: isPlanMode(),
|
|
814
|
+
onText: (text) => {
|
|
815
|
+
if (!firstTextReceived) {
|
|
816
|
+
spinner?.stop();
|
|
817
|
+
firstTextReceived = true;
|
|
818
|
+
if (!printMode) console.log(""); // breathing room before response
|
|
819
|
+
}
|
|
820
|
+
if (printMode) {
|
|
821
|
+
process.stdout.write(text);
|
|
822
|
+
} else {
|
|
823
|
+
const rendered = renderMarkdownDelta(text);
|
|
824
|
+
process.stdout.write(rendered);
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
onToolStart: (name, toolInput) => {
|
|
828
|
+
if (printMode) return;
|
|
829
|
+
spinner?.stop();
|
|
830
|
+
firstTextReceived = false;
|
|
831
|
+
recordThinking(state.buddy);
|
|
832
|
+
const icon = isPlanMode() ? theme.plan("◆") : theme.toolIcon("◆");
|
|
833
|
+
const preview = formatToolPreview(name, toolInput);
|
|
834
|
+
console.log(`\n ${icon} ${theme.toolName(name)}`);
|
|
835
|
+
console.log(theme.tertiary(` ${preview}`));
|
|
836
|
+
if (isFirstToolCall()) {
|
|
837
|
+
console.log(getBuddyReaction(state.buddy, "first_tool"));
|
|
838
|
+
}
|
|
839
|
+
spinner?.start(getToolPhrase(name));
|
|
840
|
+
},
|
|
841
|
+
onToolEnd: (_name, result, isError) => {
|
|
842
|
+
if (printMode) return;
|
|
843
|
+
spinner?.stop();
|
|
844
|
+
if (isError) {
|
|
845
|
+
recordError(state.buddy);
|
|
846
|
+
} else {
|
|
847
|
+
recordToolCallSuccess(state.buddy);
|
|
848
|
+
}
|
|
849
|
+
const status = isError ? theme.error(" ✗") : theme.success(" ✓");
|
|
850
|
+
const lines = result.split("\n");
|
|
851
|
+
const preview = lines[0]?.slice(0, 90) ?? "";
|
|
852
|
+
const extra =
|
|
853
|
+
lines.length > 1
|
|
854
|
+
? theme.tertiary(` (+${lines.length - 1} lines)`)
|
|
855
|
+
: "";
|
|
856
|
+
console.log(`${status} ${theme.toolResult(preview)}${extra}`);
|
|
857
|
+
if (isError) {
|
|
858
|
+
console.log(getBuddyReaction(state.buddy, "error"));
|
|
859
|
+
}
|
|
860
|
+
console.log("");
|
|
861
|
+
},
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
spinner?.stop();
|
|
865
|
+
|
|
866
|
+
// Flush any remaining markdown buffer
|
|
867
|
+
const remaining = flushMarkdown();
|
|
868
|
+
if (remaining) process.stdout.write(remaining);
|
|
869
|
+
|
|
870
|
+
// Update history
|
|
871
|
+
state.history.length = 0;
|
|
872
|
+
state.history.push(...result.messages);
|
|
873
|
+
|
|
874
|
+
// Persist all new messages from this turn (not just last 2)
|
|
875
|
+
const newMessages = result.messages.slice(preTurnMessageCount);
|
|
876
|
+
if (newMessages.length > 0) {
|
|
877
|
+
await state.session.appendMessages(newMessages);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Restore max tokens if ultrathink was used
|
|
881
|
+
if (isUltrathink && savedMaxTokens !== undefined) {
|
|
882
|
+
state.router.currentProvider.config.maxTokens = savedMaxTokens;
|
|
883
|
+
}
|
|
884
|
+
} catch (err) {
|
|
885
|
+
spinner?.stop();
|
|
886
|
+
resetMarkdown(); // Ensure markdown state is clean after errors
|
|
887
|
+
|
|
888
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
889
|
+
const categorized = categorizeError(error);
|
|
890
|
+
|
|
891
|
+
switch (categorized.category) {
|
|
892
|
+
case "rate_limit":
|
|
893
|
+
console.error(chalk.yellow(`\nRate limited. The router will try the next provider automatically.`));
|
|
894
|
+
break;
|
|
895
|
+
case "auth":
|
|
896
|
+
console.error(chalk.red(`\nAuth error: ${categorized.message}`));
|
|
897
|
+
console.error(chalk.dim("Check your API key (XAI_API_KEY or ANTHROPIC_API_KEY)"));
|
|
898
|
+
break;
|
|
899
|
+
case "network":
|
|
900
|
+
console.error(chalk.red(`\nNetwork error: ${categorized.message}`));
|
|
901
|
+
break;
|
|
902
|
+
default:
|
|
903
|
+
console.error(chalk.red(`\nError: ${categorized.message}`));
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
async function loadSystemPrompt(): Promise<string> {
|
|
909
|
+
// Load base system prompt
|
|
910
|
+
const promptPath = resolve(import.meta.dir, "../../prompts/system.md");
|
|
911
|
+
let prompt = "";
|
|
912
|
+
if (existsSync(promptPath)) {
|
|
913
|
+
prompt = await readFile(promptPath, "utf-8");
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const projectConfig = await loadProjectConfig(process.cwd());
|
|
917
|
+
if (projectConfig.instructions) {
|
|
918
|
+
prompt += `\n\n# Project Instructions\n\n${projectConfig.instructions}`;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Add environment context
|
|
922
|
+
prompt += `\n\n# Environment
|
|
923
|
+
- Working directory: ${process.cwd()}
|
|
924
|
+
- Platform: ${process.platform}
|
|
925
|
+
- Date: ${new Date().toISOString().split("T")[0]}
|
|
926
|
+
`;
|
|
927
|
+
|
|
928
|
+
return prompt;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
async function askPermission(
|
|
932
|
+
tool: string,
|
|
933
|
+
description: string
|
|
934
|
+
): Promise<boolean> {
|
|
935
|
+
// In plan mode, block non-read-only tools silently
|
|
936
|
+
if (isPlanMode()) {
|
|
937
|
+
return false;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Check permission system
|
|
941
|
+
const perm = checkPermission(tool);
|
|
942
|
+
if (perm === "allow") return true;
|
|
943
|
+
if (perm === "deny") return false;
|
|
944
|
+
|
|
945
|
+
return new Promise((resolve) => {
|
|
946
|
+
const rl = createInterface({
|
|
947
|
+
input: process.stdin,
|
|
948
|
+
output: process.stdout,
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
rl.question(
|
|
952
|
+
chalk.yellow(` Allow ${chalk.bold(tool)}? `) +
|
|
953
|
+
chalk.dim(description) +
|
|
954
|
+
chalk.yellow("\n [y]es / [a]lways / [n]o / [d]eny always: "),
|
|
955
|
+
async (answer) => {
|
|
956
|
+
rl.close();
|
|
957
|
+
const choice = answer.toLowerCase().trim();
|
|
958
|
+
switch (choice) {
|
|
959
|
+
case "y":
|
|
960
|
+
case "yes":
|
|
961
|
+
resolve(true);
|
|
962
|
+
break;
|
|
963
|
+
case "a":
|
|
964
|
+
case "always":
|
|
965
|
+
await recordPermission(tool, "always_allow");
|
|
966
|
+
console.log(chalk.dim(` ${tool} will be auto-allowed from now on.`));
|
|
967
|
+
resolve(true);
|
|
968
|
+
break;
|
|
969
|
+
case "d":
|
|
970
|
+
case "deny":
|
|
971
|
+
await recordPermission(tool, "always_deny");
|
|
972
|
+
console.log(chalk.dim(` ${tool} will be auto-denied from now on.`));
|
|
973
|
+
resolve(false);
|
|
974
|
+
break;
|
|
975
|
+
default:
|
|
976
|
+
resolve(false);
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
);
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function formatToolPreview(name: string, input: Record<string, unknown>): string {
|
|
985
|
+
switch (name) {
|
|
986
|
+
case "Bash":
|
|
987
|
+
return `$ ${input.command}`;
|
|
988
|
+
case "Read":
|
|
989
|
+
return `${input.file_path}${input.offset ? `:${input.offset}` : ""}`;
|
|
990
|
+
case "Write":
|
|
991
|
+
return `→ ${input.file_path}`;
|
|
992
|
+
case "Edit":
|
|
993
|
+
return `${input.file_path}`;
|
|
994
|
+
case "Glob":
|
|
995
|
+
return `${input.pattern}${input.path ? ` in ${input.path}` : ""}`;
|
|
996
|
+
case "Grep":
|
|
997
|
+
return `/${input.pattern}/${input.path ? ` in ${input.path}` : ""}`;
|
|
998
|
+
case "WebFetch":
|
|
999
|
+
return `${input.url}`;
|
|
1000
|
+
case "Agent":
|
|
1001
|
+
return `${input.description}`;
|
|
1002
|
+
case "LS":
|
|
1003
|
+
return input.path ? `${input.path}` : ".";
|
|
1004
|
+
default:
|
|
1005
|
+
return JSON.stringify(input).slice(0, 100);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function getArg(args: string[], flag: string): string | undefined {
|
|
1010
|
+
const idx = args.indexOf(flag);
|
|
1011
|
+
if (idx === -1) return undefined;
|
|
1012
|
+
return args[idx + 1];
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function timeSince(date: Date): string {
|
|
1016
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
1017
|
+
if (seconds < 60) return `${seconds}s`;
|
|
1018
|
+
const minutes = Math.floor(seconds / 60);
|
|
1019
|
+
if (minutes < 60) return `${minutes}m`;
|
|
1020
|
+
const hours = Math.floor(minutes / 60);
|
|
1021
|
+
if (hours < 24) return `${hours}h`;
|
|
1022
|
+
const days = Math.floor(hours / 24);
|
|
1023
|
+
return `${days}d`;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function printCommands() {
|
|
1027
|
+
console.log(`
|
|
1028
|
+
${theme.accent("Commands:")}
|
|
1029
|
+
${theme.toolName("/plan")} ${theme.secondary("Plan mode status")}
|
|
1030
|
+
${theme.toolName("/cost")} ${theme.secondary("Token usage and costs")}
|
|
1031
|
+
${theme.toolName("/status")} ${theme.secondary("Full session status")}
|
|
1032
|
+
${theme.toolName("/effort")} ${theme.tertiary("<lvl>")} ${theme.secondary("Set reasoning effort (fast/balanced/thorough)")}
|
|
1033
|
+
${theme.toolName("/btw")} ${theme.tertiary("<q>")} ${theme.secondary("Quick side question")}
|
|
1034
|
+
${theme.toolName("/history")} ${theme.secondary("Conversation turns")}
|
|
1035
|
+
${theme.toolName("/undo")} ${theme.secondary("Remove last turn")}
|
|
1036
|
+
${theme.toolName("/restore")} ${theme.tertiary("<f>")} ${theme.secondary("Undo file edit")}
|
|
1037
|
+
${theme.toolName("/diff")} ${theme.secondary("Git diff --stat")}
|
|
1038
|
+
${theme.toolName("/git")} ${theme.secondary("Branch, remote, changes")}
|
|
1039
|
+
${theme.toolName("/compact")} ${theme.secondary("Compress context")}
|
|
1040
|
+
${theme.toolName("/tools")} ${theme.secondary("List all tools")}
|
|
1041
|
+
${theme.toolName("/skills")} ${theme.secondary("List all skills")}
|
|
1042
|
+
${theme.toolName("/memory")} ${theme.secondary("Project memories")}
|
|
1043
|
+
${theme.toolName("/sessions")} ${theme.secondary("Saved sessions")}
|
|
1044
|
+
${theme.toolName("/buddy")} ${theme.secondary("Meet your companion")}
|
|
1045
|
+
${theme.toolName("/model")} ${theme.tertiary("<name>")} ${theme.secondary("Show/switch model")}
|
|
1046
|
+
${theme.toolName("/clear")} ${theme.secondary("Clear conversation")}
|
|
1047
|
+
${theme.toolName("/help")} ${theme.secondary("Show this help")}
|
|
1048
|
+
${theme.toolName("/quit")} ${theme.secondary("Exit")}
|
|
1049
|
+
|
|
1050
|
+
${theme.accent("Tips:")}
|
|
1051
|
+
${theme.tertiary("Shift+Tab")} ${theme.secondary("Cycle mode: Normal → Plan → Edits → YOLO")}
|
|
1052
|
+
${theme.tertiary("line ending \\\\")} ${theme.secondary("Multi-line input")}
|
|
1053
|
+
${theme.tertiary("[y/a/n/d]")} ${theme.secondary("Permission: yes / always / no / deny-always")}
|
|
1054
|
+
`);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function printHelp() {
|
|
1058
|
+
console.log(`
|
|
1059
|
+
${chalk.bold.cyan("AshlrCode")} ${chalk.dim(`v${VERSION}`)} — Multi-provider AI coding agent
|
|
1060
|
+
|
|
1061
|
+
${chalk.bold("USAGE")}
|
|
1062
|
+
ac [message] Run with a single message (non-interactive)
|
|
1063
|
+
ac Start interactive REPL
|
|
1064
|
+
ac --resume <id> Resume a previous session
|
|
1065
|
+
|
|
1066
|
+
${chalk.bold("OPTIONS")}
|
|
1067
|
+
-h, --help Show this help
|
|
1068
|
+
-v, --version Show version
|
|
1069
|
+
-c, --continue Resume last session in this directory
|
|
1070
|
+
--resume <id> Resume a specific session
|
|
1071
|
+
--fork-session <id> Copy session into new session
|
|
1072
|
+
--dangerously-skip-permissions Auto-approve all tool calls (alias: --yolo)
|
|
1073
|
+
--auto-accept-edits Auto-approve Write/Edit (Bash still asks)
|
|
1074
|
+
--print Output only text (for piping)
|
|
1075
|
+
--max-cost <dollars> Stop when cost exceeds limit
|
|
1076
|
+
|
|
1077
|
+
${chalk.bold("COMMANDS")} (in REPL)
|
|
1078
|
+
/plan Show plan mode status
|
|
1079
|
+
/cost Show token usage and costs
|
|
1080
|
+
/compact Compress conversation context
|
|
1081
|
+
/sessions List saved sessions
|
|
1082
|
+
/model Show current model
|
|
1083
|
+
/clear Clear conversation
|
|
1084
|
+
/help Show available commands
|
|
1085
|
+
/quit Exit
|
|
1086
|
+
|
|
1087
|
+
${chalk.bold("TOOLS")} (available to the AI)
|
|
1088
|
+
Bash Execute shell commands
|
|
1089
|
+
Read Read files with line numbers
|
|
1090
|
+
Write Create/overwrite files
|
|
1091
|
+
Edit Exact string replacement
|
|
1092
|
+
Glob Find files by pattern
|
|
1093
|
+
Grep Search file contents
|
|
1094
|
+
WebFetch Fetch URLs
|
|
1095
|
+
AskUser Ask questions with structured options
|
|
1096
|
+
Agent Spawn sub-agents for exploration
|
|
1097
|
+
TaskCreate/Update/List Track work progress
|
|
1098
|
+
EnterPlan Enter plan mode (read-only exploration)
|
|
1099
|
+
PlanWrite Write to plan file
|
|
1100
|
+
ExitPlan Exit plan mode
|
|
1101
|
+
|
|
1102
|
+
${chalk.bold("ENVIRONMENT")}
|
|
1103
|
+
XAI_API_KEY xAI API key (primary provider)
|
|
1104
|
+
ANTHROPIC_API_KEY Anthropic API key (fallback provider)
|
|
1105
|
+
AC_MODEL Override default model
|
|
1106
|
+
|
|
1107
|
+
${chalk.bold("CONFIG")}
|
|
1108
|
+
~/.ashlrcode/settings.json Provider configuration
|
|
1109
|
+
~/.ashlrcode/sessions/ Saved sessions (JSONL)
|
|
1110
|
+
~/.ashlrcode/plans/ Plan files
|
|
1111
|
+
./ASHLR.md Project-level instructions
|
|
1112
|
+
./CLAUDE.md Also supported for compatibility
|
|
1113
|
+
`);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Run
|
|
1117
|
+
main().catch((err) => {
|
|
1118
|
+
console.error(chalk.red(`Fatal: ${err.message}`));
|
|
1119
|
+
process.exit(1);
|
|
1120
|
+
});
|