Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/agents/cli-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Pass a flag only through layers that consume it:
MCP and AI SDK tool schemas thus exclude parameters for the model to write that input.
For a shared common key, declare the same audience in its `packages/command-surface/src/common-input-fields.ts` row instead.
5. If Node.js exposes the option, update the public typed client option in the owning `packages/contracts/src/client-*.ts` type.
6. If the request still needs public-to-internal translation, update daemon flag normalization in `src/client/client-normalizers.ts`.
6. If the request still needs public-to-internal translation, update daemon flag normalization in `packages/client/src/client-normalizers.ts`.
7. If the field enters platform dispatch, add it to `packages/daemon/src/core/context.ts` and `packages/daemon/src/core/dispatch-context.ts`.
8. Only after the command surface, grammar, and projection prove applicability, pass the option through handler/platform modules.
9. In `scripts/integration-progress-model.ts`, classify the flag as device-observable or intentionally-outside.
Expand Down
1 change: 1 addition & 0 deletions examples/sdk/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
{ "path": "../../packages/ad-script" },
{ "path": "../../packages/apple-runner" },
{ "path": "../../packages/capture-kit" },
{ "path": "../../packages/client" },
{ "path": "../../packages/command-registry" },
{ "path": "../../packages/command-runtime" },
{ "path": "../../packages/command-surface" },
Expand Down
2 changes: 1 addition & 1 deletion fallow-baselines/health.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
"packages/daemon/src/core/daemon-command-registry.ts:high impact",
"packages/daemon/src/sessions/handlers/session-doctor-output.ts:high impact",
"packages/platform-apple/src/core/perf-xml.ts:high impact",
"src/client/screenshot-result.ts:high impact",
"packages/client/src/screenshot-result.ts:high impact",
"packages/platform-web/src/json-utils.ts:high impact",
"packages/platform-android/src/settings.ts:complexity",
"packages/capture-kit/src/snapshot/snapshot-presentation/text-surface.ts:high impact",
Expand Down
2 changes: 1 addition & 1 deletion oxlint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const LEGACY_FILE_LINE_CAPS = {
'packages/platform-android/src/__tests__/snapshot.test.ts': 1416,
'packages/apple-runner/src/__tests__/index-command-retry.test.ts': 1064,
'packages/apple-runner/src/__tests__/runner-session.test.ts': 1456,
'src/__tests__/agent-device-client.test.ts': 1271,
'packages/client/src/__tests__/index.test.ts': 1264,
'src/__tests__/cli-client-commands.test.ts': 1234,
'src/__tests__/cli-config.test.ts': 1282,
'src/__tests__/cli-network.test.ts': 1092,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@
"@agent-device/ad-script": "workspace:*",
"@agent-device/apple-runner": "workspace:*",
"@agent-device/capture-kit": "workspace:*",
"@agent-device/client": "workspace:*",
"@agent-device/command-registry": "workspace:*",
"@agent-device/command-runtime": "workspace:*",
"@agent-device/command-surface": "workspace:*",
Expand Down
28 changes: 28 additions & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@agent-device/client",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "The typed Node.js client factory: createAgentDeviceClient builds the AgentDeviceClient over the daemon transport, with request normalizers, lease client, and screenshot result shaping. Internal workspace package bundled into the published agent-device artifact.",
"dependencies": {
"@agent-device/command-registry": "workspace:*",
"@agent-device/command-surface": "workspace:*",
"@agent-device/contracts": "workspace:*",
"@agent-device/daemon-client": "workspace:*",
"@agent-device/daemon-contracts": "workspace:*",
"@agent-device/host-kit": "workspace:*",
"@agent-device/kernel": "workspace:*",
"@agent-device/platform-apple": "workspace:*",
"@agent-device/react-native": "workspace:*"
},
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"devDependencies": {
"@agent-device/test-kit": "workspace:*",
"vitest": "5.0.4"
}
Comment thread
thymikee marked this conversation as resolved.
Resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
normalizeDeviceSelection,
normalizeOpenDevice,
normalizeSession,
} from './client-normalizers.ts';
} from '../client-normalizers.ts';
import { PUBLIC_PLATFORMS } from '@agent-device/kernel/device';

test('normalizeOpenDevice accepts exactly the canonical leaf platforms', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
normalizeDevice,
normalizeOpenForegroundComposition,
normalizeRuntimeHints,
} from './client-normalizers.ts';
} from '../client-normalizers.ts';

