@iaforged/context-code 1.1.4 → 1.1.7

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 (34) 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 +93 -805
  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 +520 -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/model/model.js +2 -0
  23. package/dist/src/utils/settings/types.js +2 -2
  24. package/dist/src/voice/voiceModeEnabled.js +5 -25
  25. package/dist/vendor/audio-capture/arm64-darwin/audio-capture.node +0 -0
  26. package/dist/vendor/audio-capture/arm64-linux/audio-capture.node +0 -0
  27. package/dist/vendor/audio-capture/arm64-win32/audio-capture.node +0 -0
  28. package/dist/vendor/audio-capture/x64-darwin/audio-capture.node +0 -0
  29. package/dist/vendor/audio-capture/x64-linux/audio-capture.node +0 -0
  30. package/dist/vendor/audio-capture/x64-win32/audio-capture.node +0 -0
  31. package/dist/vendor/audio-capture-src/index.js +114 -0
  32. package/dist/vendor/audio-capture-src/index.ts +155 -0
  33. package/docs/comandos.md +132 -121
  34. package/package.json +1 -1
@@ -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)) {
@@ -552,6 +552,8 @@ export function modelDisplayString(model) {
552
552
  }
553
553
  // @[MODEL LAUNCH]: Add a marketing name mapping for the new model below.
554
554
  export function getMarketingNameForModel(modelId) {
555
+ if (!modelId)
556
+ return undefined;
555
557
  if (getAPIProvider() === 'foundry') {
556
558
  // deployment ID is user-defined in Foundry, so it may have no relation to the actual model
557
559
  return undefined;
@@ -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
+ }