@ours.network/claude-code 1.1.1 → 1.2.0-nightly.1

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.
@@ -3,7 +3,7 @@
3
3
  "name": "ours.network",
4
4
  "displayName": "ours.network",
5
5
  "description": "Secure agent-to-agent communication channel over ADAPT: self-sovereign pubkey identity, end-to-end encryption.",
6
- "version": "1.1.1",
6
+ "version": "1.2.0-nightly.1",
7
7
  "author": {
8
8
  "name": "Adapt Toolkit"
9
9
  },
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # OURS Claude Code plugin
2
+
3
+ The plugin connects Claude's existing native session to the selected OURS server.
4
+ Its host bridge and hooks remain part of the Claude integration; the main MCP
5
+ server runs with the server installation.
6
+
7
+ New plugin invocations select explicit `OURS_CONFIG` first, otherwise the private
8
+ `~/.ours-client/profile.json` when present. With the managed profile, ordinary
9
+ `claude` launches need no environment override. The file contains the existing
10
+ `endpoint`, `expectedInstanceId` and absolute `credentialPath` tuple. Invalid or
11
+ unreadable managed profiles fail without selecting Docker or a local MCP server.
12
+ When neither selection exists, existing unmanaged behavior remains available.
13
+
14
+ An explicit override applies to a new invocation. It does not rebind existing
15
+ sessions or change `OURS_MCP_CONFIG`, application state or native ownership.
16
+ The existing SessionEnd hook remains responsible for native session completion.
@@ -0,0 +1,147 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { createReadStream, createWriteStream, mkdirSync } from 'node:fs';
3
+ import { basename, dirname, resolve } from 'node:path';
4
+ import { spawn, spawnSync } from 'node:child_process';
5
+ import { pipeline } from 'node:stream/promises';
6
+
7
+ function helperInvocation(container, command, args) {
8
+ const entrypoint = container.args.indexOf('/opt/ours/node_modules/@ours.network/mcp/dist/container.js');
9
+ return { ...container, args: [...container.args.slice(0, entrypoint + 2), command, ...args] };
10
+ }
11
+
12
+ function helperJson(container, command, args) {
13
+ const call = helperInvocation(container, command, args);
14
+ const child = spawnSync(call.command, call.args, { env: call.env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000, maxBuffer: 1024 * 1024 });
15
+ if (child.error) throw child.error;
16
+ if (child.status !== 0) throw new Error(child.stderr.trim() || `${command} exited ${child.status}`);
17
+ return JSON.parse(child.stdout);
18
+ }
19
+
20
+ async function runHelper(container, command, args, input, output) {
21
+ const call = helperInvocation(container, command, args);
22
+ const child = spawn(call.command, call.args, { env: call.env, stdio: ['pipe', 'pipe', 'pipe'] });
23
+ let stderr = '';
24
+ child.stderr.setEncoding('utf8');
25
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
26
+ const exited = new Promise((resolveExit, reject) => {
27
+ child.once('error', reject);
28
+ child.once('close', (code, signal) => code === 0 ? resolveExit() : reject(new Error(stderr.trim() || `${command} exited ${signal ?? code}`)));
29
+ });
30
+ const streams = [];
31
+ if (input) streams.push(pipeline(input, child.stdin)); else child.stdin.end();
32
+ if (output) streams.push(pipeline(child.stdout, output)); else child.stdout.resume();
33
+ const results = await Promise.allSettled([...streams, exited]);
34
+ const failed = results.find((result) => result.status === 'rejected');
35
+ if (failed) throw failed.reason;
36
+ }
37
+
38
+ async function stageFile(container, id, hostPath) {
39
+ const abs = resolve(hostPath);
40
+ const call = helperInvocation(container, 'file-stage', [id, basename(abs)]);
41
+ const child = spawn(call.command, call.args, { env: call.env, stdio: ['pipe', 'pipe', 'pipe'] });
42
+ let stdout = '', stderr = '';
43
+ child.stdout.setEncoding('utf8'); child.stdout.on('data', (v) => { stdout += v; });
44
+ child.stderr.setEncoding('utf8'); child.stderr.on('data', (v) => { stderr += v; });
45
+ const exited = new Promise((resolveExit, reject) => {
46
+ child.once('error', reject);
47
+ child.once('close', (code, signal) => code === 0 ? resolveExit() : reject(new Error(stderr.trim() || `file-stage exited ${signal ?? code}`)));
48
+ });
49
+ const results = await Promise.allSettled([pipeline(createReadStream(abs), child.stdin), exited]);
50
+ const failed = results.find((result) => result.status === 'rejected');
51
+ if (failed) throw failed.reason;
52
+ return { path: JSON.parse(stdout).path, abs };
53
+ }
54
+
55
+ function toolError(id, error) {
56
+ return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: `host file transfer failed: ${error instanceof Error ? error.message : String(error)}` }], isError: true } };
57
+ }
58
+
59
+ async function removeTransfer(container, id) {
60
+ try { await runHelper(container, 'file-remove', [id]); } catch { /* preserve the primary result */ }
61
+ }
62
+
63
+ async function eachLine(stream, visit) {
64
+ let pending = Buffer.alloc(0);
65
+ for await (const chunk of stream) {
66
+ pending = Buffer.concat([pending, chunk]);
67
+ let newline;
68
+ while ((newline = pending.indexOf(10)) !== -1) {
69
+ const line = pending.subarray(0, newline + 1);
70
+ pending = pending.subarray(newline + 1);
71
+ await visit(line);
72
+ }
73
+ }
74
+ if (pending.length) await visit(pending);
75
+ }
76
+
77
+ export function runContainerProxy(container) {
78
+ const child = spawn(container.command, container.args, { stdio: ['pipe', 'pipe', 'inherit'], env: container.env });
79
+ const pending = new Map();
80
+ let stopping = false;
81
+ const stopInput = () => { stopping = true; process.stdin.destroy(); };
82
+ child.stdin.on('error', stopInput);
83
+ let outputWrites = Promise.resolve();
84
+ const writeOutput = (bytes) => { outputWrites = outputWrites.then(() => new Promise((done) => process.stdout.write(bytes, done))); return outputWrites; };
85
+
86
+ const inputDone = eachLine(process.stdin, async (raw) => {
87
+ let message;
88
+ try { message = JSON.parse(raw.toString('utf8')); } catch { child.stdin.write(raw); return; }
89
+ // Forward cancellation unchanged. The upstream handler owns cancellation;
90
+ // keep transfer tracking until its actual response so a completed save is
91
+ // copied to the host and its staging directory is removed.
92
+ if (message?.method === 'notifications/cancelled') { child.stdin.write(raw); return; }
93
+ if (message?.method !== 'tools/call' || !message.params?.arguments) { child.stdin.write(raw); return; }
94
+ const name = message.params.name;
95
+ if (name !== 'send_file' && name !== 'save_file') { child.stdin.write(raw); return; }
96
+ const args = message.params.arguments;
97
+ if (name === 'send_file' && !args.path) { child.stdin.write(raw); return; }
98
+ const id = randomUUID();
99
+ try {
100
+ if (name === 'send_file') {
101
+ const staged = await stageFile(container, id, args.path);
102
+ message.params.arguments = { ...args, path: staged.path, filename: args.filename ?? basename(staged.abs) };
103
+ pending.set(message.id, { id, kind: name });
104
+ } else {
105
+ const hostPath = resolve(args.dest_path);
106
+ const target = helperJson(container, 'file-target', [id]);
107
+ message.params.arguments = { ...args, dest_path: target.path };
108
+ pending.set(message.id, { id, kind: name, hostPath, containerPath: target.path });
109
+ }
110
+ if (!stopping) child.stdin.write(`${JSON.stringify(message)}\n`);
111
+ } catch (error) {
112
+ await removeTransfer(container, id);
113
+ await writeOutput(`${JSON.stringify(toolError(message.id, error))}\n`);
114
+ }
115
+ }).catch((error) => {
116
+ if (!stopping) { process.stderr.write(`ours: container MCP input failed: ${error.message}\n`); process.exitCode = 1; child.kill(); }
117
+ }).finally(() => { if (!child.stdin.destroyed) child.stdin.end(); });
118
+
119
+ const outputDone = eachLine(child.stdout, async (raw) => {
120
+ let message;
121
+ try { message = JSON.parse(raw.toString('utf8')); } catch { await writeOutput(raw); return; }
122
+ const transfer = pending.get(message?.id);
123
+ if (!transfer) { await writeOutput(raw); return; }
124
+ pending.delete(message.id);
125
+ try {
126
+ if (transfer.kind === 'save_file' && !message.error && message.result?.isError !== true) {
127
+ mkdirSync(dirname(transfer.hostPath), { recursive: true });
128
+ await runHelper(container, 'file-read', [transfer.id], null, createWriteStream(transfer.hostPath));
129
+ for (const item of message.result?.content ?? []) {
130
+ if (item?.type === 'text' && typeof item.text === 'string') item.text = item.text.split(transfer.containerPath).join(transfer.hostPath);
131
+ }
132
+ }
133
+ await writeOutput(`${JSON.stringify(message)}\n`);
134
+ } catch (error) {
135
+ await writeOutput(`${JSON.stringify(toolError(message.id, error))}\n`);
136
+ } finally { await removeTransfer(container, transfer.id); }
137
+ });
138
+
139
+ child.on('error', (error) => { stopInput(); process.stderr.write(`ours: cannot launch container MCP: ${error.message}\n`); process.exitCode = 1; });
140
+ child.on('close', async (code, signal) => {
141
+ stopInput();
142
+ await Promise.allSettled([inputDone, outputDone, outputWrites]);
143
+ await Promise.allSettled([...pending.values()].map((v) => removeTransfer(container, v.id)));
144
+ if (signal) process.kill(process.pid, signal); else process.exitCode = process.exitCode || (code === null || code < 0 ? 1 : code);
145
+ });
146
+ return child;
147
+ }
@@ -0,0 +1,2 @@
1
+ export function containerInvocation(command: string, args?: string[], env?: NodeJS.ProcessEnv): { command: string; args: string[]; env: NodeJS.ProcessEnv } | null;
2
+ export function readContainerJson(command: string, env?: NodeJS.ProcessEnv): unknown;
@@ -0,0 +1,40 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { isAbsolute, join } from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+
6
+ // An explicit container selection is authoritative: failures never fall back
7
+ // to a host MCP or host application-identity configuration.
8
+ export function containerInvocation(command, args = [], env = process.env) {
9
+ const explicitPath = (env.OURS_CONFIG ?? '').trim();
10
+ const path = explicitPath || join(homedir(), '.ours', 'config.json');
11
+ let profile;
12
+ try { profile = JSON.parse(readFileSync(path, 'utf8')); }
13
+ catch (error) {
14
+ if (!explicitPath && error.code === 'ENOENT') return null;
15
+ throw new Error(`cannot read OURS_CONFIG profile ${path}: ${error.message}`);
16
+ }
17
+ if (!profile || !Object.hasOwn(profile, 'composeFile')) return null;
18
+ if (typeof profile.composeFile !== 'string' || !isAbsolute(profile.composeFile)) throw new Error('composeFile must be an absolute path');
19
+ if (typeof profile.expectedInstanceId !== 'string' || !profile.expectedInstanceId.trim()) throw new Error('container profile requires expectedInstanceId');
20
+ if (profile.composeProject !== undefined && (typeof profile.composeProject !== 'string' || !profile.composeProject.trim())) throw new Error('composeProject must be a nonempty string');
21
+ const dockerArgs = ['compose', '-f', profile.composeFile];
22
+ if (profile.composeProject !== undefined) dockerArgs.push('-p', profile.composeProject);
23
+ dockerArgs.push('exec', '-T');
24
+ for (const name of ['CLAUDE_CODE_SESSION_ID', 'OURS_BIND_IDENTITY']) {
25
+ if (env[name] !== undefined) dockerArgs.push('-e', `${name}=${env[name]}`);
26
+ }
27
+ dockerArgs.push('daemon', 'node', '/opt/ours/node_modules/@ours.network/mcp/dist/container.js', profile.expectedInstanceId, command, ...args);
28
+ return { command: 'docker', args: dockerArgs, env: { ...env, OURS_DAEMON_ID: profile.expectedInstanceId } };
29
+ }
30
+
31
+ export function readContainerJson(command, env = process.env) {
32
+ const invocation = containerInvocation(command, [], env);
33
+ if (!invocation) return null;
34
+ const child = spawnSync(invocation.command, invocation.args, { env: invocation.env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000, maxBuffer: 1024 * 1024 });
35
+ if (child.error) throw child.error;
36
+ if (child.status !== 0) throw new Error(`ours container ${command} failed: ${child.stderr.trim() || child.status}`);
37
+ const value = JSON.parse(child.stdout);
38
+ if (value === null) throw new Error(`ours container ${command} returned null`);
39
+ return value;
40
+ }
package/bin/proxy.mjs CHANGED
@@ -1,90 +1,97 @@
1
1
  #!/usr/bin/env node
