astrabot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +411 -0
  2. package/ai/ai.config.ts +27 -0
  3. package/ai/auto-retry.ts +117 -0
  4. package/ai/config-loader.ts +132 -0
  5. package/ai/index.ts +4 -0
  6. package/ai/retry-prompt.ts +30 -0
  7. package/bin/astra +2 -0
  8. package/core/retry/error-classifier.ts +208 -0
  9. package/core/retry/index.ts +29 -0
  10. package/core/retry/retry-config.ts +142 -0
  11. package/core/retry/retry-engine.ts +215 -0
  12. package/game/index.html +573 -0
  13. package/game/neon-breaker.html +1037 -0
  14. package/index.ts +140 -0
  15. package/modes/agent/action-tracker.ts +47 -0
  16. package/modes/agent/agent-tools.ts +338 -0
  17. package/modes/agent/approval.ts +184 -0
  18. package/modes/agent/diff-view.ts +34 -0
  19. package/modes/agent/orchestrator.ts +234 -0
  20. package/modes/agent/tool-executor.ts +993 -0
  21. package/modes/agent/types.ts +68 -0
  22. package/modes/ask/orchestrator.ts +230 -0
  23. package/modes/auto.ts +88 -0
  24. package/modes/cli.ts +43 -0
  25. package/modes/multi/agent-pool-manager.ts +337 -0
  26. package/modes/multi/examples.ts +441 -0
  27. package/modes/multi/message-broker.ts +179 -0
  28. package/modes/multi/multi-agent-orchestrator.ts +891 -0
  29. package/modes/multi/orchestrator.ts +414 -0
  30. package/modes/multi/types.ts +245 -0
  31. package/modes/multi/workflow-builder.ts +569 -0
  32. package/modes/plan/orchestrator.ts +198 -0
  33. package/modes/plan/planner.ts +121 -0
  34. package/modes/plan/selection.ts +43 -0
  35. package/modes/plan/types.ts +13 -0
  36. package/modes/plan/web-tools.ts +132 -0
  37. package/modes/setup.ts +210 -0
  38. package/package.json +62 -0
  39. package/session/index.ts +45 -0
  40. package/session/session-context.ts +188 -0
  41. package/session/session-manager.ts +374 -0
  42. package/session/session-tools.ts +109 -0
  43. package/session/store.ts +278 -0
  44. package/tsconfig.json +30 -0
  45. package/tui/spinner.ts +182 -0
  46. package/tui/terminal-md.ts +17 -0
  47. package/tui/wakeup.ts +231 -0
