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.
- package/README.md +411 -0
- package/ai/ai.config.ts +27 -0
- package/ai/auto-retry.ts +117 -0
- package/ai/config-loader.ts +132 -0
- package/ai/index.ts +4 -0
- package/ai/retry-prompt.ts +30 -0
- package/bin/astra +2 -0
- package/core/retry/error-classifier.ts +208 -0
- package/core/retry/index.ts +29 -0
- package/core/retry/retry-config.ts +142 -0
- package/core/retry/retry-engine.ts +215 -0
- package/game/index.html +573 -0
- package/game/neon-breaker.html +1037 -0
- package/index.ts +140 -0
- package/modes/agent/action-tracker.ts +47 -0
- package/modes/agent/agent-tools.ts +338 -0
- package/modes/agent/approval.ts +184 -0
- package/modes/agent/diff-view.ts +34 -0
- package/modes/agent/orchestrator.ts +234 -0
- package/modes/agent/tool-executor.ts +993 -0
- package/modes/agent/types.ts +68 -0
- package/modes/ask/orchestrator.ts +230 -0
- package/modes/auto.ts +88 -0
- package/modes/cli.ts +43 -0
- package/modes/multi/agent-pool-manager.ts +337 -0
- package/modes/multi/examples.ts +441 -0
- package/modes/multi/message-broker.ts +179 -0
- package/modes/multi/multi-agent-orchestrator.ts +891 -0
- package/modes/multi/orchestrator.ts +414 -0
- package/modes/multi/types.ts +245 -0
- package/modes/multi/workflow-builder.ts +569 -0
- package/modes/plan/orchestrator.ts +198 -0
- package/modes/plan/planner.ts +121 -0
- package/modes/plan/selection.ts +43 -0
- package/modes/plan/types.ts +13 -0
- package/modes/plan/web-tools.ts +132 -0
- package/modes/setup.ts +210 -0
- package/package.json +62 -0
- package/session/index.ts +45 -0
- package/session/session-context.ts +188 -0
- package/session/session-manager.ts +374 -0
- package/session/session-tools.ts +109 -0
- package/session/store.ts +278 -0
- package/tsconfig.json +30 -0
- package/tui/spinner.ts +182 -0
- package/tui/terminal-md.ts +17 -0
- package/tui/wakeup.ts +231 -0
package/session/store.ts
ADDED
|
@@ -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
|
+
}
|