@sidevoice/uplink 0.2.0 → 0.4.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/skill.mjs ADDED
@@ -0,0 +1,57 @@
1
+ /** `sidevoice skill install|remove|status [--dir <skills dir>]`: the Claude Code skill that joins the room
2
+ * (`/voice-room`). One file; running install again repairs it. A directory of the same name that is not
3
+ * ours is never touched. */
4
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const here = path.dirname(fileURLToPath(import.meta.url));
10
+ export const SKILL_NAME = 'voice-room';
11
+ const MARKER = 'sidevoice: installed copy';
12
+
13
+ export function skillsDir(argv = process.argv.slice(2), env = process.env) {
14
+ const index = argv.indexOf('--dir');
15
+ if (index >= 0 && argv[index + 1]) return path.resolve(argv[index + 1]);
16
+ return path.join(env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude'), 'skills');
17
+ }
18
+
19
+ export function status(dir) {
20
+ const target = path.join(dir, SKILL_NAME);
21
+ const manifest = path.join(target, 'SKILL.md');
22
+ if (!existsSync(target)) return { state: 'absent', target };
23
+ let ours = false;
24
+ try { ours = readFileSync(manifest, 'utf8').includes(MARKER); } catch {}
25
+ return { state: ours ? 'installed' : 'foreign', target };
26
+ }
27
+
28
+ export function install(dir) {
29
+ const current = status(dir);
30
+ if (current.state === 'foreign') throw new Error(`${current.target} already holds a skill that is not Sidevoice's; remove or rename it first.`);
31
+ mkdirSync(current.target, { recursive: true });
32
+ writeFileSync(path.join(current.target, 'SKILL.md'), readFileSync(path.join(here, 'skill', SKILL_NAME, 'SKILL.md'), 'utf8'));
33
+ for (const stale of ['hook.mjs', 'harness-contract.mjs', 'harnesses.mjs', 'harness-claude.mjs', 'harness-codex.mjs', 'harness-http.mjs']) {
34
+ rmSync(path.join(current.target, stale), { force: true }); // an older copy carried a hook runtime; it is gone
35
+ }
36
+ return { ...status(dir), action: current.state === 'installed' ? 'updated' : 'installed' };
37
+ }
38
+
39
+ export function remove(dir) {
40
+ const current = status(dir);
41
+ if (current.state === 'foreign') throw new Error(`${current.target} is not Sidevoice's skill; left as it is.`);
42
+ if (current.state === 'installed') rmSync(current.target, { recursive: true, force: true });
43
+ return { state: 'absent', target: current.target, action: current.state === 'installed' ? 'removed' : 'nothing to remove' };
44
+ }
45
+
46
+ if (process.env.SIDEVOICE_SKILL_MAIN === '1') {
47
+ const [command] = process.argv.slice(2);
48
+ const dir = skillsDir();
49
+ try {
50
+ const result = command === 'install' ? install(dir) : command === 'remove' ? remove(dir) : command === 'status' ? status(dir) : null;
51
+ if (!result) { console.error('usage: sidevoice skill <install|remove|status> [--dir <skills dir>]'); process.exit(2); }
52
+ console.log(`${result.action || result.state}: ${result.target}`);
53
+ if (result.action === 'installed' || result.action === 'updated') {
54
+ console.log('In Claude Code, /voice-room joins the room for that conversation. New sessions see the skill; a session already open needs a restart.');
55
+ }
56
+ } catch (error) { console.error(error.message); process.exit(1); }
57
+ }
package/adapters.mjs DELETED
@@ -1,69 +0,0 @@
1
- /** The last mile, one function per harness. Each takes a binding's delivery target and a room event. */
2
- import net from 'node:net';
3
- import { execFile } from 'node:child_process';
4
-
5
- /** The header the skill expects before the user's literal words. */
6
- export function envelope(event) {
7
- const header = { channel: event.channel === 'room-control' ? 'room-control' : 'voice',
8
- session_id: event.session_id, revision: event.revision, message_id: event.message_id };
9
- return JSON.stringify(header) + '\n\n' + event.text;
10
- }
11
-
12
- /** Claude Code: the session's own inbox socket, inherited by the façade that registered the binding.
13
- * The inbox sends no acknowledgement, so this can never report more than what the wire showed:
14
- * the peer hanging up right after the frames is the one observable sign of a refusal. */
15
- function deliverClaude(delivery, event) {
16
- return new Promise((resolve, reject) => {
17
- const started = Date.now();
18
- const socket = net.createConnection(delivery.socket);
19
- let settled = false, wrote = 0, replied = '';
20
- const finish = (error, status, detail) => {
21
- if (settled) return;
22
- settled = true; clearTimeout(timer); socket.destroy();
23
- if (error) return reject(error);
24
- resolve({ status, detail: `${detail} after ${Date.now() - started}ms${replied ? ', peer said ' + replied.slice(0, 120) : ''}` });
25
- };
26
- const timer = setTimeout(() => finish(null, 'unknown', 'connection still open, no acknowledgement'), 1500);
27
- socket.on('error', error => finish(error));
28
- socket.on('data', chunk => { replied += chunk; });
29
- socket.on('connect', () => {
30
- socket.write(JSON.stringify({ type: 'auth', token: delivery.token }) + '\n');
31
- socket.write(JSON.stringify({ type: 'user', message: { role: 'user', content: envelope(event) } }) + '\n');
32
- wrote = Date.now();
33
- });
34
- // A hang-up right after the frames is how a refused auth or a closed inbox looks from here.
35
- socket.on('close', () => finish(null, 'rejected', wrote ? 'peer closed the connection' : 'peer closed before the frames were written'));
36
- });
37
- }
38
-
39
- /** Codex: `codex queue` enqueues the next user turn on the local app-server daemon. */
40
- function deliverCodex(delivery, event) {
41
- return new Promise((resolve, reject) => {
42
- const binary = process.env.SIDEVOICE_CODEX_BIN || 'codex';
43
- const args = ['queue', '--thread', delivery.thread, '--message', envelope(event)];
44
- execFile(binary, args, { timeout: 30_000, maxBuffer: 1 << 20 }, (error, stdout, stderr) => {
45
- if (error) return reject(new Error((stderr || stdout || error.message).toString().trim().slice(0, 400)));
46
- resolve({ status: 'accepted', detail: 'codex queue confirmed the thread' });
47
- });
48
- });
49
- }
50
-
51
- /** Fallback: an HTTP receiver next to the harness (the slimmed Codex Desktop bridge speaks this). */
52
- async function deliverHttp(delivery, event) {
53
- const response = await fetch(delivery.url, {
54
- method: 'POST', headers: { 'content-type': 'application/json' },
55
- body: JSON.stringify({ thread_id: delivery.thread, text: event.text, message_id: event.message_id,
56
- session_id: event.session_id, revision: event.revision, channel: event.channel || 'voice' }),
57
- signal: AbortSignal.timeout(30_000),
58
- });
59
- if (!response.ok) throw new Error(`Harness delivery failed (${response.status})`);
60
- return { status: 'accepted', detail: `receiver answered ${response.status}` };
61
- }
62
-
63
- export const adapters = { 'claude-uds': deliverClaude, 'codex-queue': deliverCodex, http: deliverHttp };
64
-
65
- export function deliver(delivery, event) {
66
- const adapter = adapters[delivery?.kind];
67
- if (!adapter) throw new Error(`Unknown delivery kind: ${delivery?.kind}`);
68
- return adapter(delivery, event);
69
- }