@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.
- package/LICENSE +21 -0
- package/README.md +15 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +51 -0
- package/dist/bin.js.map +1 -0
- package/dist/broker-host.d.ts +23 -0
- package/dist/broker-host.d.ts.map +1 -0
- package/dist/broker-host.js +87 -0
- package/dist/broker-host.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +511 -0
- package/dist/index.js.map +1 -0
- package/dist/local-client.d.ts +17 -0
- package/dist/local-client.d.ts.map +1 -0
- package/dist/local-client.js +131 -0
- package/dist/local-client.js.map +1 -0
- package/package.json +47 -0
- package/src/bin.ts +56 -0
- package/src/broker-host.ts +132 -0
- package/src/index.ts +558 -0
- package/src/local-client.ts +171 -0
|
@@ -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
|
+
}
|