@swttch/extend-kit 0.3.0 → 0.5.0

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.ko.md CHANGED
@@ -7,7 +7,7 @@
7
7
  | 도구 | 번들에 못 들어가는 이유 |
8
8
  | --- | --- |
9
9
  | **battery** | *사용자 본인의* 머신에 저장된 Claude Code 로그인 정보(keychain 또는 자격증명 파일)를 읽어야 합니다 |
10
- | **stt** | 예정. 플러그인이 실어 나를 수 없는 네이티브 바이너리가 필요합니다 |
10
+ | **stt** | 받아쓰기는 OAuth 토큰을 전송하는데, JetBrains 플러그인은 자격증명을 다룰 수 없습니다 |
11
11
 
12
12
  ## battery
13
13
 
@@ -396,6 +396,67 @@ $ ccb oauth usage --json
396
396
  }
397
397
  ```
398
398
 
399
+ ## stt
400
+
401
+ 다른 Claude Code 클라이언트들이 쓰는 것과 동일한 음성 서비스로 실시간 받아쓰기를 합니다. 이 머신에 이미 있는 Claude Code 로그인(`battery`가 읽는 그 로그인)으로 인증하므로, 별도의 API 키도 별도의 과금도 없습니다.
402
+
403
+ ```typescript
404
+ import { stt } from '@swttch/extend-kit';
405
+
406
+ const stream = await stt.openSpeechToTextStream({
407
+ onTranscript: (text, isFinal) => {
408
+ // isFinal false → 곧 교체될 추정값입니다. 보여주되 쌓아두지 마세요.
409
+ // isFinal true → 확정된 텍스트입니다. 이어붙이세요.
410
+ if (isFinal) console.log(text);
411
+ },
412
+ onError: (message, info) => {
413
+ // info.fatal 이면 재시도해도 소용없습니다(예: 토큰이 거부됨).
414
+ console.error(message);
415
+ },
416
+ });
417
+
418
+ // 16비트 PCM, 16kHz, 모노를 그대로 넣습니다. 소켓이 열리기 전에 보낸 오디오는
419
+ // 버퍼링되므로, 곧바로 녹음을 시작해도 앞부분이 잘리지 않습니다.
420
+ stream.sendAudio(pcmChunk);
421
+
422
+ // 닫을 때 마지막 발화를 잠시 기다린 뒤 종료합니다.
423
+ await stream.close();
424
+ ```
425
+
426
+ ### 오디오 형식
427
+
428
+ 서비스는 한 가지 형식만 받으며, 다른 것을 보내면 에러가 아니라 **무음으로 처리**됩니다.
429
+
430
+ | | |
431
+ | --- | --- |
432
+ | 인코딩 | `linear16` (부호 있는 16비트 PCM, 리틀 엔디언) |
433
+ | 샘플레이트 | 16000 Hz |
434
+ | 채널 | 1 (모노) |
435
+
436
+ 런타임에 이 값이 필요하면 `stt.AUDIO_FORMAT`에 들어 있습니다.
437
+
438
+ ### 옵션
439
+
440
+ ```typescript
441
+ await stt.openSpeechToTextStream(handlers, {
442
+ language: 'ko', // BCP-47. 기본값은 'en'
443
+ extraKeyterms: ['Swttch', 'JCEF'], // 사용하는 어휘 쪽으로 인식을 유도
444
+ typedInterims: true, // 확정 전 추정값도 함께 스트리밍
445
+ });
446
+ ```
447
+
448
+ 일반 음성 모델은 기술 용어를 잘못 알아듣습니다("MCP"를 "MTP"로). 기본 프로그래밍 용어 목록이 항상 적용되며, `extraKeyterms`는 거기에 추가됩니다.
449
+
450
+ ### 사용 가능 여부 먼저 확인하기
451
+
452
+ ```typescript
453
+ if (!(await stt.isSpeechToTextAvailable())) {
454
+ // 이 머신에 Claude Code가 로그인되어 있지 않습니다.
455
+ }
456
+ ```
457
+
458
+ > **참고**: 이 엔드포인트는 Anthropic이 문서화한 API가 아닙니다. 예고 없이 바뀔 수 있습니다. 클라이언트는 모르는 메시지 타입을 실패로 처리하지 않고 무시하지만, 더 큰 변경이 생기면 받아쓰기가 깨질 수 있습니다.
459
+
399
460
  ## 개발
400
461
 
401
462
  ### 설치
package/README.md CHANGED
@@ -7,7 +7,7 @@ The companion tools [Swttch](https://github.com/Swttch/swttch) needs but cannot
7
7
  | Tool | Why it lives outside the bundle |
8
8
  | --- | --- |
9
9
  | **battery** | Reads the Claude Code login stored on *your* machine (keychain, or a credentials file) |
10
- | **stt** | Planned. Needs native binaries the plugin cannot carry |
10
+ | **stt** | Dictation sends your OAuth token, and a JetBrains plugin may not handle credentials |
11
11
 
12
12
  ## battery
13
13
 
@@ -396,6 +396,67 @@ $ ccb oauth usage --json
396
396
  }
397
397
  ```
398
398
 
