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,278 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { getConfigDir } from "../ai/config-loader";
4
+ import type { ActionLog } from "../modes/agent/types";
5
+
6
+ // ── Types ──────────────────────────────────────────────────────────────────
7
+
8
+ export type SessionMode = "agent" | "ask" | "plan" | "multi" | "auto";
9
+ export type SessionStatus = "active" | "completed" | "interrupted";
10
+
11
+ export interface TranscriptMessage {
12
+ role: "user" | "agent" | "system";
13
+ content: string;
14
+ timestamp: string;
15
+ }
16
+
17
+ export interface SessionEntry {
18
+ id: string;
19
+ workspacePath: string;
20
+ mode: SessionMode;
21
+ status: SessionStatus;
22
+ /** Natural-language summary of what happened in this session. */
23
+ summary: string;
24
+ /** The last user prompt / goal that was provided */
25
+ lastGoal: string;
26
+ /** All user goals across the session (for multi-turn awareness) */
27
+ allGoals: string[];
28
+ /** Key files that were touched (created, modified, read) */
29
+ touchedFiles: string[];
30
+ /** Number of actions that were approved and applied */
31
+ appliedActions: number;
32
+ /** Number of actions that were rejected or discarded */
33
+ rejectedActions: number;
34
+ /**
35
+ * The full conversation transcript for this session.
36
+ * Stored inline so context reconstruction doesn't need a separate file read.
37
+ * Capped at TRANSCRIPT_CAP messages (oldest trimmed first).
38
+ */
39
+ transcript: TranscriptMessage[];
40
+ /**
41
+ * Tasks/sub-goals the agent identified but didn't complete.
42
+ * Enables "pick up where we left off" without re-analysing the transcript.
43
+ */
44
+ pendingTasks: string[];
45
+ /**
46
+ * The agent's final message verbatim (truncated to 2 000 chars).
47
+ * Useful for one-shot context injection without loading the full transcript.
48
+ */
49
+ lastAgentResponse: string;
50
+ /** ISO-8601 timestamps */
51
+ createdAt: string;
52
+ updatedAt: string;
53
+ /** Previous session ID for chaining */
54
+ previousSessionId?: string;
55
+ }
56
+
57
+ export interface SessionStoreIndex {
58
+ version: number;
59
+ sessions: SessionEntry[];
60
+ maxSessions: number;
61
+ }
62
+
63
+ // ── Constants ──────────────────────────────────────────────────────────────
64
+
65
+ const STORE_DIR = path.join(getConfigDir(), "sessions");
66
+ const INDEX_FILE = path.join(STORE_DIR, "index.json");
67
+ const MAX_SESSIONS = 100;
68
+ const TRANSCRIPT_CAP = 60; // max messages kept inline
69
+ const CURRENT_VERSION = 2;
70
+
71
+ // ── Helpers ────────────────────────────────────────────────────────────────
72
+
73
+ function ensureStoreDir(): void {
74
+ if (!fs.existsSync(STORE_DIR)) {
75
+ fs.mkdirSync(STORE_DIR, { recursive: true });
76
+ }
77
+ }
78
+
79
+ function generateSessionId(): string {
80
+ const ts = Date.now().toString(36);
81
+ const rand = Math.random().toString(36).slice(2, 8);
82
+ return `sess_${ts}_${rand}`;
83
+ }
84
+
85
+ function now(): string {
86
+ return new Date().toISOString();
87
+ }
88
+
89
+ function atomicWrite(filePath: string, data: string): void {
90
+ ensureStoreDir();
91
+ const tmp = `${filePath}.tmp_${process.pid}_${Date.now()}`;
92
+ fs.writeFileSync(tmp, data, "utf8");
93
+ fs.renameSync(tmp, filePath);
94
+ }
95
+
96
+ // ── Index Operations ───────────────────────────────────────────────────────
97
+
98
+ function readIndex(): SessionStoreIndex {
99
+ if (!fs.existsSync(INDEX_FILE)) {
100
+ return { version: CURRENT_VERSION, sessions: [], maxSessions: MAX_SESSIONS };
101
+ }
102
+ try {
103
+ const raw = fs.readFileSync(INDEX_FILE, "utf8");
104
+ const parsed = JSON.parse(raw) as SessionStoreIndex;
105
+ // Back-compat: fill missing fields from older entries
106
+ for (const s of parsed.sessions) {
107
+ s.allGoals ??= [s.lastGoal];
108
+ s.transcript ??= [];
109
+ s.pendingTasks ??= [];
110
+ s.lastAgentResponse ??= "";
111
+ }
112
+ parsed.sessions.sort(
113
+ (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
114
+ );
115
+ return parsed;
116
+ } catch {
117
+ return { version: CURRENT_VERSION, sessions: [], maxSessions: MAX_SESSIONS };
118
+ }
119
+ }
120
+
121
+ function writeIndex(index: SessionStoreIndex): void {
122
+ atomicWrite(INDEX_FILE, JSON.stringify(index, null, 2));
123
+ }
124
+
125
+ // ── Public API ─────────────────────────────────────────────────────────────
126
+
127
+ export function listSessions(
128
+ workspacePath?: string,
129
+ limit = 20
130
+ ): SessionEntry[] {
131
+ const index = readIndex();
132
+ let sessions = index.sessions;
133
+ if (workspacePath) {
134
+ const root = path.resolve(workspacePath);
135
+ sessions = sessions.filter((s) => path.resolve(s.workspacePath) === root);
136
+ }
137
+ return sessions.slice(0, limit);
138
+ }
139
+
140
+ export function getSession(id: string): SessionEntry | undefined {
141
+ const index = readIndex();
142
+ return index.sessions.find((s) => s.id === id);
143
+ }
144
+
145
+ export function getMostRecentSession(
146
+ workspacePath?: string
147
+ ): SessionEntry | undefined {
148
+ return listSessions(workspacePath, 1)[0];
149
+ }
150
+
151
+ export function createSession(input: {
152
+ workspacePath: string;
153
+ mode: SessionMode;
154
+ goal: string;
155
+ previousSessionId?: string;
156
+ }): SessionEntry {
157
+ const entry: SessionEntry = {
158
+ id: generateSessionId(),
159
+ workspacePath: path.resolve(input.workspacePath),
160
+ mode: input.mode,
161
+ status: "active",
162
+ summary: "",
163
+ lastGoal: input.goal,
164
+ allGoals: [input.goal],
165
+ touchedFiles: [],
166
+ appliedActions: 0,
167
+ rejectedActions: 0,
168
+ transcript: [],
169
+ pendingTasks: [],
170
+ lastAgentResponse: "",
171
+ createdAt: now(),
172
+ updatedAt: now(),
173
+ previousSessionId: input.previousSessionId,
174
+ };
175
+
176
+ const index = readIndex();
177
+ index.sessions.unshift(entry);
178
+ if (index.sessions.length > MAX_SESSIONS) {
179
+ const removed = index.sessions.splice(MAX_SESSIONS);
180
+ for (const r of removed) {
181
+ try {
182
+ fs.unlinkSync(path.join(STORE_DIR, `${r.id}.json`));
183
+ } catch {
184
+ /* ignore */
185
+ }
186
+ }
187
+ }
188
+ writeIndex(index);
189
+ return entry;
190
+ }
191
+
192
+ export function updateSession(
193
+ id: string,
194
+ patch: Partial<Omit<SessionEntry, "id" | "createdAt">>,
195
+ actions?: readonly ActionLog[]
196
+ ): SessionEntry | undefined {
197
+ const index = readIndex();
198
+ const entry = index.sessions.find((s) => s.id === id);
199
+ if (!entry) return undefined;
200
+
201
+ // Merge transcript carefully: cap at TRANSCRIPT_CAP
202
+ if (patch.transcript) {
203
+ const merged = [...entry.transcript, ...patch.transcript];
204
+ patch.transcript = merged.slice(-TRANSCRIPT_CAP);
205
+ }
206
+
207
+ // Merge allGoals without duplicates
208
+ if (patch.lastGoal && !entry.allGoals.includes(patch.lastGoal)) {
209
+ patch.allGoals = [...entry.allGoals, patch.lastGoal];
210
+ }
211
+
212
+ Object.assign(entry, patch, { updatedAt: now() });
213
+ writeIndex(index);
214
+
215
+ if (actions) {
216
+ const historyFile = path.join(STORE_DIR, `${id}.json`);
217
+ atomicWrite(historyFile, JSON.stringify(actions, null, 2));
218
+ }
219
+
220
+ return entry;
221
+ }
222
+
223
+ /**
224
+ * Append transcript messages to an active session without a full patch.
225
+ * More efficient for high-frequency updates during a live session.
226
+ */
227
+ export function appendTranscript(
228
+ id: string,
229
+ messages: TranscriptMessage[]
230
+ ): void {
231
+ const index = readIndex();
232
+ const entry = index.sessions.find((s) => s.id === id);
233
+ if (!entry) return;
234
+ entry.transcript.push(...messages);
235
+ if (entry.transcript.length > TRANSCRIPT_CAP) {
236
+ entry.transcript = entry.transcript.slice(-TRANSCRIPT_CAP);
237
+ }
238
+ entry.updatedAt = now();
239
+ writeIndex(index);
240
+ }
241
+
242
+ export function deleteSession(id: string): boolean {
243
+ const index = readIndex();
244
+ const idx = index.sessions.findIndex((s) => s.id === id);
245
+ if (idx === -1) return false;
246
+ index.sessions.splice(idx, 1);
247
+ writeIndex(index);
248
+ try {
249
+ fs.unlinkSync(path.join(STORE_DIR, `${id}.json`));
250
+ } catch {
251
+ /* ignore */
252
+ }
253
+ return true;
254
+ }
255
+
256
+ export function clearAllSessions(): number {
257
+ const index = readIndex();
258
+ const count = index.sessions.length;
259
+ for (const s of index.sessions) {
260
+ try {
261
+ fs.unlinkSync(path.join(STORE_DIR, `${s.id}.json`));
262
+ } catch {
263
+ /* ignore */
264
+ }
265
+ }
266
+ writeIndex({ version: CURRENT_VERSION, sessions: [], maxSessions: MAX_SESSIONS });
267
+ return count;
268
+ }
269
+
270
+ export function readSessionActions(id: string): ActionLog[] {
271
+ const historyFile = path.join(STORE_DIR, `${id}.json`);
272
+ if (!fs.existsSync(historyFile)) return [];
273
+ try {
274
+ return JSON.parse(fs.readFileSync(historyFile, "utf8"));
275
+ } catch {
276
+ return [];
277
+ }
278
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+ "types": ["bun"],
11
+
12
+ // Bundler mode
13
+ "moduleResolution": "bundler",
14
+ "allowImportingTsExtensions": true,
15
+ "verbatimModuleSyntax": true,
16
+ "noEmit": true,
17
+
18
+ // Best practices
19
+ "strict": true,
20
+ "skipLibCheck": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedIndexedAccess": true,
23
+ "noImplicitOverride": true,
24
+
25
+ // Some stricter flags (disabled by default)
26
+ "noUnusedLocals": false,
27
+ "noUnusedParameters": false,
28
+ "noPropertyAccessFromIndexSignature": false
29
+ }
30
+ }
package/tui/spinner.ts ADDED
@@ -0,0 +1,182 @@
1
+ import chalk from "chalk";
2
+
3
+ // ── Biological Neuro-States ───────────────────────────────────────────────
4
+ const METABOLIC_RATES = {
5
+ HYPER: 45, // Rapid heart rate for immediate tasks
6
+ STEADY: 75, // Cruising breath rhythm
7
+ STRESSED: 110, // Fast, irregular twitching
8
+ HIBERNATE: 180 // Deep, slow dynamic sighing
9
+ };
10
+
11
+ // Shifting structural shapes based on kinetic mood
12
+ const MOODS = {
13
+ PULSE: ["⬖", "⬘", "⬗", "⬙"],
14
+ CRAWL: ["•««««", "»•«««", "»»•««", "»»»•«", "»»»»•", "»»»•«", "»»•««", "»•«««"],
15
+ TWITCH: ["⤜•⤛ ", " ⤜•⤛ ", " ⤜•⤛", " ⤜•⤛ "],
16
+ SIGH: ["⦾🪶 ", " ⦾🪶 ", " ⦾🪶 ", " ⦾🪶", " ⦾🪶 ", " ⦾🪶 "]
17
+ };
18
+
19
+ const C = {
20
+ // Dynamic color gradient engine mappings
21
+ vitality: (pct: number) => {
22
+ // Blends from energetic Violet/Cyan to a stressed Crimson Magenta as fatigue mounts
23
+ if (pct < 0.4) return chalk.bold.hex("#a78bfa"); // Calm Lavender
24
+ if (pct < 0.7) return chalk.bold.hex("#38bdf8"); // Processing Cyan
25
+ if (pct < 0.9) return chalk.bold.hex("#fb7185"); // Agitated Rose
26
+ return chalk.bold.hex("#f43f5e"); // High-Panic Crimson
27
+ },
28
+ text: chalk.hex("#f3f4f6"),
29
+ dim: chalk.hex("#4b5563"),
30
+ success: chalk.bold.hex("#34d399"),
31
+ error: chalk.bold.hex("#ef4444"),
32
+ telemetry: chalk.hex("#60a5fa")
33
+ };
34
+
35
+ function formatElapsed(ms: number): string {
36
+ const s = Math.floor(ms / 1000);
37
+ const dec = Math.floor((ms % 1000) / 100);
38
+ return `${s}.${dec}s`;
39
+ }
40
+
41
+ export interface SpinnerContext {
42
+ updateMessage: (msg: string) => void;
43
+ updateMetric: (metric: string) => void;
44
+ }
45
+
46
+ export interface SpinnerOptions {
47
+ message: string;
48
+ doneMessage?: string;
49
+ failMessage?: string;
50
+ hideTime?: boolean;
51
+ }
52
+
53
+ // ── The Autonomous Organism Engine ────────────────────────────────────────
54
+ class AutonomousLoader {
55
+ private timer: ReturnType<typeof setInterval> | null = null;
56
+ private currentInterval = METABOLIC_RATES.STEADY;
57
+ private tickCount = 0;
58
+ private startTime = 0;
59
+ private message: string;
60
+ private metric = "";
61
+ private readonly showTime: boolean;
62
+
63
+ constructor(message: string, showTime = true) {
64
+ this.message = message;
65
+ this.showTime = showTime;
66
+ }
67
+
68
+ get elapsed(): number {
69
+ return Date.now() - this.startTime;
70
+ }
71
+
72
+ public updateMessage(msg: string) {
73
+ this.message = msg;
74
+ }
75
+
76
+ public updateMetric(metric: string) {
77
+ this.metric = metric;
78
+ }
79
+
80
+ start(): void {
81
+ this.startTime = Date.now();
82
+ this.tickCount = 0;
83
+ process.stdout.write("\u001B[?25l"); // Clean interface focus mode
84
+ this.loop();
85
+ }
86
+
87
+ private loop(): void {
88
+ this.render();
89
+ this.tickCount++;
90
+
91
+ // Calculate a physiological "fatigue index" from 0.0 to 1.0 (Caps at 12 seconds)
92
+ const fatigue = Math.min(this.elapsed / 12000, 1.0);
93
+
94
+ // The heart rate dynamically mutates based on the current system load
95
+ let targetInterval = METABOLIC_RATES.STEADY;
96
+ if (fatigue < 0.15) targetInterval = METABOLIC_RATES.HYPER;
97
+ else if (fatigue > 0.8) targetInterval = METABOLIC_RATES.HIBERNATE;
98
+ else if (fatigue > 0.5) targetInterval = METABOLIC_RATES.STRESSED;
99
+
100
+ // Smooth metabolic shifting to avoid jagged frame skips
101
+ this.currentInterval = Math.round(this.currentInterval * 0.7 + targetInterval * 0.3);
102
+
103
+ this.timer = setTimeout(() => this.loop(), this.currentInterval);
104
+ }
105
+
106
+ private render(): void {
107
+ const ms = this.elapsed;
108
+ const fatigue = Math.min(ms / 12000, 1.0);
109
+
110
+ let shape = "";
111
+
112
+ // Morph structural behavior profiles cleanly
113
+ if (fatigue > 0.8) {
114
+ shape = MOODS.SIGH[Math.floor(this.tickCount / 2) % MOODS.SIGH.length] ?? "";
115
+ } else if (fatigue > 0.5) {
116
+ shape = MOODS.TWITCH[this.tickCount % MOODS.TWITCH.length] ?? "";
117
+ } else if (fatigue > 0.15) {
118
+ shape = MOODS.CRAWL[this.tickCount % MOODS.CRAWL.length] ?? "";
119
+ } else {
120
+ // Heartbeat ambient pulse for immediate executions
121
+ shape = MOODS.PULSE[this.tickCount % MOODS.PULSE.length] ?? "";
122
+ }
123
+
124
+ const telemetry = this.metric ? ` ${C.dim("➔")} ${C.telemetry(this.metric)}` : "";
125
+ const ageIndicator = this.showTime ? ` ${C.dim(`[${formatElapsed(ms)}]`)}` : "";
126
+
127
+ // Build composite biological visual output
128
+ const coreNode = C.vitality(fatigue)(shape);
129
+ const line = ` ${coreNode} ${C.text(this.message)}${telemetry}${ageIndicator}`;
130
+
131
+ const cols = process.stdout.columns || 80;
132
+ process.stdout.write(`\r${"\u001B[K"}${line.slice(0, cols - 1)}`);
133
+ }
134
+
135
+ stop(finalLine: string): void {
136
+ if (this.timer) {
137
+ clearTimeout(this.timer);
138
+ this.timer = null;
139
+ }
140
+ process.stdout.write(`\r${"\u001B[K"}${finalLine}\n\u001B[?25h`);
141
+ }
142
+ }
143
+
144
+ // ── Public API Orchestration ────────────────────────────────────────────
145
+ export async function withSpinner<T>(
146
+ opts: SpinnerOptions,
147
+ task: (ctx: SpinnerContext) => Promise<T>,
148
+ ): Promise<T> {
149
+ const loader = new AutonomousLoader(opts.message, !opts.hideTime);
150
+ loader.start();
151
+
152
+ const ctx: SpinnerContext = {
153
+ updateMessage: (msg) => loader.updateMessage(msg),
154
+ updateMetric: (metric) => loader.updateMetric(metric),
155
+ };
156
+
157
+ try {
158
+ const result = await task(ctx);
159
+ const totalTime = loader.elapsed;
160
+ const elapsedStr = formatElapsed(totalTime);
161
+
162
+ // Celebratory reactive state: entity expresses joy/relief depending on runtime friction
163
+ let endingIcon = "🌱";
164
+ if (totalTime < 800) endingIcon = "⚡";
165
+ else if (totalTime > 6000) endingIcon = "🧘";
166
+
167
+ const done = opts.doneMessage
168
+ ? ` ${endingIcon} ${C.success("●")} ${C.text(opts.message)} ${C.dim(`— ${opts.doneMessage}`)} ${C.dim(`(${elapsedStr})`)}`
169
+ : ` ${endingIcon} ${C.success("●")} ${C.text(opts.message)} ${C.dim(`(${elapsedStr})`)}`;
170
+
171
+ loader.stop(done);
172
+ return result;
173
+ } catch (e) {
174
+ const elapsedStr = formatElapsed(loader.elapsed);
175
+ const fail = opts.failMessage
176
+ ? ` 🌋 ${C.error("✘")} ${C.text(opts.message)} ${C.dim(`— ${opts.failMessage}`)} ${C.dim(`(${elapsedStr})`)}`
177
+ : ` 🌋 ${C.error("✘")} ${C.text(opts.message)} ${C.dim(`— system rupture (${elapsedStr})`)}`;
178
+
179
+ loader.stop(fail);
180
+ throw e;
181
+ }
182
+ }
@@ -0,0 +1,17 @@
1
+ import { marked } from "marked";
2
+ import {markedTerminal} from 'marked-terminal'
3
+
4
+ let ready = false
5
+
6
+ function ensureMarked(): void {
7
+ if (ready) return
8
+ const w = Math.max(40, Math.min(process.stdout.columns || 80,120))
9
+ //@ts-ignore
10
+ marked.use(markedTerminal({width:w, reflowText: true}, {}))
11
+ ready = true
12
+ }
13
+
14
+ export function renderTerminalMarkdown(source:string):string{
15
+ ensureMarked()
16
+ return marked.parse(source.trimEnd(), {async:false})
17
+ }