2
- // Resolves @ours.network/mcp (the MCP server dependency) and runs its
3
- // daemon CLI in proxy mode, forwarding the stdio MCP channel unchanged.
4
- //
5
- // Claude Code installs a plugin's npm dependency in one of two layouts,
6
- // depending on its version. Either inside this plugin's own per-version
7
- // node_modules (reachable by Node's default walk-up from this file), or in a
8
- // SHARED `~/.claude/plugins/npm-cache/node_modules` tree that sits as a SIBLING
9
- // of the plugin cache root — which a plain walk-up can never reach. We try the
10
- // default resolution first, then fall back to candidate node_modules roots (any
11
- // `npm-cache` found by walking up from this file, plus the documented
12
- // CLAUDE_PLUGIN_DATA dir) so the proxy launches under either layout.
13
2
  import { createRequire } from 'node:module';
14
3
  import { spawn } from 'node:child_process';
15
- import { fileURLToPath } from 'node:url';
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+ import { homedir } from 'node:os';
16
6
  import { dirname, join } from 'node:path';
17
- import { existsSync } from 'node:fs';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ import { containerInvocation } from './container-launch.mjs';
10
+ import { runContainerProxy } from './container-file-transfer.mjs';
18
11
 
12
+ const sessionEnd = process.argv[2] === 'session-end';
13
+ const watch = process.argv[2] === 'watch';
14
+ const command = sessionEnd ? 'session-end' : 'proxy';
15
+ const forwarded = process.argv.slice(sessionEnd ? 3 : 2);
16
+ const env = { ...process.env };
19
17
  const require = createRequire(import.meta.url);