399
+ ## stt
400
+
401
+ Live dictation against the same speech service the other Claude Code clients use. It authenticates with the Claude Code login already on this machine — the same one `battery` reads — so there is no separate API key and no separate bill.
402
+
403
+ ```typescript
404
+ import { stt } from '@swttch/extend-kit';
405
+
406
+ const stream = await stt.openSpeechToTextStream({
407
+ onTranscript: (text, isFinal) => {
408
+ // isFinal false → a live guess that will be replaced; show it, don't keep it.
409
+ // isFinal true → settled text; append it.
410
+ if (isFinal) console.log(text);
411
+ },
412
+ onError: (message, info) => {
413
+ // info.fatal means retrying won't help (e.g. the token was rejected).
414
+ console.error(message);
415
+ },
416
+ });
417
+
418
+ // Feed raw 16-bit PCM, 16 kHz, mono. Audio sent before the socket finishes
419
+ // opening is buffered, so you can start recording immediately.
420
+ stream.sendAudio(pcmChunk);
421
+
422
+ // Closing waits briefly for the last words, then resolves.
423
+ await stream.close();
424
+ ```
425
+
426
+ ### Audio format
427
+
428
+ The service accepts one format, and sending anything else yields silence rather than an error:
429
+
430
+ | | |
431
+ | --- | --- |
432
+ | Encoding | `linear16` (raw signed 16-bit PCM, little-endian) |
433
+ | Sample rate | 16000 Hz |
434
+ | Channels | 1 (mono) |
435
+
436
+ `stt.AUDIO_FORMAT` carries these values if you need them at runtime.
437
+
438
+ ### Options
439
+
440
+ ```typescript
441
+ await stt.openSpeechToTextStream(handlers, {
442
+ language: 'ko', // BCP-47; defaults to 'en'
443
+ extraKeyterms: ['Swttch', 'JCEF'], // bias recognition toward your vocabulary
444
+ typedInterims: true, // also stream partial guesses
445
+ });
446
+ ```
447
+
448
+ A general speech model mishears technical words — "MCP" becomes "MTP". A default set of programming terms is always applied; `extraKeyterms` adds to it.
449
+
450
+ ### Checking availability first
451
+
452
+ ```typescript
453
+ if (!(await stt.isSpeechToTextAvailable())) {
454
+ // Claude Code is not logged in on this machine.
455
+ }
456
+ ```
457
+
458
+ > **Note**: this endpoint is not part of Anthropic's documented API. It may change without warning. The client ignores message types it does not recognise rather than failing, but a larger change could still break dictation.
459
+
399
460
  ## Development
400
461
 
401
462
  ### Installation
@@ -443,3 +504,16 @@ MIT
443
504
  ## Author
444
505
 
445
506
  yhk1038
507
+
508
+ ## Saved-account usage (CLI)
509
+
510
+ `ccb --capabilities --json` reports `oauth.usage.account-file` when supported.
511
+ `ccb oauth usage --account-file="/path/to/account-snapshot.json" --json` reads a
512
+ CCG account snapshot (`credentials` is a JSON string, `oauthAccount.emailAddress`
513
+ identifies the account) and queries that account without switching the live login.
514
+ Credential values are never command arguments or CLI output. The CLI may use a
515
+ newer live credential when the live email matches the selected snapshot; otherwise
516
+ it uses the snapshot. Expired tokens return `token_expired`. No token refresh,
517
+ credential writes, caching, retries or automatic account switching are performed.
518
+ The caller owns process timeout and recovery policy. Plain `ccb oauth usage`
519
+ retains its existing behaviour.
@@ -1,2 +1,2 @@
1
1
  import type { ClaudeCredentials } from './types.js';
2
- export declare function readKeychainCredentials(): Promise<ClaudeCredentials>;
2
+ export declare function readKeychainCredentials(configDir?: string): Promise<ClaudeCredentials>;
@@ -1,4 +1,5 @@
1
1
  import { execFile } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
2
3
  import { promisify } from 'node:util';
3
4
  const execFileAsync = promisify(execFile);
4
5
  const SERVICE_NAME = 'Claude Code-credentials';
@@ -6,11 +7,11 @@ async function getOsUsername() {
6
7
  const { stdout } = await execFileAsync('whoami');
7
8
  return stdout.trim();
8
9
  }
