@astra-code/astra-ai 0.1.0 → 0.1.1
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/.env.example +3 -1
- package/README.md +1 -1
- package/dist/app/App.js +303 -50
- package/dist/app/App.js.map +1 -1
- package/dist/lib/backendClient.js +39 -2
- package/dist/lib/backendClient.js.map +1 -1
- package/dist/lib/voice.js +38 -3
- package/dist/lib/voice.js.map +1 -1
- package/package.json +1 -1
- package/src/app/App.tsx +409 -53
- package/src/lib/backendClient.ts +41 -2
- package/src/lib/voice.ts +40 -3
- package/src/types/events.ts +11 -2
package/src/lib/backendClient.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "./config.js";
|
|
8
8
|
import type {AgentEvent, AuthSession, ChatMessage} from "../types/events.js";
|
|
9
9
|
import type {WorkspaceFile} from "./workspaceScanner.js";
|
|
10
|
+
import {loadSession} from "./sessionStore.js";
|
|
10
11
|
|
|
11
12
|
type JsonRecord = Record<string, unknown>;
|
|
12
13
|
|
|
@@ -20,9 +21,15 @@ export type SessionSummary = {
|
|
|
20
21
|
|
|
21
22
|
export class BackendClient {
|
|
22
23
|
private readonly baseUrl: string;
|
|
24
|
+
private authToken: string | null;
|
|
23
25
|
|
|
24
26
|
public constructor(baseUrl = getBackendUrl()) {
|
|
25
27
|
this.baseUrl = baseUrl;
|
|
28
|
+
this.authToken = loadSession()?.access_token?.trim() || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public setAuthSession(session: AuthSession | null): void {
|
|
32
|
+
this.authToken = session?.access_token?.trim() || null;
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
public async get(path: string, params?: Record<string, string>): Promise<JsonRecord> {
|
|
@@ -68,6 +75,30 @@ export class BackendClient {
|
|
|
68
75
|
}
|
|
69
76
|
}
|
|
70
77
|
|
|
78
|
+
public async getUserProfile(session: AuthSession): Promise<AuthSession> {
|
|
79
|
+
const data = await this.get("/api/user/profile", {
|
|
80
|
+
user_id: session.user_id,
|
|
81
|
+
org_id: session.org_id
|
|
82
|
+
});
|
|
83
|
+
const profile = data as Record<string, unknown>;
|
|
84
|
+
const merged: AuthSession = {
|
|
85
|
+
...session,
|
|
86
|
+
};
|
|
87
|
+
const email = typeof profile.email === "string" && profile.email ? profile.email : session.email;
|
|
88
|
+
const role = typeof profile.role === "string" ? profile.role : session.role;
|
|
89
|
+
const displayName = typeof profile.display_name === "string" ? profile.display_name : session.display_name;
|
|
90
|
+
if (email) {
|
|
91
|
+
merged.email = email;
|
|
92
|
+
}
|
|
93
|
+
if (role) {
|
|
94
|
+
merged.role = role;
|
|
95
|
+
}
|
|
96
|
+
if (displayName) {
|
|
97
|
+
merged.display_name = displayName;
|
|
98
|
+
}
|
|
99
|
+
return merged;
|
|
100
|
+
}
|
|
101
|
+
|
|
71
102
|
public async ensureSessionId(
|
|
72
103
|
user: AuthSession,
|
|
73
104
|
existingSessionId: string | null,
|
|
@@ -185,9 +216,13 @@ export class BackendClient {
|
|
|
185
216
|
}
|
|
186
217
|
|
|
187
218
|
private async request(url: string, method: string, payload?: JsonRecord): Promise<JsonRecord> {
|
|
219
|
+
const headers: Record<string, string> = {"content-type": "application/json"};
|
|
220
|
+
if (this.authToken) {
|
|
221
|
+
headers.authorization = `Bearer ${this.authToken}`;
|
|
222
|
+
}
|
|
188
223
|
const response = await fetch(url, {
|
|
189
224
|
method,
|
|
190
|
-
headers
|
|
225
|
+
headers,
|
|
191
226
|
body: payload ? JSON.stringify(payload) : null
|
|
192
227
|
});
|
|
193
228
|
|
|
@@ -204,9 +239,13 @@ export class BackendClient {
|
|
|
204
239
|
}
|
|
205
240
|
|
|
206
241
|
private async *streamSse(path: string, payload: JsonRecord): AsyncGenerator<AgentEvent> {
|
|
242
|
+
const headers: Record<string, string> = {"content-type": "application/json"};
|
|
243
|
+
if (this.authToken) {
|
|
244
|
+
headers.authorization = `Bearer ${this.authToken}`;
|
|
245
|
+
}
|
|
207
246
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
208
247
|
method: "POST",
|
|
209
|
-
headers
|
|
248
|
+
headers,
|
|
210
249
|
body: JSON.stringify(payload)
|
|
211
250
|
});
|
|
212
251
|
|
package/src/lib/voice.ts
CHANGED
|
@@ -10,6 +10,11 @@ const DEFAULT_CHUNK_SECONDS = Number(process.env.ASTRA_STT_CHUNK_SECONDS ?? "2.5
|
|
|
10
10
|
|
|
11
11
|
const safeText = (text: string): string => text.replace(/\s+/g, " ").trim().slice(0, VOICE_TEXT_LIMIT);
|
|
12
12
|
|
|
13
|
+
const commandExists = (binary: string): boolean => {
|
|
14
|
+
const probe = spawnSync("bash", ["-lc", `command -v ${binary}`], {stdio: "ignore"});
|
|
15
|
+
return probe.status === 0;
|
|
16
|
+
};
|
|
17
|
+
|
|
13
18
|
const runShell = async (command: string): Promise<void> =>
|
|
14
19
|
new Promise((resolve, reject) => {
|
|
15
20
|
const child = spawn(command, {shell: true, stdio: "ignore"});
|
|
@@ -30,9 +35,24 @@ const captureAudioChunk = async (seconds: number): Promise<string> => {
|
|
|
30
35
|
let cmd = custom ?? "";
|
|
31
36
|
if (!cmd) {
|
|
32
37
|
if (process.platform === "darwin") {
|
|
33
|
-
|
|
38
|
+
// Prefer ffmpeg on macOS (works on Apple Silicon/Homebrew setups).
|
|
39
|
+
if (commandExists("ffmpeg")) {
|
|
40
|
+
cmd = `ffmpeg -hide_banner -loglevel error -f avfoundation -i ":0" -ar 16000 -ac 1 -t ${seconds} "${outPath}"`;
|
|
41
|
+
} else if (commandExists("rec")) {
|
|
42
|
+
cmd = `rec -q -r 16000 -c 1 "${outPath}" trim 0 ${seconds}`;
|
|
43
|
+
} else {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"No macOS recorder found. Install ffmpeg (recommended) or sox (`rec`), or set ASTRA_STT_CAPTURE_COMMAND."
|
|
46
|
+
);
|
|
47
|
+
}
|
|
34
48
|
} else if (process.platform === "linux") {
|
|
35
|
-
|
|
49
|
+
if (commandExists("arecord")) {
|
|
50
|
+
cmd = `arecord -q -f S16_LE -r 16000 -c 1 -d ${Math.ceil(seconds)} "${outPath}"`;
|
|
51
|
+
} else if (commandExists("ffmpeg")) {
|
|
52
|
+
cmd = `ffmpeg -hide_banner -loglevel error -f pulse -i default -ar 16000 -ac 1 -t ${seconds} "${outPath}"`;
|
|
53
|
+
} else {
|
|
54
|
+
throw new Error("No Linux recorder found. Install arecord/alsa-utils or ffmpeg, or set ASTRA_STT_CAPTURE_COMMAND.");
|
|
55
|
+
}
|
|
36
56
|
} else {
|
|
37
57
|
throw new Error("No default audio capture command for this platform. Set ASTRA_STT_CAPTURE_COMMAND.");
|
|
38
58
|
}
|
|
@@ -135,6 +155,7 @@ export const startLiveTranscription = (handlers: {
|
|
|
135
155
|
let running = true;
|
|
136
156
|
let stopping = false;
|
|
137
157
|
let transcript = "";
|
|
158
|
+
let consecutiveErrors = 0;
|
|
138
159
|
let finishedResolve: (() => void) | null = null;
|
|
139
160
|
const finished = new Promise<void>((resolve) => {
|
|
140
161
|
finishedResolve = resolve;
|
|
@@ -146,12 +167,28 @@ export const startLiveTranscription = (handlers: {
|
|
|
146
167
|
try {
|
|
147
168
|
audioPath = await captureAudioChunk(DEFAULT_CHUNK_SECONDS);
|
|
148
169
|
const piece = await transcribeAudioFile(audioPath);
|
|
170
|
+
consecutiveErrors = 0;
|
|
149
171
|
if (piece) {
|
|
150
172
|
transcript = transcript ? `${transcript} ${piece}` : piece;
|
|
151
173
|
handlers.onPartial(transcript);
|
|
152
174
|
}
|
|
153
175
|
} catch (error) {
|
|
154
|
-
|
|
176
|
+
consecutiveErrors += 1;
|
|
177
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
178
|
+
if (/Command failed \(127\)/.test(err.message)) {
|
|
179
|
+
handlers.onError(
|
|
180
|
+
new Error(
|
|
181
|
+
"Audio capture tool not found. Install ffmpeg/rec (macOS) or arecord/ffmpeg (Linux), or set ASTRA_STT_CAPTURE_COMMAND."
|
|
182
|
+
)
|
|
183
|
+
);
|
|
184
|
+
running = false;
|
|
185
|
+
} else {
|
|
186
|
+
handlers.onError(err);
|
|
187
|
+
// Stop error spam on persistent capture/transcription failures.
|
|
188
|
+
if (consecutiveErrors >= 2) {
|
|
189
|
+
running = false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
155
192
|
} finally {
|
|
156
193
|
if (audioPath) {
|
|
157
194
|
await rm(audioPath, {force: true});
|
package/src/types/events.ts
CHANGED
|
@@ -2,6 +2,9 @@ export type AuthSession = {
|
|
|
2
2
|
user_id: string;
|
|
3
3
|
org_id: string;
|
|
4
4
|
email?: string;
|
|
5
|
+
role?: string;
|
|
6
|
+
display_name?: string;
|
|
7
|
+
access_token?: string;
|
|
5
8
|
[key: string]: unknown;
|
|
6
9
|
};
|
|
7
10
|
|
|
@@ -13,14 +16,19 @@ export type ChatMessage = {
|
|
|
13
16
|
export type AgentEvent =
|
|
14
17
|
| {type: "text"; content?: string}
|
|
15
18
|
| {type: "thinking"; content?: string}
|
|
16
|
-
| {type: "tool_start"; tool?: {name?: string}}
|
|
19
|
+
| {type: "tool_start"; tool?: {name?: string; id?: string; arguments?: Record<string, unknown>}}
|
|
20
|
+
| {type: "file_edit_start"; tool_id?: string; tool_name?: string; data?: {tool_id?: string; tool_name?: string}}
|
|
17
21
|
| {
|
|
18
22
|
type: "tool_result";
|
|
19
23
|
success?: boolean;
|
|
20
24
|
tool_name?: string;
|
|
25
|
+
result_type?: string;
|
|
26
|
+
tool_call_id?: string;
|
|
21
27
|
data?: Record<string, unknown>;
|
|
22
28
|
error?: string;
|
|
23
29
|
}
|
|
30
|
+
| {type: "file_edit_path"; tool_id?: string; path?: string; is_create?: boolean; tool_name?: string; data?: {tool_id?: string; path?: string; is_create?: boolean; tool_name?: string}}
|
|
31
|
+
| {type: "file_edit_delta"; tool_id?: string; content?: string; data?: {tool_id?: string}}
|
|
24
32
|
| {
|
|
25
33
|
type: "run_in_terminal";
|
|
26
34
|
terminal_id?: string;
|
|
@@ -30,4 +38,5 @@ export type AgentEvent =
|
|
|
30
38
|
}
|
|
31
39
|
| {type: "credits_update"; remaining?: number; cost?: number}
|
|
32
40
|
| {type: "credits_exhausted"; message?: string}
|
|
33
|
-
| {type: "error"; error?: string; content?: string}
|
|
41
|
+
| {type: "error"; error?: string; content?: string}
|
|
42
|
+
| {type: string; [key: string]: unknown};
|