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
package/modes/setup.ts ADDED
@@ -0,0 +1,210 @@
1
+ import chalk from "chalk";
2
+ import { intro, outro, text, confirm, isCancel, autocomplete } from "@clack/prompts";
3
+ import {
4
+ getEnv,
5
+ getConfigPath,
6
+ saveConfig,
7
+ } from "../ai/config-loader";
8
+ import { withSpinner } from "../tui/spinner"; // Adjust path as necessary
9
+
10
+ // 1. Updated interface to include OpenRouter's exact pricing object structure
11
+ interface OpenRouterModel {
12
+ id: string;
13
+ name: string;
14
+ architecture?: {
15
+ modality?: string;
16
+ output_modalities?: string[];
17
+ };
18
+ pricing?: {
19
+ prompt: string; // Price per individual token
20
+ completion: string; // Price per individual token
21
+ };
22
+ }
23
+
24
+ export async function runSetup(): Promise<void> {
25
+ intro(chalk.bold("astra setup"));
26
+
27
+ console.log(
28
+ chalk.dim(
29
+ `Config will be saved to ${getConfigPath()}\n` +
30
+ `Existing values will be preserved when possible.\n`
31
+ )
32
+ );
33
+
34
+ const currentKey = getEnv("OPENROUTER_API_KEY") ?? "";
35
+ const currentModel = getEnv("OPENROUTER_DEFAULT_MODEL") ?? "";
36
+ const currentFirecrawl = getEnv("FIRECRAWL_API_KEY") ?? "";
37
+ const currentSkillsDirs = getEnv("SKILLS_DIRS") ?? "";
38
+
39
+ // ── OpenRouter API Key ──────────────────────────────────────────
40
+ const setKey = await confirm({
41
+ message: "Set OpenRouter API key?",
42
+ initialValue: !currentKey,
43
+ });
44
+ if (isCancel(setKey)) return outro(chalk.dim("Setup cancelled."));
45
+
46
+ let apiKey = currentKey;
47
+ if (setKey) {
48
+ const val = await text({
49
+ message: "OpenRouter API key",
50
+ placeholder: "sk-or-...",
51
+ initialValue: currentKey,
52
+ validate: (v) =>
53
+ (v ?? "").trim() ? undefined : "API key is required",
54
+ });
55
+ if (isCancel(val)) return outro(chalk.dim("Setup cancelled."));
56
+ apiKey = val.trim();
57
+ }
58
+
59
+ // ── Fetching All OpenRouter Models Dynamically ───────────────────
60
+ let modelOptions: Array<{ value: string; label: string; hint?: string }> = [];
61
+
62
+ const setModel = await confirm({
63
+ message: "Set default OpenRouter model?",
64
+ initialValue: !currentModel,
65
+ });
66
+ if (isCancel(setModel)) return outro(chalk.dim("Setup cancelled."));
67
+
68
+ if (setModel) {
69
+ try {
70
+ modelOptions = await withSpinner(
71
+ {
72
+ message: "Fetching all available OpenRouter models & pricing...",
73
+ doneMessage: "Loaded models successfully.",
74
+ failMessage: "Failed to fetch dynamic list. Dropping back to fallback entry.",
75
+ },
76
+ async () => {
77
+ const response = await fetch("https://openrouter.ai/api/v1/models");
78
+ if (!response.ok) throw new Error("Failed to communicate with OpenRouter registry");
79
+
80
+ const json = (await response.json()) as { data: OpenRouterModel[] };
81
+
82
+ return json.data
83
+ .filter((model) => {
84
+ const outModalities = model.architecture?.output_modalities;
85
+ const modalityStr = model.architecture?.modality || "";
86
+
87
+ if (outModalities) return outModalities.includes("text");
88
+ if (modalityStr) return modalityStr.endsWith("->text");
89
+
90
+ return true;
91
+ })
92
+ .map((model) => {
93
+ const provider = model.id.split("/")[0];
94
+
95
+ // Convert price-per-token strings to a clean price per 1 Million tokens
96
+ const promptPriceNum = parseFloat(model.pricing?.prompt || "0") * 1_000_000;
97
+ const completionPriceNum = parseFloat(model.pricing?.completion || "0") * 1_000_000;
98
+
99
+ let pricingHint = "Free";
100
+ if (promptPriceNum > 0 || completionPriceNum > 0) {
101
+ pricingHint = `In: $${promptPriceNum.toFixed(2)}, Out: $${completionPriceNum.toFixed(2)} /1M`;
102
+ }
103
+
104
+ return {
105
+ value: model.id,
106
+ label: model.name || model.id,
107
+ hint: `${provider} (${pricingHint})`,
108
+ };
109
+ });
110
+ }
111
+ );
112
+ } catch (error) {
113
+ modelOptions = [];
114
+ }
115
+
116
+ let modelId = currentModel;
117
+
118
+ if (modelOptions.length > 0) {
119
+ const selectedModel = await autocomplete({
120
+ message: "Select an OpenRouter text model (Type to search & compare pricing)",
121
+ options: [
122
+ ...modelOptions,
123
+ { value: "custom", label: "Custom Entry...", hint: "Type manual ID" }
124
+ ],
125
+ placeholder: "Search e.g. 'claude', 'gpt', 'llama'...",
126
+ });
127
+ if (isCancel(selectedModel)) return outro(chalk.dim("Setup cancelled."));
128
+
129
+ if (selectedModel === "custom") {
130
+ const customVal = await text({
131
+ message: "Enter custom OpenRouter Model ID",
132
+ placeholder: "provider/model-name",
133
+ initialValue: currentModel,
134
+ validate: (v) => ((v ?? "").trim() ? undefined : "Model ID is required"),
135
+ });
136
+ if (isCancel(customVal)) return outro(chalk.dim("Setup cancelled."));
137
+ modelId = customVal.trim();
138
+ } else {
139
+ modelId = selectedModel as string;
140
+ }
141
+ } else {
142
+ const fallbackVal = await text({
143
+ message: "Enter OpenRouter Model ID",
144
+ placeholder: "anthropic/claude-3.5-sonnet",
145
+ initialValue: currentModel || "anthropic/claude-3.5-sonnet",
146
+ validate: (v) => ((v ?? "").trim() ? undefined : "Model ID is required"),
147
+ });
148
+ if (isCancel(fallbackVal)) return outro(chalk.dim("Setup cancelled."));
149
+ modelId = fallbackVal.trim();
150
+ }
151
+
152
+ var finalModelId = modelId;
153
+ } else {
154
+ var finalModelId = currentModel;
155
+ }
156
+
157
+ // ── Firecrawl API Key (optional) ────────────────────────────────
158
+ const setFirecrawl = await confirm({
159
+ message: "Set Firecrawl API key? (optional — enables web search & crawl)",
160
+ initialValue: false,
161
+ });
162
+ if (isCancel(setFirecrawl)) return outro(chalk.dim("Setup cancelled."));
163
+
164
+ let firecrawlKey = currentFirecrawl;
165
+ if (setFirecrawl) {
166
+ const val = await text({
167
+ message: "Firecrawl API key",
168
+ placeholder: "fc-...",
169
+ initialValue: currentFirecrawl,
170
+ validate: (_v) => undefined,
171
+ });
172
+ if (isCancel(val)) return outro(chalk.dim("Setup cancelled."));
173
+ firecrawlKey = (val ?? "").trim();
174
+ }
175
+
176
+ // ── Skills Directories (optional) ───────────────────────────────
177
+ const setSkills = await confirm({
178
+ message: "Set custom skills directories? (optional)",
179
+ initialValue: false,
180
+ });
181
+ if (isCancel(setSkills)) return outro(chalk.dim("Setup cancelled."));
182
+
183
+ let skillsDirs = currentSkillsDirs;
184
+ if (setSkills) {
185
+ const val = await text({
186
+ message: "Skills directories (semicolon-separated)",
187
+ placeholder: "/path/to/skills;/another/dir",
188
+ initialValue: currentSkillsDirs,
189
+ validate: (_v) => undefined,
190
+ });
191
+ if (isCancel(val)) return outro(chalk.dim("Setup cancelled."));
192
+ skillsDirs = (val ?? "").trim();
193
+ }
194
+
195
+ // ── Save ────────────────────────────────────────────────────────
196
+ const entries: Record<string, string> = {};
197
+ if (apiKey) entries.OPENROUTER_API_KEY = apiKey;
198
+ if (finalModelId) entries.OPENROUTER_DEFAULT_MODEL = finalModelId;
199
+ if (firecrawlKey) entries.FIRECRAWL_API_KEY = firecrawlKey;
200
+ if (skillsDirs) entries.SKILLS_DIRS = skillsDirs;
201
+
202
+ saveConfig(entries);
203
+
204
+ outro(
205
+ chalk.green(
206
+ `\n✔ Configuration saved to ${getConfigPath()}\n` +
207
+ ` You can now run "astra wakeup" to get started.\n`
208
+ )
209
+ );
210
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "astrabot",
3
+ "version": "0.1.0",
4
+ "description": "AI-native development companion — Agent, Ask, and Plan modes in your terminal.",
5
+ "module": "index.ts",
6
+ "type": "module",
7
+ "bin": {
8
+ "astra": "bin/astra"
9
+ },
10
+ "files": [
11
+ "bin/astra",
12
+ "index.ts",
13
+ "ai/",
14
+ "modes/",
15
+ "tui/",
16
+ "session/",
17
+ "core/",
18
+ "game/",
19
+ "README.md",
20
+ "tsconfig.json"
21
+ ],
22
+ "scripts": {
23
+ "dev": "bun run index.ts",
24
+ "setup": "bun run index.ts setup",
25
+ "wakeup": "bun run index.ts wakeup",
26
+ "test": "bun test",
27
+ "prepublishOnly": "bun test"
28
+ },
29
+ "devDependencies": {
30
+ "@types/bun": "latest"
31
+ },
32
+ "peerDependencies": {
33
+ "typescript": "^5"
34
+ },
35
+ "dependencies": {
36
+ "@clack/core": "^1.3.1",
37
+ "@clack/prompts": "^1.4.0",
38
+ "@mendable/firecrawl-js": "^4.25.1",
39
+ "@openrouter/ai-sdk-provider": "^2.9.0",
40
+ "@types/marked-terminal": "^6.1.1",
41
+ "@types/node": "^25.9.1",
42
+ "chalk": "^5.6.2",
43
+ "commander": "^15.0.0",
44
+ "diff": "^9.0.0",
45
+ "docx": "^9.7.1",
46
+ "dotenv": "^17.4.2",
47
+ "figlet": "^1.11.0",
48
+ "marked": "^18.0.4",
49
+ "marked-terminal": "^7.3.0"
50
+ },
51
+ "keywords": [
52
+ "ai",
53
+ "cli",
54
+ "agent",
55
+ "openrouter",
56
+ "coding-assistant"
57
+ ],
58
+ "license": "MIT",
59
+ "engines": {
60
+ "bun": ">=1.0.0"
61
+ }
62
+ }
@@ -0,0 +1,45 @@
1
+ // ── Store (raw persistence layer) ─────────────────────────────────────────
2
+ export {
3
+ listSessions,
4
+ getSession,
5
+ getMostRecentSession,
6
+ createSession,
7
+ updateSession,
8
+ deleteSession,
9
+ clearAllSessions,
10
+ appendTranscript,
11
+ readSessionActions,
12
+ } from "./store";
13
+ export type {
14
+ SessionMode,
15
+ SessionStatus,
16
+ SessionEntry,
17
+ TranscriptMessage,
18
+ SessionStoreIndex,
19
+ } from "./store";
20
+
21
+ // ── Context building ───────────────────────────────────────────────────────
22
+ export {
23
+ captureSessionContext,
24
+ buildContextSummary,
25
+ buildSessionOneliner,
26
+ } from "./session-context";
27
+ export type { SessionContextData } from "./session-context";
28
+
29
+ // ── Session lifecycle ──────────────────────────────────────────────────────
30
+ export {
31
+ beginSession,
32
+ endSession,
33
+ endMultiSession,
34
+ markSessionInterrupted,
35
+ recordUserMessage,
36
+ recordAgentMessage,
37
+ getResumableSession,
38
+ getSessionHistory,
39
+ removeSession,
40
+ formatSessionLine,
41
+ } from "./session-manager";
42
+ export type { BeginSessionResult } from "./session-manager";
43
+
44
+ // ── Agent tools ────────────────────────────────────────────────────────────
45
+ export { createSessionTools } from "./session-tools";
@@ -0,0 +1,188 @@
1
+ import type { ActionTracker } from "../modes/agent/action-tracker";
2
+ import type { SessionEntry, TranscriptMessage } from "./store";
3
+
4
+ export type { TranscriptMessage };
5
+
6
+ /**
7
+ * Rich context persisted alongside a session entry.
8
+ */
9
+ export interface SessionContextData {
10
+ transcript: TranscriptMessage[];
11
+ activeFiles: string[];
12
+ pendingTasks: string[];
13
+ lastAgentResponse: string;
14
+ }
15
+
16
+ /**
17
+ * Build the full context payload from the current session state.
18
+ * This is called at session end so all fields are populated.
19
+ */
20
+ export function captureSessionContext(
21
+ tracker: ActionTracker,
22
+ transcript: TranscriptMessage[],
23
+ lastAgentResponse: string,
24
+ pendingTasks: string[] = []
25
+ ): SessionContextData {
26
+ const actions = tracker.getActions();
27
+ const activeFiles = [
28
+ ...new Set(
29
+ actions
30
+ .filter(
31
+ (a) =>
32
+ a.type === "file_create" ||
33
+ a.type === "file_modify" ||
34
+ a.type === "file_delete" ||
35
+ a.type === "code_analysis"
36
+ )
37
+ .map((a) => a.path)
38
+ .filter(Boolean)
39
+ ),
40
+ ];
41
+
42
+ return {
43
+ transcript,
44
+ activeFiles,
45
+ pendingTasks,
46
+ lastAgentResponse,
47
+ };
48
+ }
49
+
50
+ // ── Context summary builders ────────────────────────────────────────────────
51
+
52
+ /**
53
+ * Build the FULL resumption context block injected into the agent's system
54
+ * prompt when continuing an existing session.
55
+ *
56
+ * Includes:
57
+ * - session metadata
58
+ * - pending tasks
59
+ * - recent transcript (last N turns, configurable)
60
+ * - touched files
61
+ * - action counts
62
+ * - the agent's last response
63
+ */
64
+ export function buildContextSummary(
65
+ entry: SessionEntry,
66
+ opts: { transcriptTurns?: number } = {}
67
+ ): string {
68
+ const { transcriptTurns = 10 } = opts;
69
+ const parts: string[] = [];
70
+
71
+ parts.push("╔══════════════════════════════════════╗");
72
+ parts.push("║ RESUMED SESSION CONTEXT ║");
73
+ parts.push("╚══════════════════════════════════════╝");
74
+ parts.push("");
75
+
76
+ // ── Core metadata
77
+ parts.push(`Mode : ${entry.mode}`);
78
+ parts.push(`Session ID : ${entry.id}`);
79
+ parts.push(`Started : ${new Date(entry.createdAt).toLocaleString()}`);
80
+ parts.push("");
81
+
82
+ // ── Goals (all goals across session, most recent last)
83
+ if (entry.allGoals && entry.allGoals.length > 1) {
84
+ parts.push("Goals in this session:");
85
+ entry.allGoals.forEach((g, i) => parts.push(` ${i + 1}. ${g}`));
86
+ } else {
87
+ parts.push(`Goal: ${entry.lastGoal}`);
88
+ }
89
+ parts.push("");
90
+
91
+ // ── Summary (LLM-generated)
92
+ if (entry.summary) {
93
+ parts.push("What happened:");
94
+ parts.push(` ${entry.summary}`);
95
+ parts.push("");
96
+ }
97
+
98
+ // ── Pending tasks
99
+ if (entry.pendingTasks && entry.pendingTasks.length > 0) {
100
+ parts.push("⚠ Pending / incomplete tasks:");
101
+ entry.pendingTasks.forEach((t) => parts.push(` • ${t}`));
102
+ parts.push("");
103
+ }
104
+
105
+ // ── Files
106
+ if (entry.touchedFiles.length > 0) {
107
+ const shown = entry.touchedFiles.slice(0, 20);
108
+ parts.push(`Files touched (${entry.touchedFiles.length}):`);
109
+ shown.forEach((f) => parts.push(` • ${f}`));
110
+ if (entry.touchedFiles.length > 20) {
111
+ parts.push(` … and ${entry.touchedFiles.length - 20} more`);
112
+ }
113
+ parts.push("");
114
+ }
115
+
116
+ // ── Action counts
117
+ if (entry.appliedActions > 0 || entry.rejectedActions > 0) {
118
+ parts.push(
119
+ `Actions: ${entry.appliedActions} applied, ${entry.rejectedActions} rejected.`
120
+ );
121
+ parts.push("");
122
+ }
123
+
124
+ // ── Transcript (most recent N turns)
125
+ if (entry.transcript && entry.transcript.length > 0) {
126
+ const turns = entry.transcript.slice(-transcriptTurns * 2);
127
+ parts.push(`Recent conversation (last ${Math.floor(turns.length / 2)} turns):`);
128
+ parts.push("─".repeat(40));
129
+ for (const msg of turns) {
130
+ const label =
131
+ msg.role === "user"
132
+ ? "User"
133
+ : msg.role === "agent"
134
+ ? "Agent"
135
+ : "System";
136
+ const snippet = msg.content.length > 400
137
+ ? msg.content.slice(0, 400) + "…"
138
+ : msg.content;
139
+ parts.push(`[${label}] ${snippet}`);
140
+ }
141
+ parts.push("─".repeat(40));
142
+ parts.push("");
143
+ }
144
+
145
+ // ── Last agent response (in case transcript was trimmed)
146
+ if (entry.lastAgentResponse && entry.transcript.length === 0) {
147
+ parts.push("Agent's last response:");
148
+ parts.push(
149
+ entry.lastAgentResponse.length > 800
150
+ ? entry.lastAgentResponse.slice(0, 800) + "…"
151
+ : entry.lastAgentResponse
152
+ );
153
+ parts.push("");
154
+ }
155
+
156
+ parts.push("You are now resuming this session. Continue where you left off.");
157
+ parts.push(
158
+ entry.pendingTasks && entry.pendingTasks.length > 0
159
+ ? "Address the pending tasks above unless the user specifies otherwise."
160
+ : "Await the user's next instruction."
161
+ );
162
+
163
+ return parts.join("\n");
164
+ }
165
+
166
+ /**
167
+ * Lightweight one-line summary for display in the CLI session picker.
168
+ */
169
+ export function buildSessionOneliner(entry: SessionEntry): string {
170
+ const age = humanAge(entry.updatedAt);
171
+ const pending = entry.pendingTasks?.length
172
+ ? ` [${entry.pendingTasks.length} pending]`
173
+ : "";
174
+ return `${age.padEnd(8)} [${entry.mode}] ${entry.lastGoal.slice(0, 60)}${pending}`;
175
+ }
176
+
177
+ // ── Helpers ────────────────────────────────────────────────────────────────
178
+
179
+ function humanAge(isoString: string): string {
180
+ const diff = Date.now() - new Date(isoString).getTime();
181
+ const minutes = Math.floor(diff / 60_000);
182
+ if (minutes < 1) return "just now";
183
+ if (minutes < 60) return `${minutes}m ago`;
184
+ const hours = Math.floor(minutes / 60);
185
+ if (hours < 24) return `${hours}h ago`;
186
+ const days = Math.floor(hours / 24);
187
+ return `${days}d ago`;
188
+ }