@@ -0,0 +1,198 @@
1
+ import { text, isCancel, confirm } from "@clack/prompts";
2
+ import chalk from "chalk";
3
+ import { generatePlan } from "./planner";
4
+ import { printPlan, selectSteps } from "./selection";
5
+ import { defaultAgentConfig } from "../agent/types";
6
+ import { ActionTracker } from "../agent/action-tracker";
7
+ import { ToolExecutor } from "../agent/tool-executor";
8
+ import { createAgentTools } from "../agent/agent-tools";
9
+ import { stepCountIs, ToolLoopAgent } from "ai";
10
+ import { getAgentModel } from "../../ai";
11
+ import type { PlanStep } from "./types";
12
+ import { renderTerminalMarkdown } from "../../tui/terminal-md";
13
+ import { runApprovalFlow } from "../agent/approval";
14
+ import { createWebTools } from "./web-tools";
15
+ import { withSpinner } from "../../tui/spinner";
16
+ import { beginSession, endSession, markSessionInterrupted } from "../../session";
17
+ import { createSessionTools } from "../../session/session-tools";
18
+ import { promptToRetryAiCall } from "../../ai/retry-prompt";
19
+
20
+ function stepPrompt(goal: string, step: PlanStep): string {
21
+ return [`Goal: ${goal}`, `Step: ${step.title}`, step.description].join("\n");
22
+ }
23
+
24
+ export async function runPlanMode(preCapturedGoal?: string): Promise<void> {
25
+ console.log(chalk.bold("\nPlan Mode\n"));
26
+
27
+ const goal = preCapturedGoal ?? await text({
28
+ message: "What would you like the agent to do for you?",
29
+ placeholder: "Concrete task for this codebase...",
30
+ });
31
+
32
+ if (isCancel(goal) || !goal.trim()) return;
33
+
34
+ const config = defaultAgentConfig();
35
+ const tracker = new ActionTracker();
36
+ const executor = new ToolExecutor(tracker, config);
37
+
38
+ const approveCreatedFile = async (filePath: string): Promise<string> => {
39
+ const ok = await runApprovalFlow(tracker, {
40
+ paths: [filePath],
41
+ skipBatchPrompt: true,
42
+ });
43
+
44
+ if (!ok) {
45
+ executor.discardStagedPath(filePath);
46
+ return `User rejected creating ${filePath}. Do not modify or rely on this file unless you recreate it later.`;
47
+ }
48
+
49
+ const { errors } = executor.applyApprovedFromTracker();
50
+ if (errors.length) {
51
+ executor.discardStagedPath(filePath);
52
+ throw new Error(
53
+ `Failed to apply approved file ${filePath}: ${errors.join("; ")}`,
54
+ );
55
+ }
56
+
57
+ return `Created and applied ${filePath} after user approval.`;
58
+ };
59
+
60
+ const { entry: sessionEntry } = beginSession({
61
+ workspacePath: config.codebasePath,
62
+ mode: "plan",
63
+ goal: goal.trim(),
64
+ });
65
+
66
+ let plan;
67
+ while (true) {
68
+ try {
69
+ plan = await generatePlan(goal);
70
+ break;
71
+ } catch (error) {
72
+ const retry = await promptToRetryAiCall(
73
+ "Plan generation hit a provider error.",
74
+ error,
75
+ );
76
+ if (retry) continue;
77
+ markSessionInterrupted(sessionEntry.id);
78
+ await endSession(sessionEntry.id, tracker, "Stopped while generating a plan.");
79
+ executor.discardChanges();
80
+ return;
81
+ }
82
+ }
83
+
84
+ printPlan(plan);
85
+
86
+ const selected = await selectSteps(plan);
87
+ if (selected.length === 0) return;
88
+
89
+ const proceed = await confirm({
90
+ message: `Execute ${selected.length} step(s)`,
91
+ initialValue: true,
92
+ });
93
+
94
+ if (isCancel(proceed) || !proceed) return;
95
+
96
+ const tools = {
97
+ ...createAgentTools(executor, {
98
+ afterCreateFile: approveCreatedFile,
99
+ }),
100
+ ...createWebTools(tracker),
101
+ ...createSessionTools(config.codebasePath),
102
+ };
103
+
104
+ let lastResponse = "";
105
+ for (const step of selected) {
106
+ console.log(chalk.bold(`\nStep: ${step.title}\n`));
107
+
108
+ const agent = new ToolLoopAgent({
109
+ model: getAgentModel(),
110
+ stopWhen: stepCountIs(50),
111
+ tools,
112
+ });
113
+
114
+ while (true) {
115
+ try {
116
+ const r = await withSpinner(
117
+ {
118
+ message: `Executing: ${step.title}`,
119
+ doneMessage: "done",
120
+ failMessage: "failed",
121
+ },
122
+ () =>
123
+ agent.generate({
124
+ prompt: stepPrompt(plan.goal, step),
125
+ onStepFinish: ({ toolCalls }) => {
126
+ for (const tc of toolCalls) {
127
+ const preview = JSON.stringify(tc.input).slice(0, 160);
128
+ console.log(
129
+ chalk.green(" *"),
130
+ chalk.bold(String(tc.toolName)),
131
+ chalk.dim(
132
+ preview + (preview.length > 160 ? "..." : ""),
133
+ ),
134
+ );
135
+ }
136
+ },
137
+ }),
138
+ );
139
+
140
+ if (r.text.trim()) {
141
+ console.log(renderTerminalMarkdown(r.text));
142
+ lastResponse = r.text.trim();
143
+ }
144
+ break;
145
+ } catch (error) {
146
+ const retry = await promptToRetryAiCall(
147
+ `Step "${step.title}" hit a provider error.`,
148
+ error,
149
+ );
150
+ if (retry) continue;
151
+ markSessionInterrupted(sessionEntry.id);
152
+ await endSession(
153
+ sessionEntry.id,
154
+ tracker,
155
+ `Stopped during step: ${step.title}`,
156
+ );
157
+ executor.discardChanges();
158
+ return;
159
+ }
160
+ }
161
+ }
162
+
163
+ const ok = await runApprovalFlow(tracker);
164
+
165
+ if (!ok) {
166
+ await endSession(
167
+ sessionEntry.id,
168
+ tracker,
169
+ lastResponse || "Plan execution cancelled",
170
+ );
171
+ executor.discardChanges();
172
+ return;
173
+ }
174
+
175
+ await withSpinner(
176
+ {
177
+ message: "Applying approved changes...",
178
+ doneMessage: "all changes applied",
179
+ failMessage: "some operations failed",
180
+ },
181
+ async () => {
182
+ const { errors } = executor.applyApprovedFromTracker();
183
+ if (errors.length) {
184
+ console.log(chalk.red("\nSome operations reported errors:\n"));
185
+ for (const e of errors) console.log(chalk.red(` - ${e}`));
186
+ } else {
187
+ console.log(chalk.green("\nApplied.\n"));
188
+ }
189
+ },
190
+ );
191
+
192
+ await endSession(
193
+ sessionEntry.id,
194
+ tracker,
195
+ lastResponse || "Plan executed with " + selected.length + " step(s).",
196
+ );
197
+ executor.discardChanges();
198
+ }
@@ -0,0 +1,121 @@
1
+ import {
2
+ Output,
3
+ generateText,
4
+ stepCountIs,
5
+ } from "ai";
6
+ import z from "zod";
7
+ import { getAgentModel } from "../../ai";
8
+ import { ActionTracker } from "../agent/action-tracker";
9
+ import { ToolExecutor } from "../agent/tool-executor";
10
+ import { createAgentTools } from "../agent/agent-tools";
11
+ import { defaultAgentConfig } from "../agent/types";
12
+ import type { Plan, PlanStep } from "./types";
13
+ import { createWebTools } from "./web-tools";
14
+ import { withSpinner } from "../../tui/spinner";
15
+ import { getEnv } from "../../ai/config-loader";
16
+
17
+ const planSchema = z.object({
18
+ researchSummary: z.string().optional(),
19
+ steps: z
20
+ .array(
21
+ z.object({
22
+ title: z.string(),
23
+ description: z.string(),
24
+ hints: z.array(z.string()).optional(),
25
+ complexity: z.enum(["low", "medium", "high"]).optional(),
26
+ }),
27
+ )
28
+ .min(1)
29
+ .max(20),
30
+ });
31
+
32
+ /**
33
+ * Read-only subset of agent tools for the planner.
34
+ * Strips all mutation, shell, staging, and executor web tools
35
+ * (web is provided by createWebTools when FIRECRAWL_API_KEY is set).
36
+ */
37
+ function createPlannerTools(executor: ToolExecutor) {
38
+ const all = createAgentTools(executor);
39
+ const {
40
+ create_file: _cf,
41
+ modify_file: _mf,
42
+ delete_file: _df,
43
+ create_folder: _cfo,
44
+ replace_in_file: _rif,
45
+ append_to_file: _atf,
46
+ insert_at_line: _ial,
47
+ run_command: _rc,
48
+ run_background_command: _rbc,
49
+ execute_shell: _es,
50
+ discard_changes: _dc,
51
+ show_pending_changes: _spc,
52
+ run_tests: _rt,
53
+ run_test_file: _rtf,
54
+ lint_project: _lp,
55
+ format_project: _fp,
56
+ create_plan: _cp,
57
+ get_plan: _gp,
58
+ // strip executor's curl-based web tools — superseded by createWebTools (Firecrawl)
59
+ web_search: _ws,
60
+ fetch_url: _fu,
61
+ ...readOnly
62
+ } = all;
63
+ return readOnly;
64
+ }
65
+
66
+ const PLAN_INSTRUCTIONS = (
67
+ codebase: string,
68
+ hasWeb: boolean,
69
+ ): string => [
70
+ "You are Astra, an AI-native development CLI companion tool built to help the user navigate, analyze, and build within their workspace codebase. If the user asks who you are, what your name is, or what model you are running on, you must always identify yourself exclusively as Astra. Do not mention your underlying model architecture or provider.",
71
+ "You are a Plan-Mode planner. You DO NOT modify files.",
72
+ `Workspace: ${codebase}`,
73
+ "Use read-only tools for codebase/skills research.",
74
+ hasWeb
75
+ ? "Web tools are available (web_search/web_crawl/fetch_url). Use only when needed."
76
+ : "Web tools are unavailable.",
77
+ "Output must match the provided JSON schema.",
78
+ "Keep it short: 1-20 steps.",
79
+ ].join("\n");
80
+
81
+ export async function generatePlan(goal: string) {
82
+ const config = defaultAgentConfig();
83
+ const tracker = new ActionTracker();
84
+ const executor = new ToolExecutor(tracker, config);
85
+
86
+ const hasWeb = !!getEnv("FIRECRAWL_API_KEY");
87
+
88
+ const tools = {
89
+ ...createPlannerTools(executor),
90
+ ...(hasWeb ? createWebTools(tracker) : {}),
91
+ };
92
+
93
+ const result = await withSpinner(
94
+ {
95
+ message: "Researching & drafting plan…",
96
+ doneMessage: "plan ready",
97
+ failMessage: "planning failed",
98
+ },
99
+ () =>
100
+ generateText({
101
+ model: getAgentModel(),
102
+ tools,
103
+ stopWhen: stepCountIs(30),
104
+ system: PLAN_INSTRUCTIONS(config.codebasePath, hasWeb),
105
+ prompt: `User goal: \n${goal}`,
106
+ output: Output.object({ schema: planSchema }),
107
+ }),
108
+ );
109
+
110
+ const validated = planSchema.parse(result.output);
111
+
112
+ const steps: PlanStep[] = validated.steps.map((s, i) => ({
113
+ id: `step-${i + 1}`,
114
+ title: s.title,
115
+ description: s.description,
116
+ hints: s.hints,
117
+ complexity: s.complexity,
118
+ }));
119
+
120
+ return { goal, researchSummary: validated.researchSummary, steps };
121
+ }
@@ -0,0 +1,43 @@
1
+ import chalk from "chalk";
2
+ import { renderTerminalMarkdown } from "../../tui/terminal-md";
3
+ import type { Plan, PlanStep } from "./types";
4
+ import { isCancel, multiselect } from "@clack/prompts";
5
+
6
+ const COMPLEXITY_COLOR: Record<NonNullable<PlanStep['complexity']>, string> = {
7
+ low: chalk.green('low'),
8
+ medium: chalk.yellow('medium'),
9
+ high: chalk.red('high'),
10
+ };
11
+
12
+
13
+ export function printPlan(plan: Plan): void {
14
+ if (plan.researchSummary?.trim()) {
15
+ console.log(chalk.bold('\n🔍 Research summary'));
16
+ console.log(renderTerminalMarkdown(plan.researchSummary));
17
+ }
18
+ console.log(chalk.bold('\n📋 Generated Plan\n'));
19
+ for (const [i, s] of plan.steps.entries()) {
20
+ const tag = s.complexity ? `[${COMPLEXITY_COLOR[s.complexity]}]` : '';
21
+ console.log(` ${chalk.cyan(`Step ${String(i + 1).padStart(2)}`)}. ${chalk.bold(s.title)} ${tag}`);
22
+ }
23
+ console.log();
24
+ }
25
+
26
+ export async function selectSteps(plan: Plan): Promise<PlanStep[]> {
27
+ const options = plan.steps.map((s) => ({
28
+ value: s.id,
29
+ label: s.title,
30
+ hint: s.complexity ?? '',
31
+ }));
32
+
33
+ const picked = await multiselect<string>({
34
+ message: 'Select steps to execute (space toggles, enter confirms)',
35
+ options,
36
+ initialValues: plan.steps.map((s) => s.id),
37
+ required: false,
38
+ });
39
+
40
+ if (isCancel(picked)) return [];
41
+ const set = new Set<string>(picked);
42
+ return plan.steps.filter((s) => set.has(s.id));
43
+ }
@@ -0,0 +1,13 @@
1
+ export interface PlanStep{
2
+ id: string,
3
+ title: string,
4
+ description: string,
5
+ hints?: string[],
6
+ complexity?: 'low' | 'medium' | 'high'
7
+ }
8
+
9
+ export interface Plan{
10
+ goal: string,
11
+ researchSummary?: string,
12
+ steps: PlanStep[]
13
+ }
@@ -0,0 +1,132 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import Firecrawl from "@mendable/firecrawl-js";
4
+ import type { ActionTracker } from "../agent/action-tracker";
5
+ import { withSpinner } from "../../tui/spinner";
6
+ import { getEnv } from "../../ai/config-loader";
7
+
8
+ let client: Firecrawl | null = null;
9
+
10
+ function getClient(): Firecrawl {
11
+ if (client) return client;
12
+ const apiKey = getEnv("FIRECRAWL_API_KEY");
13
+ if (!apiKey) {
14
+ throw new Error("FIRECRAWL_API_KEY is not set");
15
+ }
16
+ client = new Firecrawl({ apiKey });
17
+ return client;
18
+ }
19
+
20
+ function clip(s: string, n = 8000): string {
21
+ return s.length > n ? s.slice(0, n) + "\n...[truncated]" : s;
22
+ }
23
+
24
+ export function createWebTools(tracker: ActionTracker) {
25
+ return {
26
+ web_search: tool({
27
+ description: "Search the web. Returns title/url/snippet list.",
28
+ inputSchema: z.object({
29
+ query: z.string().min(1),
30
+ limit: z
31
+ .number()
32
+ .int()
33
+ .min(1)
34
+ .max(10)
35
+ .optional()
36
+ .default(5),
37
+ }),
38
+ execute: async ({ query, limit }) => {
39
+ const short =
40
+ query.length > 50 ? query.slice(0, 50) + "…" : query;
41
+ const res = await withSpinner(
42
+ {
43
+ message: `Searching the web for "${short}"…`,
44
+ doneMessage: "results received",
45
+ failMessage: "search failed",
46
+ },
47
+ () =>
48
+ getClient().search(query, {
49
+ limit,
50
+ sources: ["web"],
51
+ }),
52
+ );
53
+
54
+ const items = (res.web ?? []).slice(0, limit);
55
+
56
+ const out = items
57
+ .map((d, i) => {
58
+ const title =
59
+ "title" in d && d.title ? d.title : "(untitled)";
60
+ const url = "url" in d && d.url ? d.url : "";
61
+ const snip = "snippet" in d && d.snippet ? d.snippet : "";
62
+ return `${i + 1}. ${title}\n ${url}\n ${snip}`;
63
+ })
64
+ .join("\n\n") || "(no result found)";
65
+
66
+ tracker.log({
67
+ type: "code_analysis",
68
+ path: `web_search:${query}`,
69
+ details: { after: out, toolName: "web_search" },
70
+ status: "executed",
71
+ });
72
+
73
+ return clip(out);
74
+ },
75
+ }),
76
+
77
+ web_crawl: tool({
78
+ description: "Scrape a URL into markdown text.",
79
+ inputSchema: z.object({ url: z.string().url() }),
80
+ execute: async ({ url }) => {
81
+ const short =
82
+ url.length > 60 ? url.slice(0, 60) + "…" : url;
83
+ const doc = await withSpinner(
84
+ {
85
+ message: `Crawling ${short}…`,
86
+ doneMessage: "page scraped",
87
+ failMessage: "crawl failed",
88
+ },
89
+ () =>
90
+ getClient().scrape(url, { formats: ["markdown"] }),
91
+ );
92
+ const md = (doc as { markdown?: string }).markdown ?? "";
93
+ tracker.log({
94
+ type: "code_analysis",
95
+ path: `web_crawl:${url}`,
96
+ details: { after: clip(md), toolName: "web_crawl" },
97
+ status: "executed",
98
+ });
99
+ return clip(md) || "(empty)";
100
+ },
101
+ }),
102
+
103
+ fetch_url: tool({
104
+ description: "HTTP GET for a URL. Returns response body.",
105
+ inputSchema: z.object({ url: z.string().url() }),
106
+ execute: async ({ url }) => {
107
+ const short =
108
+ url.length > 60 ? url.slice(0, 60) + "…" : url;
109
+ const r = await withSpinner(
110
+ {
111
+ message: `Fetching ${short}…`,
112
+ doneMessage: "response received",
113
+ failMessage: "fetch failed",
114
+ },
115
+ () => fetch(url, { redirect: "follow" }),
116
+ );
117
+ const body = await r.text();
118
+ const out = clip(body, 16_000);
119
+ tracker.log({
120
+ type: "code_analysis",
121
+ path: `fetch:${url}`,
122
+ details: {
123
+ after: `HTTP ${r.status}\n\n${out}`,
124
+ toolName: "fetch_url",
125
+ },
126
+ status: "executed",
127
+ });
128
+ return `HTTP ${r.status}\n\n${out}`;
129
+ },
130
+ }),
131
+ };
132
+ }