@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.
@@ -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: {"content-type": "application/json"},
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: {"content-type": "application/json"},
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
- cmd = `sox -q -d -r 16000 -c 1 -b 16 "${outPath}" trim 0 ${seconds}`;
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
- cmd = `arecord -q -f S16_LE -r 16000 -c 1 -d ${Math.ceil(seconds)} "${outPath}"`;
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
- handlers.onError(error instanceof Error ? error : new Error(String(error)));
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});
@@ -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};