@crosshands/cli 0.1.4

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.
@@ -0,0 +1,131 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { chmod, lstat, mkdir, readFile } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { isAbsolute, join, resolve } from 'node:path';
6
+ import { CONTRACT_VERSIONS, createComputerError } from '@crosshands/contract';
7
+ import { LocalControlClient, brokerEndpoint } from '@crosshands/runtime';
8
+ export function localClientPaths() {
9
+ const uid = process.getuid?.();
10
+ const osIdentity = process.env.CROSSHANDS_OS_IDENTITY ??
11
+ (uid === undefined
12
+ ? `user:${process.env.USERNAME ?? process.env.USER ?? 'unknown'}`
13
+ : `uid:${uid}`);
14
+ const graphicalSessionId = process.env.CROSSHANDS_GRAPHICAL_SESSION_ID ??
15
+ process.env.XDG_SESSION_ID ??
16
+ process.env.SECURITYSESSIONID ??
17
+ process.env.SESSIONNAME ??
18
+ `interactive:${osIdentity}`;
19
+ const sessionKey = createHash('sha256').update(graphicalSessionId).digest('hex').slice(0, 12);
20
+ const runtimeDirectory = process.env.CROSSHANDS_RUNTIME_DIR ?? defaultRuntimeDirectory(sessionKey);
21
+ const identity = { osIdentity, graphicalSessionId };
22
+ return {
23
+ identity,
24
+ runtimeDirectory,
25
+ tokenFile: join(runtimeDirectory, 'control.token'),
26
+ endpoint: brokerEndpoint({
27
+ platform: process.platform,
28
+ osIdentity,
29
+ graphicalSessionId,
30
+ ...(process.platform === 'win32' ? {} : { runtimeDirectory })
31
+ })
32
+ };
33
+ }
34
+ function defaultRuntimeDirectory(sessionKey) {
35
+ if (process.platform === 'darwin') {
36
+ return join(homedir(), 'Library', 'Caches', 'CrossHands', 'runtime', sessionKey);
37
+ }
38
+ if (process.platform === 'linux') {
39
+ const xdgRuntimeDirectory = process.env.XDG_RUNTIME_DIR;
40
+ if (xdgRuntimeDirectory === undefined || xdgRuntimeDirectory.length === 0) {
41
+ throw createComputerError('session_unavailable', 'XDG_RUNTIME_DIR is required for a protected CrossHands broker endpoint');
42
+ }
43
+ return join(xdgRuntimeDirectory, 'crosshands', sessionKey);
44
+ }
45
+ const localAppData = process.env.LOCALAPPDATA;
46
+ if (localAppData === undefined || localAppData.length === 0) {
47
+ throw createComputerError('session_unavailable', 'LOCALAPPDATA is required for CrossHands runtime state on Windows');
48
+ }
49
+ return join(localAppData, 'CrossHands', 'runtime', sessionKey);
50
+ }
51
+ async function prepareRuntimeDirectory(path) {
52
+ await mkdir(path, { recursive: true, mode: 0o700 });
53
+ const info = await lstat(path);
54
+ if (!info.isDirectory() || info.isSymbolicLink())
55
+ throw createComputerError('provider_unavailable', 'CrossHands runtime path is unsafe');
56
+ if (process.getuid !== undefined && info.uid !== process.getuid())
57
+ throw createComputerError('provider_unavailable', 'CrossHands runtime path has another owner');
58
+ await chmod(path, 0o700);
59
+ }
60
+ async function readSecureToken(path) {
61
+ const info = await lstat(path);
62
+ if (!info.isFile() || info.isSymbolicLink() || (info.mode & 0o077) !== 0)
63
+ throw createComputerError('provider_unavailable', 'CrossHands broker token file is unsafe');
64
+ if (process.getuid !== undefined && info.uid !== process.getuid())
65
+ throw createComputerError('provider_unavailable', 'CrossHands broker token has another owner');
66
+ return (await readFile(path, 'utf8')).trim();
67
+ }
68
+ async function connect(paths) {
69
+ const token = paths.endpoint.transport === 'unix' ? await readSecureToken(paths.tokenFile) : '';
70
+ return LocalControlClient.connect({
71
+ endpoint: paths.endpoint,
72
+ token,
73
+ versions: CONTRACT_VERSIONS,
74
+ identity: paths.identity
75
+ });
76
+ }
77
+ function defaultSpawnBroker(entrypoint) {
78
+ const child = spawn(process.execPath, [entrypoint, 'broker'], {
79
+ detached: true,
80
+ stdio: 'ignore',
81
+ windowsHide: true,
82
+ env: process.env
83
+ });
84
+ child.unref();
85
+ }
86
+ function brokerIsAbsent(cause) {
87
+ if (cause === null || typeof cause !== 'object')
88
+ return false;
89
+ const code = cause.code;
90
+ return code === 'ENOENT' || code === 'ECONNREFUSED';
91
+ }
92
+ export async function createProductionBrokerClient(options = {}) {
93
+ const paths = options.paths ?? localClientPaths();
94
+ await prepareRuntimeDirectory(paths.runtimeDirectory);
95
+ try {
96
+ const control = await connect(paths);
97
+ return controlAdapter(control);
98
+ }
99
+ catch (cause) {
100
+ if (!brokerIsAbsent(cause))
101
+ throw cause;
102
+ const rawEntrypoint = options.entrypoint ?? process.argv[1];
103
+ if (rawEntrypoint === undefined)
104
+ throw createComputerError('provider_unavailable', 'Cannot locate the installed CrossHands entrypoint');
105
+ const entrypoint = isAbsolute(rawEntrypoint) ? rawEntrypoint : resolve(rawEntrypoint);
106
+ await (options.spawnBroker ?? defaultSpawnBroker)(entrypoint);
107
+ }
108
+ const deadline = Date.now() + (options.readinessMs ?? 2_500);
109
+ let lastError;
110
+ while (Date.now() < deadline) {
111
+ try {
112
+ // oxlint-disable-next-line no-await-in-loop -- readiness requires ordered retries.
113
+ const control = await connect(paths);
114
+ return controlAdapter(control);
115
+ }
116
+ catch (cause) {
117
+ lastError = cause;
118
+ // Polling is bounded and does not expose token or request data.
119
+ // oxlint-disable-next-line no-await-in-loop -- readiness requires ordered retries.
120
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 50));
121
+ }
122
+ }
123
+ throw createComputerError('provider_unavailable', 'CrossHands broker did not become ready; run `crosshands computer doctor --json`', { cause: lastError instanceof Error ? lastError.message : 'unknown' });
124
+ }
125
+ function controlAdapter(control) {
126
+ return {
127
+ request: async (operation, input) => control.request({ operation, input }, { deadlineMs: 30_000 }),
128
+ close: () => control.close()
129
+ };
130
+ }
131
+ //# sourceMappingURL=local-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-client.js","sourceRoot":"","sources":["../src/local-client.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@crosshands/cli",
3
+ "version": "0.1.4",
4
+ "description": "Agent-agnostic local computer-use CLI",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "crosshands": "dist/bin.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "src",
12
+ "README.md"
13
+ ],
14
+ "type": "module",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./src/index.ts",
18
+ "development": "./src/index.ts",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "dependencies": {
26
+ "@crosshands/contract": "0.1.4",
27
+ "@crosshands/runtime": "0.1.4"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "24.10.1",
31
+ "typescript": "5.9.3",
32
+ "vitest": "4.1.5"
33
+ },
34
+ "optionalDependencies": {
35
+ "@crosshands/platform-darwin": "0.1.4",
36
+ "@crosshands/platform-linux": "0.1.4",
37
+ "@crosshands/platform-windows": "0.1.4"
38
+ },
39
+ "engines": {
40
+ "node": ">=22"
41
+ },
42
+ "scripts": {
43
+ "build": "tsc -p tsconfig.json",
44
+ "typecheck": "tsc -p tsconfig.json --noEmit",
45
+ "test": "vitest run test"
46
+ }
47
+ }
package/src/bin.ts ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { stdin, stderr, stdout } from 'node:process'
4
+ import { StringDecoder } from 'node:string_decoder'
5
+
6
+ import { runBrokerHost } from './broker-host.js'
7
+ import { runCli, type CliIo } from './index.js'
8
+ import { createProductionBrokerClient } from './local-client.js'
9
+
10
+ async function readStdin(): Promise<string> {
11
+ const decoder = new StringDecoder('utf8')
12
+ const maximumLength = 1_000_001
13
+ let value = ''
14
+ for await (const chunk of stdin) {
15
+ const decoded = decoder.write(Buffer.from(chunk))
16
+ if (value.length < maximumLength) {
17
+ value += decoded.slice(0, maximumLength - value.length)
18
+ }
19
+ }
20
+ const final = decoder.end()
21
+ if (value.length < maximumLength) value += final.slice(0, maximumLength - value.length)
22
+ return value
23
+ }
24
+
25
+ async function main(): Promise<number> {
26
+ const argv = process.argv.slice(2)
27
+ if (argv[0] === 'broker') {
28
+ await runBrokerHost()
29
+ return 0
30
+ }
31
+ const io: CliIo = {
32
+ stdin: readStdin,
33
+ stdout: (value) => stdout.write(value),
34
+ stderr: (value) => stderr.write(value)
35
+ }
36
+ const client = await createProductionBrokerClient()
37
+ return runCli(argv, io, client)
38
+ }
39
+
40
+ main()
41
+ .then((code) => {
42
+ process.exitCode = code
43
+ })
44
+ .catch((cause: unknown) => {
45
+ stdout.write(
46
+ `${JSON.stringify({
47
+ error: {
48
+ code: 'provider_unavailable',
49
+ message: cause instanceof Error ? cause.message : 'CrossHands failed to start',
50
+ retry: false,
51
+ remediation: 'run_doctor'
52
+ }
53
+ })}\n`
54
+ )
55
+ process.exitCode = 3
56
+ })
@@ -0,0 +1,132 @@
1
+ import {
2
+ CONTRACT_VERSIONS,
3
+ createComputerError,
4
+ parseOperationInput,
5
+ type ComputerOperationName,
6
+ type ComputerProvider,
7
+ type ReferenceBindings
8
+ } from '@crosshands/contract'
9
+ import {
10
+ LocalBroker,
11
+ LocalControlServer,
12
+ type BrokerEndpoint,
13
+ type LocalControlIdentity,
14
+ type LocalControlRequest,
15
+ type StableAppIdentity,
16
+ type TargetInspection
17
+ } from '@crosshands/runtime'
18
+
19
+ import { localClientPaths } from './local-client.js'
20
+
21
+ type ProviderModule = {
22
+ packageVersion: string
23
+ createProvider(): ComputerProvider
24
+ createControlServer?: (options: {
25
+ endpoint: BrokerEndpoint
26
+ identity: LocalControlIdentity
27
+ handler: (request: LocalControlRequest) => Promise<unknown>
28
+ }) => Promise<{ start(): Promise<void>; close(): Promise<void> }>
29
+ inspectTarget?: (
30
+ operation: ComputerOperationName,
31
+ input: unknown
32
+ ) => Promise<{ bindings: ReferenceBindings; appIdentity: StableAppIdentity } | null>
33
+ }
34
+
35
+ const PLATFORM_PROVIDER_PACKAGES: Readonly<Partial<Record<NodeJS.Platform, string>>> = {
36
+ darwin: '@crosshands/platform-darwin',
37
+ linux: '@crosshands/platform-linux',
38
+ win32: '@crosshands/platform-windows'
39
+ }
40
+
41
+ export function platformProviderPackage(platform: NodeJS.Platform = process.platform): string {
42
+ const packageName = PLATFORM_PROVIDER_PACKAGES[platform]
43
+ if (packageName === undefined) {
44
+ throw new Error(
45
+ `CrossHands has no provider package for ${platform}; run doctor for the supported platform matrix`
46
+ )
47
+ }
48
+ return packageName
49
+ }
50
+
51
+ export async function loadProviderModule(
52
+ platform: NodeJS.Platform = process.platform,
53
+ importer: (specifier: string) => Promise<unknown> = (specifier) => import(specifier)
54
+ ): Promise<ProviderModule> {
55
+ const packageName = platformProviderPackage(platform)
56
+ let loaded: Partial<ProviderModule>
57
+ try {
58
+ loaded = (await importer(packageName)) as Partial<ProviderModule>
59
+ } catch (cause) {
60
+ throw new Error(
61
+ `The matching CrossHands payload ${packageName}@${CONTRACT_VERSIONS.product} is not installed`,
62
+ { cause }
63
+ )
64
+ }
65
+ if (typeof loaded.createProvider !== 'function')
66
+ throw new Error('CrossHands provider module does not export createProvider()')
67
+ if (loaded.packageVersion !== CONTRACT_VERSIONS.product) {
68
+ throw new Error(
69
+ `CrossHands payload version mismatch: main package is ${CONTRACT_VERSIONS.product}, ${packageName} is ${loaded.packageVersion ?? 'unknown'}`
70
+ )
71
+ }
72
+ return loaded as ProviderModule
73
+ }
74
+
75
+ export async function runBrokerHost(): Promise<void> {
76
+ const providerModule = await loadProviderModule()
77
+ const paths = localClientPaths()
78
+ const peer = { ...paths.identity, verified: true, local: true }
79
+ const broker = new LocalBroker({
80
+ identity: peer,
81
+ providerFactory: () => providerModule.createProvider(),
82
+ ...(providerModule.inspectTarget === undefined
83
+ ? {}
84
+ : {
85
+ inspectTarget: (
86
+ operation: ComputerOperationName,
87
+ input: unknown
88
+ ): Promise<TargetInspection | null> => providerModule.inspectTarget!(operation, input)
89
+ })
90
+ })
91
+ await broker.connect({ peer, versions: CONTRACT_VERSIONS })
92
+ const handler = async ({ payload, deadlineAt }: LocalControlRequest): Promise<unknown> => {
93
+ if (payload === null || typeof payload !== 'object') throw new Error('Invalid broker request')
94
+ const record = payload as Record<string, unknown>
95
+ if (typeof record.operation !== 'string') throw new Error('Missing operation')
96
+ const operation = record.operation as ComputerOperationName
97
+ let input: unknown
98
+ try {
99
+ input = parseOperationInput(operation, record.input)
100
+ } catch (cause) {
101
+ if (cause instanceof Error && cause.name === 'ZodError') {
102
+ throw createComputerError('invalid_argument', 'Invalid input for the selected operation')
103
+ }
104
+ throw cause
105
+ }
106
+ return broker.request({ operation, input, deadlineMs: Math.max(1, deadlineAt - Date.now()) })
107
+ }
108
+ const server =
109
+ paths.endpoint.transport === 'named-pipe'
110
+ ? await (providerModule.createControlServer?.({
111
+ endpoint: paths.endpoint,
112
+ identity: paths.identity,
113
+ handler
114
+ }) ??
115
+ Promise.reject(
116
+ new Error('The Windows payload has no native DACL and peer-token control relay')
117
+ ))
118
+ : new LocalControlServer({
119
+ endpoint: paths.endpoint,
120
+ runtimeDirectory: paths.runtimeDirectory,
121
+ tokenFile: paths.tokenFile,
122
+ identity: paths.identity,
123
+ handler
124
+ })
125
+ await server.start()
126
+ await new Promise<void>((resolve) => {
127
+ const stop = (): void => resolve()
128
+ process.once('SIGINT', stop)
129
+ process.once('SIGTERM', stop)
130
+ })
131
+ await server.close()
132
+ }