20
- const SPEC = '@ours.network/mcp/dist/cli.js';
21
18
  const here = dirname(fileURLToPath(import.meta.url));
22
19
 
23
- // Base directories whose `node_modules` may hold the dependency when the
24
- // default walk-up cannot reach it. Each is a directory that *contains* a
25
- // node_modules; require.resolve({paths}) then looks under it.
26
- function fallbackBases() {
20
+ function resolveLocalServer() {
21
+ const spec = '@ours.network/mcp/dist/cli.js';
22
+ try { return require.resolve(spec); } catch { /* try plugin cache layouts */ }
27
23
  const bases = [];
28
- // Walk up from this file looking for a sibling `npm-cache` (the shared
29
- // plugin-dependency tree Claude Code populates on some versions).
30
24
  let dir = here;
31
- for (let i = 0; i < 12; i++) {
25
+ for (let index = 0; index < 12; index += 1) {
32
26
  const shared = join(dir, 'npm-cache');
33
27
  if (existsSync(join(shared, 'node_modules'))) bases.push(shared);
34
28
  const parent = dirname(dir);
35
29
  if (parent === dir) break;
36
30
  dir = parent;
37
31
  }
38
- // Documented per-plugin persistent data dir (SessionStart-installed deps).
39
- const dataDir = process.env.CLAUDE_PLUGIN_DATA;
40
- if (dataDir) bases.push(dataDir);
41
- return bases;
32
+ if (env.CLAUDE_PLUGIN_DATA) bases.push(env.CLAUDE_PLUGIN_DATA);
33
+ for (const base of bases) {
34
+ try { return require.resolve(spec, { paths: [base] }); } catch { /* next */ }
35
+ }
36
+ return null;
42
37
  }
43
38
 
44
- let cliPath;
45
- try {
46
- // Per-version node_modules + ancestor walk-up.
47
- cliPath = require.resolve(SPEC);
48
- } catch {
49
- for (const base of fallbackBases()) {
50
- try {
51
- cliPath = require.resolve(SPEC, { paths: [base] });
52
- break;
53
- } catch {
54
- // try the next candidate
39
+ function relayChild(child, label) {
40
+ child.on('error', (error) => {
41
+ process.stderr.write(`ours: cannot launch ${label}: ${error.message}\n`);
42
+ process.exitCode = 1;
43
+ });
44
+ child.on('exit', (code, signal) => {
45
+ if (signal) process.kill(process.pid, signal);
46
+ else process.exitCode = code ?? 0;
47
+ });
48
+ }
49
+
50
+ async function main() {
51
+ const network = await import('../dist/network-client.mjs');
52
+ const profile = network.hostProfileFromEnv(env);
53
+ if (profile) {
54
+ const configPath = env.OURS_MCP_CONFIG || join(env.HOME || homedir(), '.ours-mcp', 'config.json');
55
+ const hostRecordRoot = dirname(configPath);
56
+ if (sessionEnd) {
57
+ const payload = JSON.parse(readFileSync(0, 'utf8') || '{}');
58
+ if (typeof payload.session_id !== 'string' || !payload.session_id) throw new Error('SessionEnd requires session_id.');
59
+ await network.endNetworkNativeSession({ profile, nativeSessionId: payload.session_id, hostRecordRoot });
60
+ return;
61
+ }
62
+ const nativeSessionId = env.CLAUDE_CODE_SESSION_ID;
63
+ if (watch) {
64
+ await network.runNetworkWatch({ identity: process.argv[3], nativeSessionId, profile, hostRecordRoot, env });
65
+ return;
66
+ }
67
+ await network.runNetworkProxy({ nativeSessionId, profile, hostRecordRoot, env });
68
+ return;
69
+ }
70
+
71
+ const container = containerInvocation(watch ? 'watch' : command, watch ? process.argv.slice(3) : forwarded, env);
72
+ if (container) {
73
+ const child = watch || sessionEnd
74
+ ? spawn(container.command, container.args, { stdio: watch ? ['pipe', 'inherit', 'inherit'] : 'inherit', env: container.env })
75
+ : runContainerProxy(container);
76
+ if (watch) {
77
+ const stop = () => { child.stdin?.end(); if (!child.killed) child.kill('SIGTERM'); };
78
+ process.on('SIGINT', stop);
79
+ process.on('SIGTERM', stop);
55
80
  }
81
+ if (watch || sessionEnd) relayChild(child, 'container MCP');
82
+ return;
56
83
  }
57
- }
58
84
 
59
- if (!cliPath) {
60
- process.stderr.write(
61
- 'ours: cannot resolve @ours.network/mcp (the MCP server dependency). ' +
62
- 'Reinstall the ours plugin so its dependency is installed.\n',
63
- );
64
- process.exit(1);
85
+ const cliPath = resolveLocalServer();
86
+ if (!cliPath) {
87
+ throw new Error('cannot resolve @ours.network/mcp. Install the main MCP server explicitly or select an ours host network profile.');
88
+ }
89
+ if (!env.OURS_CLIENT_PID && process.ppid > 1) env.OURS_CLIENT_PID = String(process.ppid);
90
+ const child = spawn(process.execPath, [cliPath, command, ...forwarded], { stdio: 'inherit', env });
91
+ relayChild(child, 'the proxy');
65
92
  }
66
93
 
67
- // process.ppid here is the MCP client (Claude Code) that launched this shim.
68
- // Pass it so the daemon pid-checks the CLIENT's liveness (which survives idle),
69
- // not the connector's (which Claude tears down on idle).
70
- // Only forward the pid when it is > 1; on macOS, orphaned processes are reparented
71
- // to launchd (pid 1) and pidAlive(1) is always true — skip it in that case.
72
- const env = { ...process.env };
73
- if (!env.OURS_CLIENT_PID && process.ppid > 1) env.OURS_CLIENT_PID = String(process.ppid);
74
- const sessionEnd = process.argv[2] === 'session-end';
75
- const command = sessionEnd ? 'session-end' : 'proxy';
76
- const forwarded = process.argv.slice(sessionEnd ? 3 : 2);
77
- const child = spawn(
78
- process.execPath,
79
- [cliPath, command, ...forwarded],
80
- { stdio: 'inherit', env },
81
- );
82
-
83
- child.on('error', (err) => {
84
- process.stderr.write(`ours: failed to launch the proxy: ${String(err)}\n`);
85
- process.exit(1);
86
- });
87
- child.on('exit', (code, signal) => {
88
- if (signal) process.kill(process.pid, signal);
89
- else process.exit(code ?? 0);
94
+ main().catch((error) => {
95
+ process.stderr.write(`ours: ${error.message}\n`);
96
+ process.exitCode = 1;
90
97
  });