@iaforged/context-code 1.1.3 → 1.1.5
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 +32 -8
- package/dist/src/commands/init.js +91 -219
- package/dist/src/commands/voice/index.js +6 -7
- package/dist/src/commands/voice/voice.js +87 -43
- package/dist/src/commands.js +1 -3
- package/dist/src/components/LogoV2/VoiceModeNotice.js +1 -1
- package/dist/src/components/PromptInput/VoiceIndicator.js +4 -4
- package/dist/src/components/Spinner.js +18 -18
- package/dist/src/constants/spinnerVerbs.js +9 -9
- package/dist/src/hooks/usePasteHandler.js +8 -8
- package/dist/src/hooks/useVoice.js +87 -804
- package/dist/src/hooks/useVoiceEnabled.js +3 -15
- package/dist/src/hooks/useVoiceIntegration.js +6 -25
- package/dist/src/keybindings/defaultBindings.js +9 -6
- package/dist/src/screens/REPL.js +10 -22
- package/dist/src/services/localDictation.js +377 -0
- package/dist/src/services/voice.js +9 -7
- package/dist/src/state/AppState.js +1 -3
- package/dist/src/tools/ConfigTool/ConfigTool.js +12 -15
- package/dist/src/tools/ConfigTool/supportedSettings.js +2 -2
- package/dist/src/utils/imagePaste.js +11 -5
- package/dist/src/utils/settings/types.js +2 -2
- package/dist/src/voice/voiceModeEnabled.js +5 -25
- package/dist/vendor/audio-capture/arm64-darwin/audio-capture.node +0 -0
- package/dist/vendor/audio-capture/arm64-linux/audio-capture.node +0 -0
- package/dist/vendor/audio-capture/arm64-win32/audio-capture.node +0 -0
- package/dist/vendor/audio-capture/x64-darwin/audio-capture.node +0 -0
- package/dist/vendor/audio-capture/x64-linux/audio-capture.node +0 -0
- package/dist/vendor/audio-capture/x64-win32/audio-capture.node +0 -0
- package/dist/vendor/audio-capture-src/index.js +114 -0
- package/dist/vendor/audio-capture-src/index.ts +155 -0
- package/docs/comandos.md +132 -121
- package/package.json +1 -1
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { feature } from '../recovery/bunBundleShim.js';
|
|
2
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
2
|
import { createRequire } from 'module';
|
|
4
3
|
const require = createRequire(import.meta.url);
|
|
@@ -10,9 +9,8 @@ import { logForDebugging } from '../utils/debug.js';
|
|
|
10
9
|
import { createDisabledBypassPermissionsContext, isBypassPermissionsModeDisabled } from '../utils/permissions/permissionSetup.js';
|
|
11
10
|
import { applySettingsChange } from '../utils/settings/applySettingsChange.js';
|
|
12
11
|
import { createStore } from './store.js';
|
|
13
|
-
// DCE: voice context is ant-only. External builds get a passthrough.
|
|
14
12
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
15
|
-
const VoiceProvider =
|
|
13
|
+
const VoiceProvider = require('../context/voice.js').VoiceProvider;
|
|
16
14
|
/* eslint-enable @typescript-eslint/no-require-imports */
|
|
17
15
|
import { getDefaultAppState } from './AppStateStore.js';
|
|
18
16
|
// TODO: Remove these re-exports once all callers import directly from
|
|
@@ -190,33 +190,31 @@ export const ConfigTool = buildTool({
|
|
|
190
190
|
finalValue === true) {
|
|
191
191
|
const { isVoiceModeEnabled } = await import('../../voice/voiceModeEnabled.js');
|
|
192
192
|
if (!isVoiceModeEnabled()) {
|
|
193
|
-
const { isAnthropicAuthEnabled } = await import('../../utils/auth.js');
|
|
194
193
|
return {
|
|
195
194
|
data: {
|
|
196
195
|
success: false,
|
|
197
|
-
error:
|
|
198
|
-
? 'Voice mode requires a Claude.ai account. Please run /login to sign in.'
|
|
199
|
-
: 'Voice mode is not available.',
|
|
196
|
+
error: 'El modo de dictado no esta disponible.',
|
|
200
197
|
},
|
|
201
198
|
};
|
|
202
199
|
}
|
|
203
|
-
const { isVoiceStreamAvailable } = await import('../../services/voiceStreamSTT.js');
|
|
204
200
|
const { checkRecordingAvailability, checkVoiceDependencies, requestMicrophonePermission, } = await import('../../services/voice.js');
|
|
201
|
+
const { checkLocalDictationConfiguration } = await import('../../services/localDictation.js');
|
|
205
202
|
const recording = await checkRecordingAvailability();
|
|
206
203
|
if (!recording.available) {
|
|
207
204
|
return {
|
|
208
205
|
data: {
|
|
209
206
|
success: false,
|
|
210
207
|
error: recording.reason ??
|
|
211
|
-
'
|
|
208
|
+
'El modo de dictado no esta disponible en este entorno.',
|
|
212
209
|
},
|
|
213
210
|
};
|
|
214
211
|
}
|
|
215
|
-
|
|
212
|
+
const dictation = await checkLocalDictationConfiguration();
|
|
213
|
+
if (!dictation.available) {
|
|
216
214
|
return {
|
|
217
215
|
data: {
|
|
218
216
|
success: false,
|
|
219
|
-
error:
|
|
217
|
+
error: dictation.error ?? 'El dictado local no esta configurado.',
|
|
220
218
|
},
|
|
221
219
|
};
|
|
222
220
|
}
|
|
@@ -225,27 +223,26 @@ export const ConfigTool = buildTool({
|
|
|
225
223
|
return {
|
|
226
224
|
data: {
|
|
227
225
|
success: false,
|
|
228
|
-
error: 'No
|
|
229
|
-
(deps.installCommand ? `
|
|
226
|
+
error: 'No se encontro una herramienta de grabacion de audio.' +
|
|
227
|
+
(deps.installCommand ? ` Ejecuta: ${deps.installCommand}` : ''),
|
|
230
228
|
},
|
|
231
229
|
};
|
|
232
230
|
}
|
|
233
231
|
if (!(await requestMicrophonePermission())) {
|
|
234
232
|
let guidance;
|
|
235
233
|
if (process.platform === 'win32') {
|
|
236
|
-
guidance = '
|
|
234
|
+
guidance = 'Configuracion > Privacidad > Microfono';
|
|
237
235
|
}
|
|
238
236
|
else if (process.platform === 'linux') {
|
|
239
|
-
guidance =
|
|
237
|
+
guidance = 'la configuracion de audio del sistema';
|
|
240
238
|
}
|
|
241
239
|
else {
|
|
242
|
-
guidance =
|
|
243
|
-
'System Settings \u2192 Privacy & Security \u2192 Microphone';
|
|
240
|
+
guidance = 'System Settings > Privacy & Security > Microphone';
|
|
244
241
|
}
|
|
245
242
|
return {
|
|
246
243
|
data: {
|
|
247
244
|
success: false,
|
|
248
|
-
error: `
|
|
245
|
+
error: `El acceso al microfono esta denegado. Habilitalo en ${guidance} y vuelve a intentar.`,
|
|
249
246
|
},
|
|
250
247
|
};
|
|
251
248
|
}
|
|
@@ -99,7 +99,7 @@ export const SUPPORTED_SETTINGS = {
|
|
|
99
99
|
language: {
|
|
100
100
|
source: 'settings',
|
|
101
101
|
type: 'string',
|
|
102
|
-
description: '
|
|
102
|
+
description: 'Idioma preferido para las respuestas de Context y el dictado local (por ejemplo, "japanese", "spanish")',
|
|
103
103
|
},
|
|
104
104
|
teammateMode: {
|
|
105
105
|
source: 'global',
|
|
@@ -121,7 +121,7 @@ export const SUPPORTED_SETTINGS = {
|
|
|
121
121
|
voiceEnabled: {
|
|
122
122
|
source: 'settings',
|
|
123
123
|
type: 'boolean',
|
|
124
|
-
description: '
|
|
124
|
+
description: 'Activar dictado local (mantener pulsado para hablar)',
|
|
125
125
|
},
|
|
126
126
|
}
|
|
127
127
|
: {}),
|
|
@@ -25,6 +25,7 @@ function getClipboardCommands() {
|
|
|
25
25
|
win32: join(baseTmpDir, screenshotFilename),
|
|
26
26
|
};
|
|
27
27
|
const screenshotPath = tempPaths[platform] || tempPaths.linux;
|
|
28
|
+
const escapedScreenshotPath = screenshotPath.replace(/'/g, "''");
|
|
28
29
|
// Platform-specific clipboard commands
|
|
29
30
|
const commands = {
|
|
30
31
|
darwin: {
|
|
@@ -40,10 +41,10 @@ function getClipboardCommands() {
|
|
|
40
41
|
deleteFile: `rm -f "${screenshotPath}"`,
|
|
41
42
|
},
|
|
42
43
|
win32: {
|
|
43
|
-
checkImage: 'powershell -NoProfile -Command "
|
|
44
|
-
saveImage: `powershell -NoProfile -Command "
|
|
45
|
-
getPath: 'powershell -NoProfile -Command "Get-Clipboard"',
|
|
46
|
-
deleteFile: `
|
|
44
|
+
checkImage: 'powershell -NoProfile -STA -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Clipboard]::ContainsImage()"',
|
|
45
|
+
saveImage: `powershell -NoProfile -STA -Command "Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img -ne $null) { $img.Save('${escapedScreenshotPath}', [System.Drawing.Imaging.ImageFormat]::Png) } else { exit 1 }"`,
|
|
46
|
+
getPath: 'powershell -NoProfile -Command "Get-Clipboard -Raw"',
|
|
47
|
+
deleteFile: `powershell -NoProfile -Command "Remove-Item -LiteralPath '${escapedScreenshotPath}' -Force -ErrorAction SilentlyContinue"`,
|
|
47
48
|
},
|
|
48
49
|
};
|
|
49
50
|
return {
|
|
@@ -56,7 +57,12 @@ function getClipboardCommands() {
|
|
|
56
57
|
*/
|
|
57
58
|
export async function hasImageInClipboard() {
|
|
58
59
|
if (process.platform !== 'darwin') {
|
|
59
|
-
|
|
60
|
+
const { commands } = getClipboardCommands();
|
|
61
|
+
const result = await execa(commands.checkImage, {
|
|
62
|
+
shell: true,
|
|
63
|
+
reject: false,
|
|
64
|
+
});
|
|
65
|
+
return result.exitCode === 0 && result.stdout.trim().toLowerCase() !== 'false';
|
|
60
66
|
}
|
|
61
67
|
if (feature('NATIVE_CLIPBOARD_IMAGE') &&
|
|
62
68
|
getFeatureValue_CACHED_MAY_BE_STALE('tengu_collage_kaleidoscope', true)) {
|
|
@@ -501,7 +501,7 @@ export const SettingsSchema = lazySchema(() => z
|
|
|
501
501
|
language: z
|
|
502
502
|
.string()
|
|
503
503
|
.optional()
|
|
504
|
-
.describe('
|
|
504
|
+
.describe('Idioma preferido para las respuestas de Context y el dictado local (por ejemplo, "japanese", "spanish")'),
|
|
505
505
|
skipWebFetchPreflight: z
|
|
506
506
|
.boolean()
|
|
507
507
|
.optional()
|
|
@@ -666,7 +666,7 @@ export const SettingsSchema = lazySchema(() => z
|
|
|
666
666
|
voiceEnabled: z
|
|
667
667
|
.boolean()
|
|
668
668
|
.optional()
|
|
669
|
-
.describe('
|
|
669
|
+
.describe('Activar dictado local (mantener pulsado para hablar)'),
|
|
670
670
|
}
|
|
671
671
|
: {}),
|
|
672
672
|
...(feature('KAIROS')
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { feature } from '../recovery/bunBundleShim.js';
|
|
2
1
|
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js';
|
|
3
|
-
import { getClaudeAIOAuthTokens, isAnthropicAuthEnabled, } from '../utils/auth.js';
|
|
4
2
|
/**
|
|
5
3
|
* Kill-switch check for voice mode. Returns true unless the
|
|
6
4
|
* `tengu_amber_quartz_disabled` GrowthBook flag is flipped on (emergency
|
|
@@ -10,32 +8,14 @@ import { getClaudeAIOAuthTokens, isAnthropicAuthEnabled, } from '../utils/auth.j
|
|
|
10
8
|
* should be *visible* (e.g., command registration, config UI).
|
|
11
9
|
*/
|
|
12
10
|
export function isVoiceGrowthBookEnabled() {
|
|
13
|
-
|
|
14
|
-
// Negative pattern (if (!feature(...)) return) does not eliminate
|
|
15
|
-
// inline string literals from external builds.
|
|
16
|
-
return feature('VOICE_MODE')
|
|
17
|
-
? !getFeatureValue_CACHED_MAY_BE_STALE('tengu_amber_quartz_disabled', false)
|
|
18
|
-
: false;
|
|
11
|
+
return !getFeatureValue_CACHED_MAY_BE_STALE('tengu_amber_quartz_disabled', false);
|
|
19
12
|
}
|
|
20
13
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* first call spawns `security` on macOS (~20-50ms), subsequent calls are
|
|
24
|
-
* cache hits. The memoize clears on token refresh (~once/hour), so one
|
|
25
|
-
* cold spawn per refresh is expected. Cheap enough for usage-time checks.
|
|
14
|
+
* Keep this helper for compatibility with the existing voice hooks. Local
|
|
15
|
+
* dictation no longer depends on Claude.ai auth.
|
|
26
16
|
*/
|
|
27
17
|
export function hasVoiceAuth() {
|
|
28
|
-
|
|
29
|
-
// endpoint on claude.ai which is not available with API keys,
|
|
30
|
-
// Bedrock, Vertex, or Foundry.
|
|
31
|
-
if (!isAnthropicAuthEnabled()) {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
// isAnthropicAuthEnabled only checks the auth *provider*, not whether
|
|
35
|
-
// a token exists. Without this check, the voice UI renders but
|
|
36
|
-
// connectVoiceStream fails silently when the user isn't logged in.
|
|
37
|
-
const tokens = getClaudeAIOAuthTokens();
|
|
38
|
-
return Boolean(tokens?.accessToken);
|
|
18
|
+
return true;
|
|
39
19
|
}
|
|
40
20
|
/**
|
|
41
21
|
* Full runtime check: auth + GrowthBook kill-switch. Callers: `/voice`
|
|
@@ -44,5 +24,5 @@ export function hasVoiceAuth() {
|
|
|
44
24
|
* paths use useVoiceEnabled() instead (memoizes the auth half).
|
|
45
25
|
*/
|
|
46
26
|
export function isVoiceModeEnabled() {
|
|
47
|
-
return
|
|
27
|
+
return isVoiceGrowthBookEnabled();
|
|
48
28
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
let cachedModule = null;
|
|
4
|
+
let loadAttempted = false;
|
|
5
|
+
function loadModule() {
|
|
6
|
+
if (loadAttempted) {
|
|
7
|
+
return cachedModule;
|
|
8
|
+
}
|
|
9
|
+
loadAttempted = true;
|
|
10
|
+
// Supported platforms: macOS (darwin), Linux, Windows (win32)
|
|
11
|
+
const platform = process.platform;
|
|
12
|
+
if (platform !== 'darwin' && platform !== 'linux' && platform !== 'win32') {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
// Candidate 1: native-embed path (bun compile). AUDIO_CAPTURE_NODE_PATH is
|
|
16
|
+
// defined at build time in build-with-plugins.ts for native builds only — the
|
|
17
|
+
// define resolves it to the static literal "../../audio-capture.node" so bun
|
|
18
|
+
// compile can rewrite it to /$bunfs/root/audio-capture.node. MUST stay a
|
|
19
|
+
// direct require(env var) — bun cannot analyze require(variable) from a loop.
|
|
20
|
+
if (process.env.AUDIO_CAPTURE_NODE_PATH) {
|
|
21
|
+
try {
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
23
|
+
cachedModule = require(process.env.AUDIO_CAPTURE_NODE_PATH);
|
|
24
|
+
return cachedModule;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// fall through to runtime fallbacks below
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Candidates 2/3: npm-install and dev/source layouts. Dynamic require is
|
|
31
|
+
// fine here — in bundled output (node --target build) require() resolves at
|
|
32
|
+
// runtime relative to cli.js at the package root; in dev it resolves
|
|
33
|
+
// relative to this file (vendor/audio-capture-src/index.ts).
|
|
34
|
+
const platformDir = `${process.arch}-${platform}`;
|
|
35
|
+
const fallbacks = [
|
|
36
|
+
`./vendor/audio-capture/${platformDir}/audio-capture.node`,
|
|
37
|
+
`../audio-capture/${platformDir}/audio-capture.node`,
|
|
38
|
+
];
|
|
39
|
+
for (const p of fallbacks) {
|
|
40
|
+
try {
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
42
|
+
cachedModule = require(p);
|
|
43
|
+
return cachedModule;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// try next
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
export function isNativeAudioAvailable() {
|
|
52
|
+
return loadModule() !== null;
|
|
53
|
+
}
|
|
54
|
+
export function startNativeRecording(onData, onEnd) {
|
|
55
|
+
const mod = loadModule();
|
|
56
|
+
if (!mod) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return mod.startRecording(onData, onEnd);
|
|
60
|
+
}
|
|
61
|
+
export function stopNativeRecording() {
|
|
62
|
+
const mod = loadModule();
|
|
63
|
+
if (!mod) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
mod.stopRecording();
|
|
67
|
+
}
|
|
68
|
+
export function isNativeRecordingActive() {
|
|
69
|
+
const mod = loadModule();
|
|
70
|
+
if (!mod) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return mod.isRecording();
|
|
74
|
+
}
|
|
75
|
+
export function startNativePlayback(sampleRate, channels) {
|
|
76
|
+
const mod = loadModule();
|
|
77
|
+
if (!mod) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return mod.startPlayback(sampleRate, channels);
|
|
81
|
+
}
|
|
82
|
+
export function writeNativePlaybackData(data) {
|
|
83
|
+
const mod = loadModule();
|
|
84
|
+
if (!mod) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
mod.writePlaybackData(data);
|
|
88
|
+
}
|
|
89
|
+
export function stopNativePlayback() {
|
|
90
|
+
const mod = loadModule();
|
|
91
|
+
if (!mod) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
mod.stopPlayback();
|
|
95
|
+
}
|
|
96
|
+
export function isNativePlaying() {
|
|
97
|
+
const mod = loadModule();
|
|
98
|
+
if (!mod) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return mod.isPlaying();
|
|
102
|
+
}
|
|
103
|
+
// Returns the microphone authorization status.
|
|
104
|
+
// On macOS, returns the TCC status: 0=notDetermined, 1=restricted, 2=denied, 3=authorized.
|
|
105
|
+
// On Linux, always returns 3 (authorized) — no system-level mic permission API.
|
|
106
|
+
// On Windows, returns 3 (authorized) if registry key absent or allowed, 2 (denied) if explicitly denied.
|
|
107
|
+
// Returns 0 (notDetermined) if the native module is unavailable.
|
|
108
|
+
export function microphoneAuthorizationStatus() {
|
|
109
|
+
const mod = loadModule();
|
|
110
|
+
if (!mod || !mod.microphoneAuthorizationStatus) {
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
return mod.microphoneAuthorizationStatus();
|
|
114
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
|
|
2
|
+
import { createRequire } from 'module'
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url)
|
|
5
|
+
|
|
6
|
+
type AudioCaptureNapi = {
|
|
7
|
+
startRecording(
|
|
8
|
+
onData: (data: Buffer) => void,
|
|
9
|
+
onEnd: () => void,
|
|
10
|
+
): boolean
|
|
11
|
+
stopRecording(): void
|
|
12
|
+
isRecording(): boolean
|
|
13
|
+
startPlayback(sampleRate: number, channels: number): boolean
|
|
14
|
+
writePlaybackData(data: Buffer): void
|
|
15
|
+
stopPlayback(): void
|
|
16
|
+
isPlaying(): boolean
|
|
17
|
+
// TCC microphone authorization status (macOS only):
|
|
18
|
+
// 0 = notDetermined, 1 = restricted, 2 = denied, 3 = authorized.
|
|
19
|
+
// Linux: always returns 3 (authorized) — no system-level microphone permission API.
|
|
20
|
+
// Windows: returns 3 (authorized) if registry key absent or allowed,
|
|
21
|
+
// 2 (denied) if microphone access is explicitly denied.
|
|
22
|
+
microphoneAuthorizationStatus?(): number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let cachedModule: AudioCaptureNapi | null = null
|
|
26
|
+
let loadAttempted = false
|
|
27
|
+
|
|
28
|
+
function loadModule(): AudioCaptureNapi | null {
|
|
29
|
+
if (loadAttempted) {
|
|
30
|
+
return cachedModule
|
|
31
|
+
}
|
|
32
|
+
loadAttempted = true
|
|
33
|
+
|
|
34
|
+
// Supported platforms: macOS (darwin), Linux, Windows (win32)
|
|
35
|
+
const platform = process.platform
|
|
36
|
+
if (platform !== 'darwin' && platform !== 'linux' && platform !== 'win32') {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Candidate 1: native-embed path (bun compile). AUDIO_CAPTURE_NODE_PATH is
|
|
41
|
+
// defined at build time in build-with-plugins.ts for native builds only — the
|
|
42
|
+
// define resolves it to the static literal "../../audio-capture.node" so bun
|
|
43
|
+
// compile can rewrite it to /$bunfs/root/audio-capture.node. MUST stay a
|
|
44
|
+
// direct require(env var) — bun cannot analyze require(variable) from a loop.
|
|
45
|
+
if (process.env.AUDIO_CAPTURE_NODE_PATH) {
|
|
46
|
+
try {
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
48
|
+
cachedModule = require(
|
|
49
|
+
process.env.AUDIO_CAPTURE_NODE_PATH,
|
|
50
|
+
) as AudioCaptureNapi
|
|
51
|
+
return cachedModule
|
|
52
|
+
} catch {
|
|
53
|
+
// fall through to runtime fallbacks below
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Candidates 2/3: npm-install and dev/source layouts. Dynamic require is
|
|
58
|
+
// fine here — in bundled output (node --target build) require() resolves at
|
|
59
|
+
// runtime relative to cli.js at the package root; in dev it resolves
|
|
60
|
+
// relative to this file (vendor/audio-capture-src/index.ts).
|
|
61
|
+
const platformDir = `${process.arch}-${platform}`
|
|
62
|
+
const fallbacks = [
|
|
63
|
+
`./vendor/audio-capture/${platformDir}/audio-capture.node`,
|
|
64
|
+
`../audio-capture/${platformDir}/audio-capture.node`,
|
|
65
|
+
]
|
|
66
|
+
for (const p of fallbacks) {
|
|
67
|
+
try {
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
69
|
+
cachedModule = require(p) as AudioCaptureNapi
|
|
70
|
+
return cachedModule
|
|
71
|
+
} catch {
|
|
72
|
+
// try next
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isNativeAudioAvailable(): boolean {
|
|
79
|
+
return loadModule() !== null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function startNativeRecording(
|
|
83
|
+
onData: (data: Buffer) => void,
|
|
84
|
+
onEnd: () => void,
|
|
85
|
+
): boolean {
|
|
86
|
+
const mod = loadModule()
|
|
87
|
+
if (!mod) {
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
return mod.startRecording(onData, onEnd)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function stopNativeRecording(): void {
|
|
94
|
+
const mod = loadModule()
|
|
95
|
+
if (!mod) {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
mod.stopRecording()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function isNativeRecordingActive(): boolean {
|
|
102
|
+
const mod = loadModule()
|
|
103
|
+
if (!mod) {
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
return mod.isRecording()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function startNativePlayback(
|
|
110
|
+
sampleRate: number,
|
|
111
|
+
channels: number,
|
|
112
|
+
): boolean {
|
|
113
|
+
const mod = loadModule()
|
|
114
|
+
if (!mod) {
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
return mod.startPlayback(sampleRate, channels)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function writeNativePlaybackData(data: Buffer): void {
|
|
121
|
+
const mod = loadModule()
|
|
122
|
+
if (!mod) {
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
mod.writePlaybackData(data)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function stopNativePlayback(): void {
|
|
129
|
+
const mod = loadModule()
|
|
130
|
+
if (!mod) {
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
mod.stopPlayback()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function isNativePlaying(): boolean {
|
|
137
|
+
const mod = loadModule()
|
|
138
|
+
if (!mod) {
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
return mod.isPlaying()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Returns the microphone authorization status.
|
|
145
|
+
// On macOS, returns the TCC status: 0=notDetermined, 1=restricted, 2=denied, 3=authorized.
|
|
146
|
+
// On Linux, always returns 3 (authorized) — no system-level mic permission API.
|
|
147
|
+
// On Windows, returns 3 (authorized) if registry key absent or allowed, 2 (denied) if explicitly denied.
|
|
148
|
+
// Returns 0 (notDetermined) if the native module is unavailable.
|
|
149
|
+
export function microphoneAuthorizationStatus(): number {
|
|
150
|
+
const mod = loadModule()
|
|
151
|
+
if (!mod || !mod.microphoneAuthorizationStatus) {
|
|
152
|
+
return 0
|
|
153
|
+
}
|
|
154
|
+
return mod.microphoneAuthorizationStatus()
|
|
155
|
+
}
|