@iaforged/context-code 1.1.4 → 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.
Files changed (33) hide show
  1. package/README.md +32 -8
  2. package/dist/src/commands/init.js +91 -219
  3. package/dist/src/commands/voice/index.js +6 -7
  4. package/dist/src/commands/voice/voice.js +87 -43
  5. package/dist/src/commands.js +1 -3
  6. package/dist/src/components/LogoV2/VoiceModeNotice.js +1 -1
  7. package/dist/src/components/PromptInput/VoiceIndicator.js +4 -4
  8. package/dist/src/components/Spinner.js +18 -18
  9. package/dist/src/constants/spinnerVerbs.js +9 -9
  10. package/dist/src/hooks/usePasteHandler.js +8 -8
  11. package/dist/src/hooks/useVoice.js +87 -804
  12. package/dist/src/hooks/useVoiceEnabled.js +3 -15
  13. package/dist/src/hooks/useVoiceIntegration.js +6 -25
  14. package/dist/src/keybindings/defaultBindings.js +9 -6
  15. package/dist/src/screens/REPL.js +10 -22
  16. package/dist/src/services/localDictation.js +377 -0
  17. package/dist/src/services/voice.js +9 -7
  18. package/dist/src/state/AppState.js +1 -3
  19. package/dist/src/tools/ConfigTool/ConfigTool.js +12 -15
  20. package/dist/src/tools/ConfigTool/supportedSettings.js +2 -2
  21. package/dist/src/utils/imagePaste.js +11 -5
  22. package/dist/src/utils/settings/types.js +2 -2
  23. package/dist/src/voice/voiceModeEnabled.js +5 -25
  24. package/dist/vendor/audio-capture/arm64-darwin/audio-capture.node +0 -0
  25. package/dist/vendor/audio-capture/arm64-linux/audio-capture.node +0 -0
  26. package/dist/vendor/audio-capture/arm64-win32/audio-capture.node +0 -0
  27. package/dist/vendor/audio-capture/x64-darwin/audio-capture.node +0 -0
  28. package/dist/vendor/audio-capture/x64-linux/audio-capture.node +0 -0
  29. package/dist/vendor/audio-capture/x64-win32/audio-capture.node +0 -0
  30. package/dist/vendor/audio-capture-src/index.js +114 -0
  31. package/dist/vendor/audio-capture-src/index.ts +155 -0
  32. package/docs/comandos.md +132 -121
  33. 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 = feature('VOICE_MODE') ? require('../context/voice.js').VoiceProvider : ({ children }) => children;
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: !isAnthropicAuthEnabled()
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
- 'Voice mode is not available in this environment.',
208
+ 'El modo de dictado no esta disponible en este entorno.',
212
209
  },
213
210
  };
214
211
  }
215
- if (!isVoiceStreamAvailable()) {
212
+ const dictation = await checkLocalDictationConfiguration();
213
+ if (!dictation.available) {
216
214
  return {
217
215
  data: {
218
216
  success: false,
219
- error: 'Voice mode requires a Claude.ai account. Please run /login to sign in.',
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 audio recording tool found.' +
229
- (deps.installCommand ? ` Run: ${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 = 'Settings \u2192 Privacy \u2192 Microphone';
234
+ guidance = 'Configuracion > Privacidad > Microfono';
237
235
  }
238
236
  else if (process.platform === 'linux') {
239
- guidance = "your system's audio settings";
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: `Microphone access is denied. To enable it, go to ${guidance}, then try again.`,
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: 'Preferred language for Claude responses and voice dictation (e.g., "japanese", "spanish")',
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: 'Enable voice dictation (hold-to-talk)',
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 "(Get-Clipboard -Format Image) -ne $null"',
44
- saveImage: `powershell -NoProfile -Command "$img = Get-Clipboard -Format Image; if ($img) { $img.Save('${screenshotPath.replace(/\\/g, '\\\\')}', [System.Drawing.Imaging.ImageFormat]::Png) }"`,
45
- getPath: 'powershell -NoProfile -Command "Get-Clipboard"',
46
- deleteFile: `del /f "${screenshotPath}"`,
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
- return false;
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('Preferred language for Context responses and voice dictation (e.g., "japanese", "spanish")'),
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('Enable voice mode (hold-to-talk dictation)'),
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
- // Positive ternary pattern — see docs/feature-gating.md.
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
- * Auth-only check for voice mode. Returns true when the user has a valid
22
- * Anthropic OAuth token. Backed by the memoized getClaudeAIOAuthTokens —
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
- // Voice mode requires Anthropic OAuth — it uses the voice_stream
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 hasVoiceAuth() && isVoiceGrowthBookEnabled();
27
+ return isVoiceGrowthBookEnabled();
48
28
  }
@@ -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
+ }