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,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentTool — spawn sub-agents with isolated message context.
|
|
3
|
+
*
|
|
4
|
+
* Allows the model to delegate exploration, research, and analysis
|
|
5
|
+
* to child agents that run with fresh context and report back.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import type { Tool, ToolContext } from "./types.ts";
|
|
10
|
+
import { runSubAgent, type SubAgentConfig } from "../agent/sub-agent.ts";
|
|
11
|
+
import type { ProviderRouter } from "../providers/router.ts";
|
|
12
|
+
import type { ToolRegistry } from "../tools/registry.ts";
|
|
13
|
+
|
|
14
|
+
// These get injected at registration time
|
|
15
|
+
let _router: ProviderRouter | null = null;
|
|
16
|
+
let _registry: ToolRegistry | null = null;
|
|
17
|
+
let _systemPrompt: string = "";
|
|
18
|
+
|
|
19
|
+
export function initAgentTool(
|
|
20
|
+
router: ProviderRouter,
|
|
21
|
+
registry: ToolRegistry,
|
|
22
|
+
systemPrompt: string
|
|
23
|
+
) {
|
|
24
|
+
_router = router;
|
|
25
|
+
_registry = registry;
|
|
26
|
+
_systemPrompt = systemPrompt;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const agentTool: Tool = {
|
|
30
|
+
name: "Agent",
|
|
31
|
+
|
|
32
|
+
prompt() {
|
|
33
|
+
return `Launch a sub-agent to handle a task autonomously. The sub-agent has its own fresh conversation context and access to read-only tools (Read, Glob, Grep, WebFetch).
|
|
34
|
+
|
|
35
|
+
Use this for:
|
|
36
|
+
- Exploring parts of the codebase in parallel
|
|
37
|
+
- Researching a specific question
|
|
38
|
+
- Analyzing files or patterns
|
|
39
|
+
|
|
40
|
+
The sub-agent's findings are returned as text. Provide a clear, specific prompt describing what to investigate.`;
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
inputSchema() {
|
|
44
|
+
return {
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
description: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "Short description of what the agent will do (3-5 words)",
|
|
50
|
+
},
|
|
51
|
+
prompt: {
|
|
52
|
+
type: "string",
|
|
53
|
+
description:
|
|
54
|
+
"Detailed task description for the sub-agent. Include file paths, search terms, and specific questions.",
|
|
55
|
+
},
|
|
56
|
+
readOnly: {
|
|
57
|
+
type: "boolean",
|
|
58
|
+
description: "Only allow read-only tools (default: true)",
|
|
59
|
+
},
|
|
60
|
+
mode: {
|
|
61
|
+
type: "string",
|
|
62
|
+
enum: ["in_process", "worktree"],
|
|
63
|
+
description:
|
|
64
|
+
"Execution mode. 'worktree' creates an isolated git worktree so the sub-agent can make changes without affecting the current working tree.",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
required: ["description", "prompt"],
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
isReadOnly() {
|
|
72
|
+
return true; // The agent tool itself is read-only; sub-agent tools are filtered
|
|
73
|
+
},
|
|
74
|
+
isDestructive() {
|
|
75
|
+
return false;
|
|
76
|
+
},
|
|
77
|
+
isConcurrencySafe() {
|
|
78
|
+
return true;
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
validateInput(input) {
|
|
82
|
+
if (!input.prompt || typeof input.prompt !== "string") {
|
|
83
|
+
return "prompt is required";
|
|
84
|
+
}
|
|
85
|
+
if (!input.description || typeof input.description !== "string") {
|
|
86
|
+
return "description is required";
|
|
87
|
+
}
|
|
88
|
+
if (!_router || !_registry) {
|
|
89
|
+
return "AgentTool not initialized. Call initAgentTool() first.";
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
async call(input, context) {
|
|
95
|
+
const description = input.description as string;
|
|
96
|
+
const prompt = input.prompt as string;
|
|
97
|
+
const readOnly = (input.readOnly as boolean) ?? true;
|
|
98
|
+
const mode = (input.mode as SubAgentConfig["mode"]) ?? "in_process";
|
|
99
|
+
|
|
100
|
+
const modeLabel = mode === "worktree" ? " [worktree]" : "";
|
|
101
|
+
console.log(chalk.dim(` ◈ Spawning agent${modeLabel}: ${description}`));
|
|
102
|
+
|
|
103
|
+
const result = await runSubAgent({
|
|
104
|
+
name: description,
|
|
105
|
+
prompt,
|
|
106
|
+
systemPrompt: _systemPrompt + "\n\nYou are a sub-agent. Be thorough but concise. Report your findings clearly with file paths and line numbers.",
|
|
107
|
+
router: _router!,
|
|
108
|
+
toolRegistry: _registry!,
|
|
109
|
+
toolContext: context,
|
|
110
|
+
readOnly,
|
|
111
|
+
mode,
|
|
112
|
+
maxIterations: 15,
|
|
113
|
+
onToolStart: (name) => {
|
|
114
|
+
console.log(chalk.dim(` ↳ ${name}`));
|
|
115
|
+
},
|
|
116
|
+
onToolEnd: (_name, _result, isError) => {
|
|
117
|
+
if (isError) console.log(chalk.dim(` ↳ ${chalk.red("error")}`));
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const toolSummary = result.toolCalls.length > 0
|
|
122
|
+
? `\n\nTools used: ${result.toolCalls.map((t) => t.name).join(", ")}`
|
|
123
|
+
: "";
|
|
124
|
+
|
|
125
|
+
const worktreeInfo = result.worktree
|
|
126
|
+
? `\n\nWorktree branch: \`${result.worktree.branch}\` at ${result.worktree.path}`
|
|
127
|
+
: "";
|
|
128
|
+
|
|
129
|
+
console.log(chalk.dim(` ◈ Agent "${description}" completed${modeLabel}`));
|
|
130
|
+
|
|
131
|
+
return `## Agent: ${description}\n\n${result.text}${toolSummary}${worktreeInfo}`;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AskUserQuestion tool — beautifully formatted interactive questions.
|
|
3
|
+
*
|
|
4
|
+
* Supports two input modes:
|
|
5
|
+
* 1. readline (classic CLI) — used when Ink is not active
|
|
6
|
+
* 2. pending-question callback (Ink mode) — question is displayed via
|
|
7
|
+
* console output; the next user submission in repl.tsx resolves
|
|
8
|
+
* the pending promise via `answerPendingQuestion()`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createInterface } from "readline";
|
|
12
|
+
import { isBypassMode } from "../config/permissions.ts";
|
|
13
|
+
import { theme } from "../ui/theme.ts";
|
|
14
|
+
import type { Tool, ToolContext } from "./types.ts";
|
|
15
|
+
|
|
16
|
+
export interface QuestionOption {
|
|
17
|
+
label: string;
|
|
18
|
+
description: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Ink-mode pending-question mechanism
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/** Resolver for the currently pending question (null when idle). */
|
|
26
|
+
let pendingQuestionResolve: ((answer: string) => void) | null = null;
|
|
27
|
+
|
|
28
|
+
/** Options for the currently pending question. */
|
|
29
|
+
let pendingOptions: QuestionOption[] = [];
|
|
30
|
+
|
|
31
|
+
/** Check if there is a pending question awaiting an answer. */
|
|
32
|
+
export function hasPendingQuestion(): boolean {
|
|
33
|
+
return pendingQuestionResolve !== null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Answer a pending question (called from repl.tsx when the user submits input). */
|
|
37
|
+
export function answerPendingQuestion(answer: string): boolean {
|
|
38
|
+
if (!pendingQuestionResolve) return false;
|
|
39
|
+
pendingQuestionResolve(answer);
|
|
40
|
+
pendingQuestionResolve = null;
|
|
41
|
+
pendingOptions = [];
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Return the options for the currently pending question. */
|
|
46
|
+
export function getPendingOptions(): QuestionOption[] {
|
|
47
|
+
return pendingOptions;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const askUserTool: Tool = {
|
|
51
|
+
name: "AskUser",
|
|
52
|
+
|
|
53
|
+
prompt() {
|
|
54
|
+
return `Ask the user a question with structured options. Use this when you need to:
|
|
55
|
+
1. Clarify requirements or direction
|
|
56
|
+
2. Present design choices with tradeoffs
|
|
57
|
+
3. Get user input before proceeding with a task
|
|
58
|
+
|
|
59
|
+
Each question should have 2-4 options with clear labels and descriptions.
|
|
60
|
+
Questions should be specific and emerge from actual analysis, not generic.
|
|
61
|
+
The user can always type a custom answer beyond the provided options.`;
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
inputSchema() {
|
|
65
|
+
return {
|
|
66
|
+
type: "object",
|
|
67
|
+
properties: {
|
|
68
|
+
question: {
|
|
69
|
+
type: "string",
|
|
70
|
+
description: "The question to ask the user. Should be clear and specific.",
|
|
71
|
+
},
|
|
72
|
+
options: {
|
|
73
|
+
type: "array",
|
|
74
|
+
items: {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {
|
|
77
|
+
label: {
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "Short label for this option (1-5 words)",
|
|
80
|
+
},
|
|
81
|
+
description: {
|
|
82
|
+
type: "string",
|
|
83
|
+
description: "Explanation of what this option means and its tradeoffs",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
required: ["label", "description"],
|
|
87
|
+
},
|
|
88
|
+
description: "2-4 options for the user to choose from",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
required: ["question", "options"],
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
isReadOnly() {
|
|
96
|
+
return true;
|
|
97
|
+
},
|
|
98
|
+
isDestructive() {
|
|
99
|
+
return false;
|
|
100
|
+
},
|
|
101
|
+
isConcurrencySafe() {
|
|
102
|
+
return false;
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
validateInput(input) {
|
|
106
|
+
if (!input.question || typeof input.question !== "string") {
|
|
107
|
+
return "question is required";
|
|
108
|
+
}
|
|
109
|
+
if (!Array.isArray(input.options) || input.options.length < 2) {
|
|
110
|
+
return "At least 2 options are required";
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
async call(input, _context) {
|
|
116
|
+
const question = input.question as string;
|
|
117
|
+
const options = input.options as QuestionOption[];
|
|
118
|
+
|
|
119
|
+
if (isBypassMode()) {
|
|
120
|
+
return await askInInkMode(question, options);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return await askInCliMode(question, options);
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Ink-mode implementation (bypass / Ink active)
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
async function askInInkMode(
|
|
132
|
+
question: string,
|
|
133
|
+
options: QuestionOption[],
|
|
134
|
+
): Promise<string> {
|
|
135
|
+
const w = process.stdout.columns || 80;
|
|
136
|
+
|
|
137
|
+
// Display question — Ink captures console output into its log area
|
|
138
|
+
console.log("");
|
|
139
|
+
console.log("─".repeat(w));
|
|
140
|
+
console.log(`\n ✦ ${question}\n`);
|
|
141
|
+
options.forEach((opt, i) => {
|
|
142
|
+
console.log(` ${i + 1} → ${opt.label}`);
|
|
143
|
+
console.log(` ${opt.description}`);
|
|
144
|
+
if (i < options.length - 1) console.log("");
|
|
145
|
+
});
|
|
146
|
+
console.log(`\n ${options.length + 1} → Other (type your own answer)`);
|
|
147
|
+
console.log("\n" + "─".repeat(w));
|
|
148
|
+
|
|
149
|
+
// Wait for user to submit input via the repl
|
|
150
|
+
pendingOptions = options;
|
|
151
|
+
const answer = await new Promise<string>((resolve) => {
|
|
152
|
+
pendingQuestionResolve = resolve;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const choiceNum = parseInt(answer.trim(), 10);
|
|
156
|
+
if (choiceNum >= 1 && choiceNum <= options.length) {
|
|
157
|
+
const selected = options[choiceNum - 1]!;
|
|
158
|
+
return `User selected: "${selected.label}" — ${selected.description}`;
|
|
159
|
+
}
|
|
160
|
+
return `User's custom answer: "${answer.trim()}"`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// CLI-mode implementation (readline, no Ink)
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
async function askInCliMode(
|
|
168
|
+
question: string,
|
|
169
|
+
options: QuestionOption[],
|
|
170
|
+
): Promise<string> {
|
|
171
|
+
const w = process.stdout.columns || 80;
|
|
172
|
+
|
|
173
|
+
console.log("");
|
|
174
|
+
console.log(theme.border("─".repeat(w)));
|
|
175
|
+
console.log("");
|
|
176
|
+
|
|
177
|
+
console.log(theme.accentBold(" ✦ ") + theme.primary(question));
|
|
178
|
+
console.log("");
|
|
179
|
+
|
|
180
|
+
options.forEach((opt, i) => {
|
|
181
|
+
const num = theme.accent(` ${i + 1} `);
|
|
182
|
+
const dot = theme.accent("→");
|
|
183
|
+
const label = theme.accentBold(` ${opt.label}`);
|
|
184
|
+
console.log(`${num}${dot}${label}`);
|
|
185
|
+
console.log(theme.secondary(` ${opt.description}`));
|
|
186
|
+
if (i < options.length - 1) console.log("");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
console.log("");
|
|
190
|
+
console.log(theme.muted(` ${options.length + 1} → Other (type your own answer)`));
|
|
191
|
+
|
|
192
|
+
console.log("");
|
|
193
|
+
console.log(theme.border("─".repeat(w)));
|
|
194
|
+
|
|
195
|
+
const answer = await promptUser(theme.accent(" Choice: "));
|
|
196
|
+
|
|
197
|
+
const choiceNum = parseInt(answer.trim(), 10);
|
|
198
|
+
|
|
199
|
+
if (choiceNum >= 1 && choiceNum <= options.length) {
|
|
200
|
+
const selected = options[choiceNum - 1]!;
|
|
201
|
+
console.log(theme.success(` ✓ ${selected.label}`));
|
|
202
|
+
console.log("");
|
|
203
|
+
return `User selected: "${selected.label}" — ${selected.description}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (choiceNum === options.length + 1 || isNaN(choiceNum)) {
|
|
207
|
+
const customAnswer = isNaN(choiceNum)
|
|
208
|
+
? answer.trim()
|
|
209
|
+
: await promptUser(theme.accent(" Your answer: "));
|
|
210
|
+
console.log(theme.success(` ✓ ${customAnswer}`));
|
|
211
|
+
console.log("");
|
|
212
|
+
return `User's custom answer: "${customAnswer}"`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return `User selected option ${answer.trim()}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function promptUser(prompt: string): Promise<string> {
|
|
219
|
+
return new Promise((resolve) => {
|
|
220
|
+
const rl = createInterface({
|
|
221
|
+
input: process.stdin,
|
|
222
|
+
output: process.stdout,
|
|
223
|
+
});
|
|
224
|
+
rl.question(prompt, (answer) => {
|
|
225
|
+
rl.close();
|
|
226
|
+
resolve(answer);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BashTool — execute shell commands with timeout and live output streaming.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import type { Tool, ToolContext } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TIMEOUT = 120_000; // 2 minutes
|
|
9
|
+
const LIVE_OUTPUT_THRESHOLD = 5_000; // Stream live after 5s
|
|
10
|
+
|
|
11
|
+
export const bashTool: Tool = {
|
|
12
|
+
name: "Bash",
|
|
13
|
+
|
|
14
|
+
prompt() {
|
|
15
|
+
return "Execute a bash command and return its output. Use for system commands, git operations, running tests, installing packages, etc. Commands run in the project's working directory.";
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
inputSchema() {
|
|
19
|
+
return {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
command: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "The bash command to execute",
|
|
25
|
+
},
|
|
26
|
+
timeout: {
|
|
27
|
+
type: "number",
|
|
28
|
+
description: "Timeout in milliseconds (default: 120000)",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
required: ["command"],
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
isReadOnly() {
|
|
36
|
+
return false;
|
|
37
|
+
},
|
|
38
|
+
isDestructive() {
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
isConcurrencySafe() {
|
|
42
|
+
return false;
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
validateInput(input) {
|
|
46
|
+
if (!input.command || typeof input.command !== "string") {
|
|
47
|
+
return "command is required and must be a string";
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
checkPermissions(input: Record<string, unknown>): string | null {
|
|
53
|
+
const cmd = input.command as string;
|
|
54
|
+
if (!cmd) return null;
|
|
55
|
+
const dangerous = [
|
|
56
|
+
/\brm\s+-rf\s+[\/~]/,
|
|
57
|
+
/\bdd\s+.*of=\/dev/,
|
|
58
|
+
/\bmkfs\b/,
|
|
59
|
+
];
|
|
60
|
+
for (const pattern of dangerous) {
|
|
61
|
+
if (pattern.test(cmd)) return `Dangerous command pattern: ${pattern.source}`;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
async call(input, context) {
|
|
67
|
+
const command = input.command as string;
|
|
68
|
+
const timeout = (input.timeout as number) ?? DEFAULT_TIMEOUT;
|
|
69
|
+
|
|
70
|
+
const proc = Bun.spawn(["bash", "-c", command], {
|
|
71
|
+
cwd: context.cwd,
|
|
72
|
+
stdout: "pipe",
|
|
73
|
+
stderr: "pipe",
|
|
74
|
+
env: { ...process.env },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const timeoutId = setTimeout(() => {
|
|
78
|
+
proc.kill();
|
|
79
|
+
}, timeout);
|
|
80
|
+
|
|
81
|
+
// Start reading stderr concurrently (prevents deadlock if pipe buffer fills)
|
|
82
|
+
const stderrPromise = new Response(proc.stderr).text();
|
|
83
|
+
|
|
84
|
+
// Read stdout in chunks
|
|
85
|
+
const reader = proc.stdout.getReader();
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// Collect output with live streaming for long-running commands
|
|
89
|
+
let stdout = "";
|
|
90
|
+
let liveMode = false;
|
|
91
|
+
const startTime = Date.now();
|
|
92
|
+
const decoder = new TextDecoder();
|
|
93
|
+
|
|
94
|
+
while (true) {
|
|
95
|
+
const { done, value } = await reader.read();
|
|
96
|
+
if (done) break;
|
|
97
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
98
|
+
stdout += chunk;
|
|
99
|
+
|
|
100
|
+
// Switch to live output after threshold
|
|
101
|
+
if (!liveMode && Date.now() - startTime > LIVE_OUTPUT_THRESHOLD) {
|
|
102
|
+
liveMode = true;
|
|
103
|
+
process.stderr.write(chalk.dim(" [live output]\n"));
|
|
104
|
+
// Print buffered content
|
|
105
|
+
if (stdout.length > chunk.length) {
|
|
106
|
+
process.stderr.write(chalk.dim(stdout.slice(0, -chunk.length)));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (liveMode) {
|
|
110
|
+
process.stderr.write(chalk.dim(chunk));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const stderr = await stderrPromise;
|
|
115
|
+
const exitCode = await proc.exited;
|
|
116
|
+
clearTimeout(timeoutId);
|
|
117
|
+
|
|
118
|
+
if (liveMode) {
|
|
119
|
+
process.stderr.write("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let result = "";
|
|
123
|
+
if (stdout) result += stdout;
|
|
124
|
+
if (stderr) result += (result ? "\n" : "") + stderr;
|
|
125
|
+
if (exitCode !== 0) {
|
|
126
|
+
result += `\nExit code: ${exitCode}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Truncate very long output for the model
|
|
130
|
+
if (result.length > 50_000) {
|
|
131
|
+
result =
|
|
132
|
+
result.slice(0, 20_000) +
|
|
133
|
+
`\n\n[... truncated ${result.length - 40_000} chars ...]\n\n` +
|
|
134
|
+
result.slice(-20_000);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return result || "(no output)";
|
|
138
|
+
} catch {
|
|
139
|
+
clearTimeout(timeoutId);
|
|
140
|
+
// Release stdout reader lock and drain stderr
|
|
141
|
+
try { reader.releaseLock(); } catch {}
|
|
142
|
+
try { await stderrPromise; } catch {}
|
|
143
|
+
return "Command timed out";
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfigTool — view and modify settings from within the agent.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Tool, ToolContext } from "./types.ts";
|
|
6
|
+
import { loadSettings, saveSettings } from "../config/settings.ts";
|
|
7
|
+
|
|
8
|
+
export const configTool: Tool = {
|
|
9
|
+
name: "Config",
|
|
10
|
+
|
|
11
|
+
prompt() {
|
|
12
|
+
return "View or modify AshlrCode settings. Operations: 'get' (read a setting), 'set' (write a setting), 'list' (show all settings).";
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
inputSchema() {
|
|
16
|
+
return {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
operation: {
|
|
20
|
+
type: "string",
|
|
21
|
+
enum: ["get", "set", "list"],
|
|
22
|
+
description: "Operation to perform",
|
|
23
|
+
},
|
|
24
|
+
key: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "Setting key (dot-notation, e.g. 'providers.primary.model')",
|
|
27
|
+
},
|
|
28
|
+
value: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Value to set (for 'set' operation)",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
required: ["operation"],
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
isReadOnly() {
|
|
38
|
+
return false;
|
|
39
|
+
},
|
|
40
|
+
isDestructive() {
|
|
41
|
+
return false;
|
|
42
|
+
},
|
|
43
|
+
isConcurrencySafe() {
|
|
44
|
+
return false;
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
validateInput(input) {
|
|
48
|
+
const op = input.operation as string;
|
|
49
|
+
if (!["get", "set", "list"].includes(op)) {
|
|
50
|
+
return "operation must be 'get', 'set', or 'list'";
|
|
51
|
+
}
|
|
52
|
+
if (op === "set" && (!input.key || !input.value)) {
|
|
53
|
+
return "key and value are required for set operation";
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
async call(input, _context) {
|
|
59
|
+
const op = input.operation as string;
|
|
60
|
+
const settings = await loadSettings();
|
|
61
|
+
|
|
62
|
+
switch (op) {
|
|
63
|
+
case "list": {
|
|
64
|
+
const sanitized = redactSecrets(JSON.parse(JSON.stringify(settings)));
|
|
65
|
+
return JSON.stringify(sanitized, null, 2);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
case "get": {
|
|
69
|
+
const key = input.key as string;
|
|
70
|
+
// Block direct access to API keys
|
|
71
|
+
if (key.toLowerCase().includes("apikey") || key.toLowerCase().includes("api_key")) {
|
|
72
|
+
return "API keys are redacted for security. View them in ~/.ashlrcode/settings.json directly.";
|
|
73
|
+
}
|
|
74
|
+
const value = getNestedValue(settings as unknown as Record<string, unknown>, key);
|
|
75
|
+
if (value === undefined) return `Setting not found: ${key}`;
|
|
76
|
+
if (typeof value === "object" && value !== null) {
|
|
77
|
+
// Redact any nested secrets before returning
|
|
78
|
+
const sanitized = redactSecrets(JSON.parse(JSON.stringify(value)));
|
|
79
|
+
return JSON.stringify(sanitized, null, 2);
|
|
80
|
+
}
|
|
81
|
+
return String(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
case "set": {
|
|
85
|
+
const key = input.key as string;
|
|
86
|
+
// Block setting API keys via the tool (security)
|
|
87
|
+
if (key.toLowerCase().includes("apikey") || key.toLowerCase().includes("api_key")) {
|
|
88
|
+
return "Cannot set API keys via Config tool. Set them as environment variables or edit ~/.ashlrcode/settings.json directly.";
|
|
89
|
+
}
|
|
90
|
+
const value = input.value as string;
|
|
91
|
+
setNestedValue(settings as unknown as Record<string, unknown>, key, value);
|
|
92
|
+
await saveSettings(settings);
|
|
93
|
+
return `Set ${key} = ${value}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
default:
|
|
97
|
+
return `Unknown operation: ${op}`;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
function redactSecrets(obj: Record<string, unknown>): Record<string, unknown> {
|
|
103
|
+
const result = { ...obj };
|
|
104
|
+
for (const [key, value] of Object.entries(result)) {
|
|
105
|
+
if (key.toLowerCase().includes("apikey") || key.toLowerCase().includes("api_key") || key.toLowerCase().includes("token") || key.toLowerCase().includes("secret")) {
|
|
106
|
+
result[key] = "[redacted]";
|
|
107
|
+
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
108
|
+
result[key] = redactSecrets(value as Record<string, unknown>);
|
|
109
|
+
} else if (Array.isArray(value)) {
|
|
110
|
+
result[key] = value.map((v) =>
|
|
111
|
+
typeof v === "object" && v !== null ? redactSecrets(v as Record<string, unknown>) : v
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
119
|
+
const keys = path.split(".");
|
|
120
|
+
let current: unknown = obj;
|
|
121
|
+
for (const key of keys) {
|
|
122
|
+
if (current === null || current === undefined || typeof current !== "object") {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
current = (current as Record<string, unknown>)[key];
|
|
126
|
+
}
|
|
127
|
+
return current;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function setNestedValue(obj: Record<string, unknown>, path: string, value: string): void {
|
|
131
|
+
const keys = path.split(".");
|
|
132
|
+
let current = obj;
|
|
133
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
134
|
+
const key = keys[i]!;
|
|
135
|
+
if (!(key in current) || typeof current[key] !== "object") {
|
|
136
|
+
current[key] = {};
|
|
137
|
+
}
|
|
138
|
+
current = current[key] as Record<string, unknown>;
|
|
139
|
+
}
|
|
140
|
+
// Try to parse as JSON, fallback to string
|
|
141
|
+
const lastKey = keys[keys.length - 1]!;
|
|
142
|
+
try {
|
|
143
|
+
current[lastKey] = JSON.parse(value);
|
|
144
|
+
} catch {
|
|
145
|
+
current[lastKey] = value;
|
|
146
|
+
}
|
|
147
|
+
}
|