test.each(['ios', 'android', 'harmonyos'])('runtime response preserves %s platform', (platform) => {
expect(normalizeRuntimeHints({ platform, launchUrl: 'demo://open' })).toMatchObject({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import { isRequestCanceledError } from '@agent-device/kernel/errors';
import { createAgentDeviceClient } from '../agent-device-client.ts';
import { createAgentDeviceClient } from '../index.ts';
import type { AgentDeviceDaemonTransportContext } from '@agent-device/contracts/client';
import type { DaemonRequest, DaemonResponse } from '@agent-device/kernel/contracts';
import { createTransport } from './client-transport-fixture.ts';
import { createClientTransport } from '@agent-device/test-kit';

function canceledWith(error: unknown, dispatched: 'no' | 'unknown'): boolean {
return (
Expand All @@ -24,7 +24,7 @@ function canceledWith(error: unknown, dispatched: 'no' | 'unknown'): boolean {
}

test('a client call with an already-aborted signal never reaches the transport', async () => {
const { config, calls, transport } = createTransport(() => ({ ok: true, data: {} }));
const { config, calls, transport } = createClientTransport(() => ({ ok: true, data: {} }));
const client = createAgentDeviceClient(config, { transport });
const controller = new AbortController();
controller.abort();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import { createAgentDeviceClient } from '../agent-device-client.ts';
import { createTransport } from './client-transport-fixture.ts';
import { createAgentDeviceClient } from '../index.ts';
import { createClientTransport } from '@agent-device/test-kit';

test('apps.open resolves session device identifiers from open response', async () => {
const setup = createTransport(async (req) => {
const setup = createClientTransport(async (req) => {
if (req.command === 'open') {
return {
ok: true,
Expand Down Expand Up @@ -61,7 +61,7 @@ test('apps.open resolves session device identifiers from open response', async (
});

test('apps.open resolves an open response that names no host device or paths', async () => {
const setup = createTransport(async (req) => {
const setup = createClientTransport(async (req) => {
if (req.command === 'open') {
return {
ok: true,
Expand Down Expand Up @@ -93,7 +93,7 @@ test('apps.open resolves an open response that names no host device or paths', a
test('apps.open reports an answered launch confirmation only when the daemon answered one', async () => {
let launchConfirmation: unknown = 'accepted';
let strayReason: unknown = undefined;
const setup = createTransport(async (req) => {
const setup = createClientTransport(async (req) => {
if (req.command === 'open') {
return {
ok: true,
Expand Down Expand Up @@ -136,7 +136,7 @@ test('apps.open reports an unanswered launch confirmation with its typed reason'
},
] as const;
for (const { reason, warning } of cases) {
const setup = createTransport(async (req) => {
const setup = createClientTransport(async (req) => {
if (req.command === 'open') {
return {
ok: true,
Expand All @@ -162,7 +162,7 @@ test('apps.open reports an unanswered launch confirmation with its typed reason'
});

test('apps.open drops an unrecognized launch confirmation outcome instead of guessing', async () => {
const setup = createTransport(async (req) => {
const setup = createClientTransport(async (req) => {
if (req.command === 'open') {
return {
ok: true,
Expand All @@ -185,7 +185,7 @@ test('apps.open drops an unrecognized launch confirmation outcome instead of gue
});

test('apps.open keeps an unanswered outcome whose reason predates this client', async () => {
const setup = createTransport(async (req) => {
const setup = createClientTransport(async (req) => {
if (req.command === 'open') {
return {
ok: true,
Expand Down Expand Up @@ -213,7 +213,7 @@ test('apps.open reports a typed probe-failure step and drops an unrecognized one
{ step: 'teleporter', reported: false },
] as const;
for (const { step, reported } of responses) {
const setup = createTransport(async (req) => {
const setup = createClientTransport(async (req) => {
if (req.command === 'open') {
return {
ok: true,
Expand Down Expand Up @@ -252,7 +252,7 @@ test('apps.open preserves the full initialSnapshotError shape through client nor
logPath: '/tmp/agent-device/sessions/qa/requests/snap.ndjson',
retriable: true,
};
const setup = createTransport(async (req) => {
const setup = createClientTransport(async (req) => {
if (req.command === 'open') {
return {
ok: true,
Expand Down Expand Up @@ -282,7 +282,7 @@ test('apps.open preserves the full initialSnapshotError shape through client nor
});

test('apps.open drops a malformed initialSnapshotError instead of projecting garbage', async () => {
const setup = createTransport(async () => ({
const setup = createClientTransport(async () => ({
ok: true,
data: {
session: 'qa',
Expand All @@ -305,7 +305,7 @@ test('apps.open drops a malformed initialSnapshotError instead of projecting gar
});

test('apps.open forwards explicit runtime hints through the daemon request', async () => {
const setup = createTransport(async () => ({
const setup = createClientTransport(async () => ({
ok: true,
data: {
session: 'qa',
Expand Down Expand Up @@ -344,7 +344,7 @@ test('apps.open forwards explicit runtime hints through the daemon request', asy
});

test('apps.installFromSource forwards source payload and normalizes launch identity', async () => {
const setup = createTransport(async () => ({
const setup = createClientTransport(async () => ({
ok: true,
data: {
packageName: 'com.example.demo',
Expand Down Expand Up @@ -398,7 +398,7 @@ test('apps.installFromSource forwards source payload and normalizes launch ident
});

test('apps.installFromSource derives Android launchTarget from packageName when daemon omits it', async () => {
const setup = createTransport(async () => ({
const setup = createClientTransport(async () => ({
ok: true,
data: {
packageName: 'com.example.package-name-only',
Expand Down Expand Up @@ -436,7 +436,7 @@ test('apps.installFromSource derives Android launchTarget from packageName when
});

test('apps.installFromSource forwards GitHub Actions artifact sources unchanged', async () => {
const setup = createTransport(async () => ({
const setup = createClientTransport(async () => ({
ok: true,
data: {
packageName: 'com.example.ci',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
AgentDeviceDaemonTransportContext,
} from '@agent-device/contracts/client';
import type { DaemonRequest } from '@agent-device/kernel/contracts';
import { createAgentDeviceClient } from '../agent-device-client.ts';
import { createAgentDeviceClient } from '../index.ts';

test('client sends daemon auth only through the typed transport context', async () => {
const requests: Array<Omit<DaemonRequest, 'token'>> = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
DaemonBinding,
} from '@agent-device/contracts/client';
import { AppError } from '@agent-device/kernel/errors';
import { createAgentDeviceClient } from '../agent-device-client.ts';
import { createAgentDeviceClient } from '../index.ts';

// A pin on one daemon instance is a credential and an identity, so the client's job is to read it once
// and hand it to every request of that client, and to refuse one that names no instance.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
AgentDeviceDaemonTransportContext,
DaemonLifecycle,
} from '@agent-device/contracts/client';
import { createAgentDeviceClient } from '../agent-device-client.ts';
import { createAgentDeviceClient } from '../index.ts';

// `daemonLifecycle` is a client choice that only the transport can act on, so the client's job is to
// hand it over exactly and to refuse a value it does not know.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import { createAgentDeviceClient } from '../agent-device-client.ts';
import { createTransport } from './client-transport-fixture.ts';
import { createAgentDeviceClient } from '../index.ts';
import { createClientTransport } from '@agent-device/test-kit';

test.for(['named', 'structured'] as const)(
'%s tap sends its contacts and series as structured gesture input',
async (surface) => {
const setup = createTransport(async () => ({ ok: true, data: { message: 'Tapped' } }));
const setup = createClientTransport(async () => ({ ok: true, data: { message: 'Tapped' } }));
const client = createAgentDeviceClient(setup.config, { transport: setup.transport });

const input = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { describe, expect, test } from 'vitest';
import type { CliFlags } from '@agent-device/contracts/command';
import { readInputFromCli, runCommand } from '@agent-device/command-surface';
import { createAgentDeviceClient } from './agent-device-client.ts';
import { createTransport } from './__tests__/client-transport-fixture.ts';
import { createAgentDeviceClient } from '../index.ts';
import { createClientTransport } from '@agent-device/test-kit';

const NO_FLAGS = {} as CliFlags;

Expand Down Expand Up @@ -49,7 +49,10 @@ describe('gesture command transport', () => {
},
},
])('$argv sends structured input through the client transport', async ({ argv, input }) => {
const setup = createTransport(() => ({ ok: true, data: { message: 'Performed gesture' } }));
const setup = createClientTransport(() => ({
ok: true,
data: { message: 'Performed gesture' },
}));
const client = createAgentDeviceClient(setup.config, { transport: setup.transport });
const parsed = readInputFromCli('gesture', argv, NO_FLAGS);
const result = await runCommand(client, 'gesture', {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import { createAgentDeviceClient } from '../agent-device-client.ts';
import { createTransport } from './client-transport-fixture.ts';
import { createAgentDeviceClient } from '../index.ts';
import { createClientTransport } from '@agent-device/test-kit';

const RETIRED = /readinessTimeoutMs was removed; add an explicit wait for the target/;

test('client commands refuse the retired readinessTimeoutMs before sending anything', async () => {
const setup = createTransport(async () => ({ ok: true, data: {} }));
const setup = createClientTransport(async () => ({ ok: true, data: {} }));
const client = createAgentDeviceClient(setup.config, { transport: setup.transport });

const calls = [
Expand Down Expand Up @@ -42,7 +42,7 @@ test('client commands refuse the retired readinessTimeoutMs before sending anyth
});

test('tap commands still carry post-action observation flags', async () => {
const setup = createTransport(async () => ({ ok: true, data: {} }));
const setup = createClientTransport(async () => ({ ok: true, data: {} }));
const client = createAgentDeviceClient(setup.config, { transport: setup.transport });

await client.interactions.press({ selector: 'label=Foo', settle: true });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import { createAgentDeviceClient } from '../agent-device-client.ts';
import { createTransport } from './client-transport-fixture.ts';
import { createAgentDeviceClient } from '../index.ts';
import { createClientTransport } from '@agent-device/test-kit';

test('lease allocation carries the TestMu device type to the provider flags', async () => {
const setup = createTransport(async (req) => ({
const setup = createClientTransport(async (req) => ({
ok: true,
data: {
lease: {
Expand Down Expand Up @@ -35,7 +35,7 @@ test('lease allocation carries the TestMu device type to the provider flags', as
});

test('lease allocation sends the device spec and returns the device the backend granted', async () => {
const setup = createTransport(async (req) => ({
const setup = createClientTransport(async (req) => ({
ok: true,
data: {
lease: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { afterEach, test, vi } from 'vitest';
import assert from 'node:assert/strict';
import * as daemonResolution from '@agent-device/daemon-contracts/daemon-resolution';
import { createAgentDeviceClient } from './agent-device-client.ts';
import { createTransport } from './__tests__/client-transport-fixture.ts';
import { createAgentDeviceClient } from '../index.ts';
import { createClientTransport } from '@agent-device/test-kit';

afterEach(() => vi.restoreAllMocks());

for (const command of ['open', 'close'] as const) {
test(`${command} succeeds when optional Metro hint scope resolution fails`, async () => {
const setup = createTransport(async () => ({ ok: true, data: {} }));
const setup = createClientTransport(async () => ({ ok: true, data: {} }));
const client = createAgentDeviceClient(setup.config, { transport: setup.transport });
vi.spyOn(daemonResolution, 'resolveDaemonPaths').mockImplementation(() => {
throw new Error('home directory unavailable');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@ import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { test } from 'vitest';
import { mkdtempForTestSync } from '@agent-device/test-kit';
import { createAgentDeviceClient } from '../agent-device-client.ts';
import { createTransport } from './client-transport-fixture.ts';
import { createAgentDeviceClient } from '../index.ts';
import { createClientTransport, mkdtempForTestSync } from '@agent-device/test-kit';

test('closing a session drops its dev-server binding from the client host state dir', async () => {
const stateDir = mkdtempForTestSync('agent-device-client-metro-host-');
const hintsPath = path.join(stateDir, 'metro-sessions', 'qa.json');
fs.mkdirSync(path.dirname(hintsPath), { recursive: true });
fs.writeFileSync(hintsPath, JSON.stringify({ controlBaseUrl: 'http://127.0.0.1:8081' }));
const setup = createTransport(async () => ({ ok: true, data: { session: 'qa' } }));
const setup = createClientTransport(async () => ({ ok: true, data: { session: 'qa' } }));
const client = createAgentDeviceClient(
{ ...setup.config, stateDir: undefined },
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { createServer } from 'node:http';
import type { Socket } from 'node:net';
import path from 'node:path';
import { createAgentDeviceClient } from './agent-device-client.ts';
import { createAgentDeviceClient } from '../index.ts';
import { resolveDaemonPaths } from '@agent-device/daemon-contracts/daemon-resolution';
import {
readMetroSessionFixture,
Expand Down
Loading
Loading