9
- export async function readKeychainCredentials() {
10
+ export async function readKeychainCredentials(configDir) {
10
11
  const account = await getOsUsername();
11
12
  const { stdout } = await execFileAsync('security', [
12
13
  'find-generic-password',
13
- '-s', SERVICE_NAME,
14
+ '-s', configDir ? `${SERVICE_NAME}-${createHash('sha256').update(configDir).digest('hex').slice(0, 8)}` : SERVICE_NAME,
14
15
  '-a', account,
15
16
  '-w',
16
17
  ]);
@@ -1 +1 @@
1
- {"version":3,"file":"keychain.js","sourceRoot":"","sources":["../../../src/battery/auth/keychain.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAGtC,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAE1C,MAAM,YAAY,GAAG,yBAAyB,CAAC;AAE/C,KAAK,UAAU,aAAa;IAC1B,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;IACjD,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;AACvB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,uBAAuB;IAC3C,MAAM,OAAO,GAAG,MAAM,aAAa,EAAE,CAAC;IACtC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,UAAU,EAAE;QACjD,uBAAuB;QACvB,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE,OAAO;QACb,IAAI;KACL,CAAC,CAAC;IAEH,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAC1B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAsB,CAAC;IACpD,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"keychain.js","sourceRoot":"","sources":["../../../src/battery/auth/keychain.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAGtC,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAE1C,MAAM,YAAY,GAAG,yBAAyB,CAAC;AAE/C,KAAK,UAAU,aAAa;IAC1B,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;IACjD,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;AACvB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,SAAkB;IAC9D,MAAM,OAAO,GAAG,MAAM,aAAa,EAAE,CAAC;IACtC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,UAAU,EAAE;QACjD,uBAAuB;QACvB,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,YAAY,IAAI,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY;QACtH,IAAI,EAAE,OAAO;QACb,IAAI;KACL,CAAC,CAAC;IAEH,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAC1B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAsB,CAAC;IACpD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ClaudeCredentials } from '../battery/auth/types.js';
2
+ /** Read a CCG saved-account snapshot without replacing the active credential slot. */
3
+ export declare function getAccountCredentials(file: string): Promise<ClaudeCredentials>;
@@ -0,0 +1,43 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { getCredentials } from '../battery/auth/index.js';
5
+ import { readKeychainCredentials } from '../battery/auth/keychain.js';
6
+ import { CcbError } from '../battery/errors.js';
7
+ /** Read a CCG saved-account snapshot without replacing the active credential slot. */
8
+ export async function getAccountCredentials(file) {
9
+ let snapshot;
10
+ let credentials;
11
+ try {
12
+ snapshot = JSON.parse(await readFile(file, 'utf8'));
13
+ credentials = JSON.parse(snapshot.credentials ?? '');
14
+ if (!credentials.claudeAiOauth?.accessToken)
15
+ throw new Error('Missing access token');
16
+ }
17
+ catch {
18
+ // Parsing errors must never echo a credential blob in stderr.
19
+ throw new CcbError('Saved account credentials are unavailable or invalid.', 'credentials_not_found');
20
+ }
21
+ const email = snapshot.oauthAccount?.emailAddress;
22
+ const configDir = process.env.CLAUDE_CONFIG_DIR;
23
+ const liveEmail = async () => {
24
+ const metadata = JSON.parse(await readFile(join(configDir ?? homedir(), '.claude.json'), 'utf8'));
25
+ return metadata.oauthAccount?.emailAddress;
26
+ };
27
+ // A currently active account can have a newer token than its saved snapshot.
28
+ // Never use the live slot for a different account. No writes or token refresh.
29
+ try {
30
+ if (email && await liveEmail() === email) {
31
+ const live = process.platform === 'darwin'
32
+ ? await readKeychainCredentials(configDir) : await getCredentials();
33
+ if (await liveEmail() === email && live.claudeAiOauth?.accessToken)
34
+ credentials = live;
35
+ }
36
+ }
37
+ catch { /* The saved snapshot remains the fallback. */ }
38
+ if (Number.isFinite(credentials.claudeAiOauth.expiresAt) && Date.now() >= credentials.claudeAiOauth.expiresAt) {
39
+ throw new CcbError('Saved account token has expired. Log in to this account again.', 'token_expired');
40
+ }
41
+ return credentials;
42
+ }
43
+ //# sourceMappingURL=account-credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account-credentials.js","sourceRoot":"","sources":["../../src/cli/account-credentials.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAEtE,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAEhD,sFAAsF;AACtF,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,IAAY;IACtD,IAAI,QAA4E,CAAC;IACjF,IAAI,WAA8B,CAAC;IACnC,IAAI,CAAC;QACH,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QACpD,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;QACrD,IAAI,CAAC,WAAW,CAAC,aAAa,EAAE,WAAW;YAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;IACvF,CAAC;IAAC,MAAM,CAAC;QACP,8DAA8D;QAC9D,MAAM,IAAI,QAAQ,CAAC,uDAAuD,EAAE,uBAAuB,CAAC,CAAC;IACvG,CAAC;IACD,MAAM,KAAK,GAAG,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IAClD,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;IAChD,MAAM,SAAS,GAAG,KAAK,IAAiC,EAAE;QACxD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,SAAS,IAAI,OAAO,EAAE,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,CAE/F,CAAC;QACF,OAAO,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IAC7C,CAAC,CAAC;IACF,6EAA6E;IAC7E,+EAA+E;IAC/E,IAAI,CAAC;QACH,IAAI,KAAK,IAAI,MAAM,SAAS,EAAE,KAAK,KAAK,EAAE,CAAC;YACzC,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,KAAK,QAAQ;gBACxC,CAAC,CAAC,MAAM,uBAAuB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,cAAc,EAAE,CAAC;YACtE,IAAI,MAAM,SAAS,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC,aAAa,EAAE,WAAW;gBAAE,WAAW,GAAG,IAAI,CAAC;QACzF,CAAC;IACH,CAAC;IAAC,MAAM,CAAC,CAAC,8CAA8C,CAAC,CAAC;IAC1D,IAAI,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,aAAa,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,WAAW,CAAC,aAAa,CAAC,SAAS,EAAE,CAAC;QAC9G,MAAM,IAAI,QAAQ,CAAC,gEAAgE,EAAE,eAAe,CAAC,CAAC;IACxG,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC"}
package/dist/cli/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { getCredentials, getAccessToken } from '../battery/auth/index.js';
3
3
  import { ClaudeCodeClient } from '../battery/api/index.js';
4
4
  import { oauthCommand } from './oauth.js';
5
+ import { getAccountCredentials } from './account-credentials.js';
5
6
  import { CcbError } from '../battery/errors.js';
6
7
  import { readFileSync } from 'node:fs';
7
8
  import { fileURLToPath } from 'node:url';
@@ -13,13 +14,21 @@ const args = process.argv.slice(2);
13
14
  const command = args.filter((a) => !a.startsWith('-'));
14
15
  const flags = new Set(args.filter((a) => a.startsWith('-')));
15
16
  const jsonOutput = flags.has('--json');
17
+ const accountFile = args.find(arg => arg.startsWith('--account-file='))?.slice('--account-file='.length);
16
18
  async function createClient() {
17
- const credentials = await getCredentials();
19
+ const credentials = accountFile ? await getAccountCredentials(accountFile) : await getCredentials();
18
20
  const token = getAccessToken(credentials);
19
21
  return new ClaudeCodeClient(token);
20
22
  }
21
23
  async function run() {
22
24
  const [module, ...subcommand] = command;
25
+ if (flags.has('--capabilities')) {
26
+ console.log(JSON.stringify({ capabilities: ['oauth.usage.account-file'] }));
27
+ return;
28
+ }
29
+ if (args.some(arg => arg.startsWith('--account-file')) && (!accountFile || module !== 'oauth' || subcommand[0] !== 'usage')) {
30
+ throw new CcbError('Use oauth usage --account-file=<snapshot path>.', 'invalid_argument');
31
+ }
23
32
  if (flags.has('-v') || flags.has('--version')) {
24
33
  console.log(VERSION);
25
34
  return;
@@ -34,6 +43,8 @@ Commands:
34
43
  oauth profile Show account profile
35
44
 
36
45
  Options:
46
+ --account-file=<path> Read usage for a CCG saved-account snapshot (no account switch)
47
+ --capabilities Output supported CLI capabilities as JSON
37
48
  --json Output as JSON
38
49
  -h, --help Show help
39
50
  -v, --version Show version`);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAEhD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE7C,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,SAAS,EAAE,oBAAoB,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;AACxF,MAAM,OAAO,GAAW,GAAG,CAAC,OAAO,CAAC;AAEpC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;AAC/D,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AACrE,MAAM,UAAU,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AAEvC,KAAK,UAAU,YAAY;IACzB,MAAM,WAAW,GAAG,MAAM,cAAc,EAAE,CAAC;IAC3C,MAAM,KAAK,GAAG,cAAc,CAAC,WAAW,CAAC,CAAC;IAC1C,OAAO,IAAI,gBAAgB,CAAC,KAAK,CAAC,CAAC;AACrC,CAAC;AAED,KAAK,UAAU,GAAG;IAChB,MAAM,CAAC,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,OAAO,CAAC;IAExC,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;QAC9C,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACrB,OAAO;IACT,CAAC;IAED,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtD,OAAO,CAAC,GAAG,CAAC,QAAQ,OAAO;;;;;;;;;;;gCAWC,CAAC,CAAC;QAC9B,OAAO;IACT,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,YAAY,EAAE,CAAC;IAEpC,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,OAAO;YACV,MAAM,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;YACnD,MAAM;QACR;YACE,OAAO,CAAC,KAAK,CAAC,mBAAmB,MAAM,EAAE,CAAC,CAAC;YAC3C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;AACH,CAAC;AAED,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IAClB,IAAI,UAAU,EAAE,CAAC;QACf,IAAI,GAAG,YAAY,QAAQ,EAAE,CAAC;YAC5B,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QACvD,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC;gBAC3B,KAAK,EAAE;oBACL,IAAI,EAAE,eAAe;oBACrB,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;iBAC1D;aACF,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QACf,CAAC;IACH,CAAC;SAAM,CAAC;QACN,IAAI,GAAG,YAAY,QAAQ,EAAE,CAAC;YAC5B,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;YACvC,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;gBACb,OAAO,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;YACrC,CAAC;QACH,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -13,3 +13,4 @@
13
13
  */
14
14
  export * from './battery/index.js';
15
15
  export * as battery from './battery/index.js';
16
+ export * as stt from './stt/index.js';
package/dist/index.js CHANGED
@@ -15,4 +15,9 @@ export * from './battery/index.js';
15
15
  // Namespaced access, for callers who would rather be explicit about which
16
16
  // tool they are reaching for — `import { battery } from '@swttch/extend-kit'`.
17
17
  export * as battery from './battery/index.js';
18
+ // Speech-to-text is namespace-only. Its names (`openSpeechToTextStream`,
19
+ // `AUDIO_FORMAT`) are generic enough that exporting them flat would collide
20
+ // with a caller's own, and unlike battery it has no legacy flat imports to
21
+ // preserve — `import { stt } from '@swttch/extend-kit'`.
22
+ export * as stt from './stt/index.js';
18
23
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,cAAc,oBAAoB,CAAC;AAEnC,0EAA0E;AAC1E,+EAA+E;AAC/E,OAAO,KAAK,OAAO,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,cAAc,oBAAoB,CAAC;AAEnC,0EAA0E;AAC1E,+EAA+E;AAC/E,OAAO,KAAK,OAAO,MAAM,oBAAoB,CAAC;AAE9C,yEAAyE;AACzE,4EAA4E;AAC5E,2EAA2E;AAC3E,yDAAyD;AACzD,OAAO,KAAK,GAAG,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Speech-to-text — dictation against the same service the other Claude Code
3
+ * clients use.
4
+ *
5
+ * It lives out here, outside the plugin bundle, because transcribing means
6
+ * sending the account's OAuth token and a JetBrains plugin may not handle
7
+ * credentials. This package already reads the local Claude Code login for the
8
+ * usage panel, so dictation reuses that path.
9
+ */
10
+ export { openSpeechToTextStream, isSpeechToTextAvailable } from './stream.js';
11
+ export { DEFAULT_KEYTERMS, packKeyterms } from './keyterms.js';
12
+ export { AUDIO_FORMAT, TranscriptMessageType } from './types.js';
13
+ export type { SpeechToTextOptions, SpeechToTextHandlers, SpeechToTextStream, TranscriptMessage, } from './types.js';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Speech-to-text — dictation against the same service the other Claude Code
3
+ * clients use.
4
+ *
5
+ * It lives out here, outside the plugin bundle, because transcribing means
6
+ * sending the account's OAuth token and a JetBrains plugin may not handle
7
+ * credentials. This package already reads the local Claude Code login for the
8
+ * usage panel, so dictation reuses that path.
9
+ */
10
+ export { openSpeechToTextStream, isSpeechToTextAvailable } from './stream.js';
11
+ export { DEFAULT_KEYTERMS, packKeyterms } from './keyterms.js';
12
+ export { AUDIO_FORMAT, TranscriptMessageType } from './types.js';
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/stt/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Words the recognizer is told to expect.
3
+ *
4
+ * A general speech model mishears technical vocabulary — "MCP" becomes "MTP",
5
+ * "grep" becomes "graph". Biasing the recognizer toward terms a developer
6
+ * actually says fixes that, and costs nothing when they are not spoken.
7
+ */
8
+ /** Terms every session biases toward, regardless of what the caller asks for. */
9
+ export declare const DEFAULT_KEYTERMS: readonly string[];
10
+ /**
11
+ * Pack terms into the comma-separated header value the service expects.
12
+ *
13
+ * Commas separate entries, so a comma inside a term would split it in two —
14
+ * they become spaces. Non-ASCII is dropped because the value travels in an
15
+ * HTTP header, where anything outside printable ASCII is not safely
16
+ * transmissible. Duplicates are removed so a caller repeating a default term
17
+ * does not eat the budget twice, and the list is cut at the byte cap rather
18
+ * than sent over-long.
19
+ */
20
+ export declare function packKeyterms(terms: readonly string[]): string;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Words the recognizer is told to expect.
3
+ *
4
+ * A general speech model mishears technical vocabulary — "MCP" becomes "MTP",
5
+ * "grep" becomes "graph". Biasing the recognizer toward terms a developer
6
+ * actually says fixes that, and costs nothing when they are not spoken.
7
+ */
8
+ /** Terms every session biases toward, regardless of what the caller asks for. */
9
+ export const DEFAULT_KEYTERMS = [
10
+ 'VS Code',
11
+ 'IDE',
12
+ 'webview',
13
+ 'IntelliSense',
14
+ 'MCP',
15
+ 'symlink',
16
+ 'grep',
17
+ 'regex',
18
+ 'localhost',
19
+ 'codebase',
20
+ 'TypeScript',
21
+ 'JSON',
22
+ 'OAuth',
23
+ 'webhook',
24
+ 'gRPC',
25
+ 'dotfiles',
26
+ 'subagent',
27
+ 'worktree',
28
+ ];
29
+ /** The service caps the keyterm header; longer lists are cut, not rejected. */
30
+ const MAX_KEYTERM_BYTES = 1024;
31
+ /**
32
+ * Pack terms into the comma-separated header value the service expects.
33
+ *
34
+ * Commas separate entries, so a comma inside a term would split it in two —
35
+ * they become spaces. Non-ASCII is dropped because the value travels in an
36
+ * HTTP header, where anything outside printable ASCII is not safely
37
+ * transmissible. Duplicates are removed so a caller repeating a default term
38
+ * does not eat the budget twice, and the list is cut at the byte cap rather
39
+ * than sent over-long.
40
+ */
41
+ export function packKeyterms(terms) {
42
+ const seen = new Set();
43
+ const packed = [];
44
+ let size = 0;
45
+ for (const term of terms) {
46
+ const clean = term
47
+ .replace(/,/g, ' ')
48
+ .replace(/[^\x20-\x7E]/g, '')
49
+ .replace(/\s+/g, ' ')
50
+ .trim();
51
+ if (!clean || seen.has(clean))
52
+ continue;
53
+ const cost = clean.length + (packed.length > 0 ? 1 : 0);
54
+ if (size + cost > MAX_KEYTERM_BYTES)
55
+ break;
56
+ seen.add(clean);
57
+ packed.push(clean);
58
+ size += cost;
59
+ }
60
+ return packed.join(',');
61
+ }
62
+ //# sourceMappingURL=keyterms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyterms.js","sourceRoot":"","sources":["../../src/stt/keyterms.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,iFAAiF;AACjF,MAAM,CAAC,MAAM,gBAAgB,GAAsB;IACjD,SAAS;IACT,KAAK;IACL,SAAS;IACT,cAAc;IACd,KAAK;IACL,SAAS;IACT,MAAM;IACN,OAAO;IACP,WAAW;IACX,UAAU;IACV,YAAY;IACZ,MAAM;IACN,OAAO;IACP,SAAS;IACT,MAAM;IACN,UAAU;IACV,UAAU;IACV,UAAU;CACX,CAAC;AAEF,+EAA+E;AAC/E,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAE/B;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,KAAwB;IACnD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,IAAI,GAAG,CAAC,CAAC;IAEb,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,IAAI;aACf,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC;aAClB,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC;aAC5B,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;aACpB,IAAI,EAAE,CAAC;QACV,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,SAAS;QAExC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACxD,IAAI,IAAI,GAAG,IAAI,GAAG,iBAAiB;YAAE,MAAM;QAE3C,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAChB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACnB,IAAI,IAAI,IAAI,CAAC;IACf,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,26 @@
1
+ import { type SpeechToTextHandlers, type SpeechToTextOptions, type SpeechToTextStream } from './types.js';
2
+ /**
3
+ * Open a live transcription stream against the same service the other Claude
4
+ * Code clients use.
5
+ *
6
+ * This lives outside the plugin bundle on purpose. Transcribing means sending
7
+ * the account's OAuth token, and a JetBrains plugin is not allowed to handle
8
+ * credentials — so the plugin shells out to this package, which reads the
9
+ * Claude Code login already on the machine, exactly as the usage panel does.
10
+ *
11
+ * The endpoint is undocumented. We call it the way the official clients do and
12
+ * treat every response as untrusted: unknown message types are ignored rather
13
+ * than surfaced as errors, so a new message type added server-side does not
14
+ * break dictation.
15
+ *
16
+ * @param handlers Callbacks for text, failure, and readiness.
17
+ * @param options Language and vocabulary hints.
18
+ * @param token OAuth token; read from the local Claude Code login when omitted.
19
+ */
20
+ export declare function openSpeechToTextStream(handlers: SpeechToTextHandlers, options?: SpeechToTextOptions, token?: string): Promise<SpeechToTextStream>;
21
+ /**
22
+ * Whether this machine can transcribe at all — that is, whether Claude Code is
23
+ * logged in here. Lets a caller show "sign in first" instead of opening a
24
+ * socket that is bound to be rejected.
25
+ */
26
+ export declare function isSpeechToTextAvailable(): Promise<boolean>;
@@ -0,0 +1,227 @@
1
+ import WebSocket from 'ws';
2
+ import { getCredentials, getAccessToken } from '../battery/auth/index.js';
3
+ import { CcbError } from '../battery/errors.js';
4
+ import { DEFAULT_KEYTERMS, packKeyterms } from './keyterms.js';
5
+ import { AUDIO_FORMAT, TranscriptMessageType, } from './types.js';
6
+ const VOICE_STREAM_URL = 'wss://api.anthropic.com/api/ws/speech_to_text/voice_stream';
7
+ /** The service drops idle sockets; this is well inside its window. */
8
+ const KEEPALIVE_INTERVAL_MS = 8000;
9
+ /** How long to wait for trailing words after asking the service to stop. */
10
+ const CLOSE_GRACE_MS = 3000;
11
+ const KEEPALIVE = JSON.stringify({ type: 'KeepAlive' });
12
+ const CLOSE_STREAM = JSON.stringify({ type: 'CloseStream' });
13
+ /**
14
+ * Open a live transcription stream against the same service the other Claude
15
+ * Code clients use.
16
+ *
17
+ * This lives outside the plugin bundle on purpose. Transcribing means sending
18
+ * the account's OAuth token, and a JetBrains plugin is not allowed to handle
19
+ * credentials — so the plugin shells out to this package, which reads the
20
+ * Claude Code login already on the machine, exactly as the usage panel does.
21
+ *
22
+ * The endpoint is undocumented. We call it the way the official clients do and
23
+ * treat every response as untrusted: unknown message types are ignored rather
24
+ * than surfaced as errors, so a new message type added server-side does not
25
+ * break dictation.
26
+ *
27
+ * @param handlers Callbacks for text, failure, and readiness.
28
+ * @param options Language and vocabulary hints.
29
+ * @param token OAuth token; read from the local Claude Code login when omitted.
30
+ */
31
+ export async function openSpeechToTextStream(handlers, options = {}, token) {
32
+ const accessToken = token ?? getAccessToken(await getCredentials());
33
+ const query = new URLSearchParams({
34
+ encoding: AUDIO_FORMAT.encoding,
35
+ sample_rate: String(AUDIO_FORMAT.sampleRate),
36
+ channels: String(AUDIO_FORMAT.channels),
37
+ // How long a pause counts as the end of a phrase, and of a whole utterance.
38
+ endpointing_ms: '300',
39
+ utterance_end_ms: '1000',
40
+ language: options.language ?? 'en',
41
+ use_conversation_engine: 'true',
42
+ ...(options.typedInterims ? { forward_interims: 'typed' } : {}),
43
+ stt_provider: 'deepgram-nova3',
44
+ });
45
+ const keyterms = packKeyterms([...DEFAULT_KEYTERMS, ...(options.extraKeyterms ?? [])]);
46
+ const socket = new WebSocket(`${VOICE_STREAM_URL}?${query.toString()}`, {
47
+ headers: {
48
+ Authorization: `Bearer ${accessToken}`,
49
+ 'x-app': 'swttch',
50
+ 'anthropic-client-platform': 'claude_code_swttch',
51
+ ...(keyterms ? { 'x-config-keyterms': keyterms } : {}),
52
+ },
53
+ });
54
+ let keepalive = null;
55
+ let closing = false;
56
+ /**
57
+ * Audio handed to us before the socket finished opening.
58
+ *
59
+ * Opening takes a moment, and a caller that starts recording the instant
60
+ * this function returns is doing the obvious thing — so dropping those first
61
+ * chunks would silently lose the beginning of every sentence. We hold them
62
+ * and flush on open instead. The cap keeps a socket that never opens from
63
+ * growing without bound; at 16 kHz mono 16-bit that is a few seconds of audio,
64
+ * far longer than a handshake.
65
+ */
66
+ const preOpenAudio = [];
67
+ let preOpenBytes = 0;
68
+ const MAX_PREOPEN_BYTES = AUDIO_FORMAT.sampleRate * 2 * 5;
69
+ /**
70
+ * Best text so far for the utterance in progress, not yet handed over as
71
+ * final.
72
+ *
73
+ * "Best" rather than "latest" because the service revises downward as well as
74
+ * up: a run reads "Hello. This is a test", then the next message comes back
75
+ * as just "Hello." while it reconsiders. Committing whatever arrived last
76
+ * would throw away the longer reading, so we keep the longest we have seen
77
+ * and let a later message replace it only when it extends that reading.
78
+ */
79
+ let pending = '';
80
+ function stopKeepalive() {
81
+ closing = true;
82
+ if (keepalive) {
83
+ clearInterval(keepalive);
84
+ keepalive = null;
85
+ }
86
+ }
87
+ /**
88
+ * Hand over whatever the speaker said but never finished. Without this, the
89
+ * last phrase before a close is silently dropped — the user watches their
90
+ * final sentence disappear.
91
+ */
92
+ function flushPending() {
93
+ if (!pending)
94
+ return;
95
+ const text = pending;
96
+ pending = '';
97
+ handlers.onTranscript(text, true);
98
+ }
99
+ socket.on('open', () => {
100
+ handlers.onOpen?.();
101
+ socket.send(KEEPALIVE);
102
+ // Whatever the caller recorded while we were connecting goes first, in
103
+ // order, so the transcript starts at the first word actually spoken.
104
+ for (const chunk of preOpenAudio)
105
+ socket.send(chunk);
106
+ preOpenAudio.length = 0;
107
+ preOpenBytes = 0;
108
+ keepalive = setInterval(() => {
109
+ if (socket.readyState === WebSocket.OPEN)
110
+ socket.send(KEEPALIVE);
111
+ }, KEEPALIVE_INTERVAL_MS);
112
+ });
113
+ socket.on('message', (raw) => {
114
+ let msg;
115
+ try {
116
+ msg = JSON.parse(raw.toString());
117
+ }
118
+ catch {
119
+ // Not JSON: nothing we can act on, and not worth alarming the user.
120
+ return;
121
+ }
122
+ switch (msg.type) {
123
+ case TranscriptMessageType.TranscriptInterim:
124
+ case TranscriptMessageType.TranscriptText:
125
+ // Both are still-changing guesses at the current utterance. Show every
126
+ // one of them so the caller can render live text, but only remember a
127
+ // reading that is at least as complete as what we already have — see
128
+ // the note on `pending` for why the newest is not always the best.
129
+ if (msg.data) {
130
+ if (msg.data.length >= pending.length)
131
+ pending = msg.data;
132
+ handlers.onTranscript(msg.data, false);
133
+ }
134
+ break;
135
+ case TranscriptMessageType.TranscriptEndpoint:
136
+ // The speaker paused — what we have is settled.
137
+ flushPending();
138
+ break;
139
+ case TranscriptMessageType.TranscriptError:
140
+ handlers.onError(msg.description ?? 'transcription error');
141
+ break;
142
+ case TranscriptMessageType.Error:
143
+ handlers.onError(msg.message ?? 'server error');
144
+ break;
145
+ default:
146
+ // Unknown type: ignore. The endpoint is undocumented and may grow new
147
+ // messages; treating them as failures would break dictation for no reason.
148
+ break;
149
+ }
150
+ });
151
+ socket.on('error', (err) => {
152
+ if (closing)
153
+ return;
154
+ // A handshake rejection arrives as "Unexpected server response: 401". 4xx is
155
+ // the account's problem (expired token, no access) and retrying will not
156
+ // help; 5xx and transport errors are worth another attempt.
157
+ const status = /^Unexpected server response: (\d+)/.exec(err.message)?.[1];
158
+ const code = status ? Number(status) : undefined;
159
+ handlers.onError(`WebSocket error: ${err.message}`, {
160
+ fatal: code !== undefined && code >= 400 && code < 500,
161
+ });
162
+ });
163
+ socket.on('close', () => {
164
+ flushPending();
165
+ stopKeepalive();
166
+ });
167
+ return {
168
+ sendAudio(chunk) {
169
+ if (closing)
170
+ return;
171
+ if (socket.readyState === WebSocket.OPEN) {
172
+ socket.send(chunk);
173
+ return;
174
+ }
175
+ // Still connecting: hold it rather than drop it. Past the cap we discard
176
+ // the oldest, because if the socket is this slow the recent audio is the
177
+ // part still worth transcribing.
178
+ if (socket.readyState === WebSocket.CONNECTING) {
179
+ preOpenAudio.push(chunk);
180
+ preOpenBytes += chunk.byteLength;
181
+ while (preOpenBytes > MAX_PREOPEN_BYTES && preOpenAudio.length > 0) {
182
+ preOpenBytes -= preOpenAudio.shift().byteLength;
183
+ }
184
+ }
185
+ },
186
+ close() {
187
+ if (closing || socket.readyState === WebSocket.CLOSED)
188
+ return Promise.resolve();
189
+ return new Promise((resolve) => {
190
+ // Ask politely, then stop waiting. The service usually sends a last
191
+ // transcript after CloseStream, but a hung socket must not hang the
192
+ // caller's UI, so the grace period is capped.
193
+ const giveUp = setTimeout(() => {
194
+ stopKeepalive();
195
+ flushPending();
196
+ if (socket.readyState !== WebSocket.CLOSED)
197
+ socket.close();
198
+ resolve();
199
+ }, CLOSE_GRACE_MS);
200
+ if (socket.readyState === WebSocket.OPEN)
201
+ socket.send(CLOSE_STREAM);
202
+ socket.once('close', () => {
203
+ clearTimeout(giveUp);
204
+ stopKeepalive();
205
+ resolve();
206
+ });
207
+ });
208
+ },
209
+ };
210
+ }
211
+ /**
212
+ * Whether this machine can transcribe at all — that is, whether Claude Code is
213
+ * logged in here. Lets a caller show "sign in first" instead of opening a
214
+ * socket that is bound to be rejected.
215
+ */
216
+ export async function isSpeechToTextAvailable() {
217
+ try {
218
+ getAccessToken(await getCredentials());
219
+ return true;
220
+ }
221
+ catch (err) {
222
+ if (err instanceof CcbError)
223
+ return false;
224
+ throw err;
225
+ }
226
+ }
227
+ //# sourceMappingURL=stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../../src/stt/stream.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Wire types for Claude's speech-to-text stream.
3
+ *
4
+ * These mirror what the service actually sends. The endpoint is undocumented,
5
+ * so the shapes here are what we observed rather than a published contract —
6
+ * treat every field as possibly absent and never assume a message type we do
7
+ * not know about is an error.
8
+ */
9
+ /** Audio the service expects: raw 16-bit PCM, 16 kHz, mono. */
10
+ export declare const AUDIO_FORMAT: {
11
+ readonly encoding: "linear16";
12
+ readonly sampleRate: 16000;
13
+ readonly channels: 1;
14
+ };
15
+ /** Messages the service sends down the socket. */
16
+ export declare enum TranscriptMessageType {
17
+ /** A partial guess that will still change. Show it, do not commit it. */
18
+ TranscriptInterim = "TranscriptInterim",
19
+ /** Improved text for the utterance in progress. Still not final. */
20
+ TranscriptText = "TranscriptText",
21
+ /** The speaker paused: whatever we have accumulated is now final. */
22
+ TranscriptEndpoint = "TranscriptEndpoint",
23
+ /** Transcription failed. `description` carries the reason. */
24
+ TranscriptError = "TranscriptError",
25
+ /** Generic server-side failure. `message` carries the reason. */
26
+ Error = "error"
27
+ }
28
+ export interface TranscriptMessage {
29
+ type: TranscriptMessageType | string;
30
+ /** Present on TranscriptInterim / TranscriptText. */
31
+ data?: string;
32
+ /** Present on TranscriptError. */
33
+ description?: string;
34
+ /** Present on error. */
35
+ message?: string;
36
+ }
37
+ export interface SpeechToTextOptions {
38
+ /** BCP-47 language code the speaker is using. Defaults to 'en'. */
39
+ language?: string;
40
+ /**
41
+ * Domain words to bias recognition toward — product names, jargon, anything
42
+ * a general model would mishear. A default set of programming terms is
43
+ * always included; these are added on top.
44
+ */
45
+ extraKeyterms?: string[];
46
+ /**
47
+ * Ask the service to also stream interim guesses. Off by default: interim
48
+ * text flickers and rewrites itself, which is fine for a live caption and
49
+ * distracting in a text field.
50
+ */
51
+ typedInterims?: boolean;
52
+ }
53
+ export interface SpeechToTextHandlers {
54
+ /**
55
+ * Called as text arrives. `isFinal` marks the end of an utterance: text with
56
+ * `isFinal: false` will be replaced by a later call, text with `true` is
57
+ * settled and should be appended.
58
+ */
59
+ onTranscript: (text: string, isFinal: boolean) => void;
60
+ /**
61
+ * Called when the stream fails. `fatal` means retrying will not help —
62
+ * typically a rejected token — as opposed to a dropped connection.
63
+ */
64
+ onError: (message: string, info?: {
65
+ fatal?: boolean;
66
+ }) => void;
67
+ /** Called once the socket is open and audio can be sent. */
68
+ onOpen?: () => void;
69
+ }
70
+ /** A live transcription stream. Feed it audio; close it when done speaking. */
71
+ export interface SpeechToTextStream {
72
+ /** Send one chunk of raw PCM audio. Ignored once the stream is closing. */
73
+ sendAudio: (chunk: Uint8Array) => void;
74
+ /**
75
+ * Stop the stream, giving the service a moment to flush the last words.
76
+ * Resolves once the socket is closed.
77
+ */
78
+ close: () => Promise<void>;
79
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Wire types for Claude's speech-to-text stream.
3
+ *
4
+ * These mirror what the service actually sends. The endpoint is undocumented,
5
+ * so the shapes here are what we observed rather than a published contract —
6
+ * treat every field as possibly absent and never assume a message type we do
7
+ * not know about is an error.
8
+ */
9
+ /** Audio the service expects: raw 16-bit PCM, 16 kHz, mono. */
10
+ export const AUDIO_FORMAT = {
11
+ encoding: 'linear16',
12
+ sampleRate: 16000,
13
+ channels: 1,
14
+ };
15
+ /** Messages the service sends down the socket. */
16
+ export var TranscriptMessageType;
17
+ (function (TranscriptMessageType) {
18
+ /** A partial guess that will still change. Show it, do not commit it. */
19
+ TranscriptMessageType["TranscriptInterim"] = "TranscriptInterim";
20
+ /** Improved text for the utterance in progress. Still not final. */
21
+ TranscriptMessageType["TranscriptText"] = "TranscriptText";
22
+ /** The speaker paused: whatever we have accumulated is now final. */
23
+ TranscriptMessageType["TranscriptEndpoint"] = "TranscriptEndpoint";
24
+ /** Transcription failed. `description` carries the reason. */
25
+ TranscriptMessageType["TranscriptError"] = "TranscriptError";
26
+ /** Generic server-side failure. `message` carries the reason. */
27
+ TranscriptMessageType["Error"] = "error";
28
+ })(TranscriptMessageType || (TranscriptMessageType = {}));
29
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/stt/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,+DAA+D;AAC/D,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,QAAQ,EAAE,UAAU;IACpB,UAAU,EAAE,KAAK;IACjB,QAAQ,EAAE,CAAC;CACH,CAAC;AAEX,kDAAkD;AAClD,MAAM,CAAN,IAAY,qBAWX;AAXD,WAAY,qBAAqB;IAC/B,yEAAyE;IACzE,gEAAuC,CAAA;IACvC,oEAAoE;IACpE,0DAAiC,CAAA;IACjC,qEAAqE;IACrE,kEAAyC,CAAA;IACzC,8DAA8D;IAC9D,4DAAmC,CAAA;IACnC,iEAAiE;IACjE,wCAAe,CAAA;AACjB,CAAC,EAXW,qBAAqB,KAArB,qBAAqB,QAWhC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@swttch/extend-kit",
3
- "version": "0.3.0",
4
- "description": "Companion tools for Swttch that live outside the plugin bundle: Claude Code account/usage SDK and the ccb CLI.",
3
+ "version": "0.5.0",
4
+ "description": "Companion tools for Swttch that live outside the plugin bundle: Claude Code account/usage SDK, the ccb CLI, and speech-to-text.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -14,6 +14,10 @@
14
14
  "types": "./dist/battery/index.d.ts",
15
15
  "default": "./dist/battery/index.js"
16
16
  },
17
+ "./stt": {
18
+ "types": "./dist/stt/index.d.ts",
19
+ "default": "./dist/stt/index.js"
20
+ },
17
21
  "./cli": "./dist/cli/index.js"
18
22
  },
19
23
  "bin": {
@@ -23,7 +27,7 @@
23
27
  "build": "tsc",
24
28
  "dev": "tsc --watch",
25
29
  "lint": "tsc --noEmit",
26
- "test": "node --test dist/**/*.test.js"
30
+ "test": "node --test \"dist/**/*.test.js\""
27
31
  },
28
32
  "keywords": [
29
33
  "claude",
@@ -52,6 +56,10 @@
52
56
  },
53
57
  "devDependencies": {
54
58
  "@types/node": "^25.5.0",
59
+ "@types/ws": "^8.18.1",
55
60
  "typescript": "^6.0.2"
61
+ },
62
+ "dependencies": {
63
+ "ws": "^8.21.3"
56
64
  }
57
65
  }