appduct 0.13.0 → 0.15.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.
Files changed (62) hide show
  1. package/README.md +55 -24
  2. package/dist/artifact-inspect.d.ts +38 -39
  3. package/dist/bin.js +106 -7
  4. package/dist/cli/result-types.d.ts +35 -2
  5. package/dist/cli/routes/daemon/index.js +90 -2
  6. package/dist/cli/routes/daemon/run.js +1088 -517
  7. package/dist/cli/routes/daemon/start.js +98 -2
  8. package/dist/cli/routes/daemon/status.js +99 -2
  9. package/dist/cli/routes/daemon/stop.js +98 -2
  10. package/dist/cli/routes/doctor.js +98 -3
  11. package/dist/cli/routes/events/index.d.ts +0 -3
  12. package/dist/cli/routes/events/index.js +92 -3
  13. package/dist/cli/routes/events/ls.d.ts +5 -0
  14. package/dist/cli/routes/events/ls.js +1837 -0
  15. package/dist/cli/routes/events/since.js +98 -2
  16. package/dist/cli/routes/events/tail.js +98 -2
  17. package/dist/cli/routes/init.js +265 -108
  18. package/dist/cli/routes/keygen.js +90 -2
  19. package/dist/cli/routes/mcp.js +243 -25
  20. package/dist/cli/routes/sessions/index.js +90 -2
  21. package/dist/cli/routes/sessions/link.js +199 -22
  22. package/dist/cli/routes/sessions/ls.js +98 -2
  23. package/dist/cli/routes/sessions/revoke.js +98 -2
  24. package/dist/cli/routes/tools/call.js +98 -2
  25. package/dist/cli/routes/tools/describe.js +98 -2
  26. package/dist/cli/routes/tools/index.js +90 -2
  27. package/dist/cli/routes/tools/ls.js +98 -2
  28. package/dist/client/attach-page.d.ts +39 -0
  29. package/dist/client/bootstrap.d.ts +17 -3
  30. package/dist/client/index.d.ts +2 -1
  31. package/dist/client/index.js +286 -112
  32. package/dist/commands/doctor.d.ts +3 -4
  33. package/dist/commands/events-ls.d.ts +21 -0
  34. package/dist/commands/init.d.ts +8 -2
  35. package/dist/commands/link.d.ts +10 -1
  36. package/dist/daemon/calls.d.ts +3 -1
  37. package/dist/daemon/config.d.ts +6 -0
  38. package/dist/daemon/daemon.d.ts +6 -1
  39. package/dist/daemon/event-bus.d.ts +10 -0
  40. package/dist/daemon/links.d.ts +9 -5
  41. package/dist/daemon/listener.d.ts +28 -1
  42. package/dist/daemon/registry.d.ts +20 -12
  43. package/dist/daemon/rpc-errors.d.ts +6 -0
  44. package/dist/daemon/sessions.d.ts +14 -8
  45. package/dist/daemon/tls.d.ts +0 -3
  46. package/dist/devtools-relay/attach-tab.d.ts +33 -0
  47. package/dist/devtools-relay/index.d.ts +8 -0
  48. package/dist/devtools-relay/memory-daemon-sockets.d.ts +20 -0
  49. package/dist/devtools-relay/memory-devtools-browser.d.ts +10 -0
  50. package/dist/devtools-relay/memory-page-channel.d.ts +16 -0
  51. package/dist/devtools-relay/node-daemon-socket.d.ts +3 -0
  52. package/dist/devtools-relay/node-devtools-browser.d.ts +3 -0
  53. package/dist/devtools-relay/ports.d.ts +48 -0
  54. package/dist/devtools-relay/relay.d.ts +16 -0
  55. package/dist/index.js +106 -7
  56. package/dist/link.d.ts +35 -1
  57. package/dist/mcp/app-tools.d.ts +19 -0
  58. package/dist/mcp/connect-tool.d.ts +15 -2
  59. package/dist/mcp/events-tool.d.ts +39 -0
  60. package/dist/native-scheme.d.ts +11 -7
  61. package/dist/scheme.d.ts +5 -1
  62. package/package.json +15 -5
package/dist/link.d.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  * (`sha256/<44-char-base64>`, may contain `+`/`/`/`=`), so it is percent-encoded here; native/JS
18
18
  * parsers use `URLSearchParams`, which decodes it back.
19
19
  */
20
- import { type AgentEndpoint, type LinkCreateResult } from "@appduct/shared";
20
+ import { type AgentEndpoint, type LinkCreateResult, type WebAttachParams, type WebAttachResult } from "@appduct/shared";
21
21
  import { type ExecFn, type OpenTarget } from "./cli/open-target.js";
22
22
  import { type SpawnFn } from "./rpc/client.js";
23
23
  /**
@@ -30,6 +30,40 @@ import { type SpawnFn } from "./rpc/client.js";
30
30
  * *after* the bootstrap blob, so anything reading `appduct=` must stop at the `&`.
31
31
  */
32
32
  export declare const composeDeepLink: (scheme: string, result: LinkCreateResult) => string;
33
+ /** What a web target hands back instead of a deep link: the page URL to open, and the script to
34
+ * run in an already-open page. Both carry the same bootstrap payload. */
35
+ export type WebLinkResult = {
36
+ sessionId: string;
37
+ /** The page URL with `#appduct=<payload>` appended (replacing any fragment it had). */
38
+ url: string;
39
+ script: string;
40
+ /** Unix seconds. */
41
+ expiresAt: number;
42
+ };
43
+ /** Throws a usage error unless `pageUrl` is an http(s) URL. Called before a link is minted, so a
44
+ * bad URL does not strand a pending link. */
45
+ export declare const assertWebPageUrl: (pageUrl: string) => void;
46
+ /** Composes the web target's result from a `link.create` result minted with `transport: "web"`. */
47
+ export declare const composeWebLink: (pageUrl: string, result: LinkCreateResult) => WebLinkResult;
48
+ export type MintWebLinkOptions = {
49
+ stateDir: string;
50
+ spawn?: SpawnFn;
51
+ autoSpawn?: boolean;
52
+ ttlSeconds?: number;
53
+ /** The page the link will be opened in. */
54
+ url: string;
55
+ };
56
+ /** Mints a link only the web listener accepts. Needs no scheme: a web page is reached by URL. */
57
+ export declare const mintWebLink: (options: MintWebLinkOptions) => Promise<WebLinkResult>;
58
+ export type AttachWebTabOptions = WebAttachParams & {
59
+ stateDir: string;
60
+ spawn?: SpawnFn;
61
+ autoSpawn?: boolean;
62
+ };
63
+ /** Asks the daemon to attach a tab of the debugging-port browser at `browserUrl` and claim a session on it. */
64
+ export declare const attachWebTab: ({ stateDir, spawn, autoSpawn, ...params }: AttachWebTabOptions) => Promise<WebAttachResult & {
65
+ attached: true;
66
+ }>;
33
67
  export type MintLinkOptions = {
34
68
  stateDir: string;
35
69
  spawn?: SpawnFn;
@@ -107,12 +107,30 @@ export type ResolvedAppTool = {
107
107
  descriptor: ListedToolDescriptor;
108
108
  policy: EffectivePolicyDecision;
109
109
  };
110
+ type ResolvedSession = {
111
+ sessionId: string;
112
+ alias: string;
113
+ };
110
114
  export type CallToolArgs = {
111
115
  selector?: string;
112
116
  name: string;
113
117
  args: Record<string, unknown>;
114
118
  timeoutMs?: number;
115
119
  };
120
+ /** Every key an agent sends must be one the tool declares. A misspelled key (`arguments` for
121
+ * `args`, `timeout_ms` for `timeoutMs`) would otherwise be dropped silently, and the tool would run
122
+ * without what the agent meant to pass. */
123
+ export declare const rejectUnknownKeys: (args: Record<string, unknown>, tool: string, allowed: readonly string[]) => void;
124
+ /** `null` counts as absent for every optional field: some clients fill unset optional
125
+ * parameters with `null` rather than leaving them out. */
126
+ export declare const asOptionalString: (value: unknown, field: string) => string | undefined;
127
+ /** Resolves `selector` (an alias, a session id, or nothing) to one concrete session first, with
128
+ * the daemon's own rules and errors (`no_session`, `ambiguous_session`, `unknown_session`).
129
+ * Everything after that is routed by the **session id**, never the alias: the daemon frees an
130
+ * alias when a session ends and gives it to the next device of the same model, so routing by alias
131
+ * could land a call — one the user may already have approved — on a different device. If the
132
+ * session goes away mid-call, the next daemon call fails with `unknown_session` instead. */
133
+ export declare const resolveSession: (call: DaemonCall, selector: string | undefined) => Promise<ResolvedSession>;
116
134
  export declare const handleListToolsTool: (rawArgs: unknown, call: DaemonCall) => Promise<{
117
135
  groups?: import("@appduct/shared").ToolGroupSummary[] | undefined;
118
136
  tools: {
@@ -152,3 +170,4 @@ export declare const parseCallToolArgs: (rawArgs: unknown) => CallToolArgs;
152
170
  */
153
171
  export declare const resolveCallDeadline: (tool: ResolvedAppTool, requestedTimeoutMs: number | undefined) => number;
154
172
  export declare const resolveAppTool: (call: DaemonCall, selector: string | undefined, name: string) => Promise<ResolvedAppTool>;
173
+ export {};
@@ -11,7 +11,9 @@
11
11
  * caller asked for it with `target: "none"`), where it carries explicit instructions telling the
12
12
  * agent to show the code and ask.
13
13
  */
14
+ import { type WebAttachResult } from "@appduct/shared";
14
15
  import { type ExecFn, type OpenTarget } from "../cli/open-target.js";
16
+ import { type WebLinkResult } from "../link.js";
15
17
  import { type SpawnFn } from "../rpc/client.js";
16
18
  import type { DaemonCall } from "./daemon-tools.js";
17
19
  export declare const CONNECT_TOOL_NAME = "appduct_connect";
@@ -24,7 +26,16 @@ export declare const CONNECT_TOOL_DESCRIPTOR: {
24
26
  readonly properties: {
25
27
  readonly target: {
26
28
  readonly type: "string";
27
- readonly enum: readonly ["android", "ios-sim", "ios-device", "none"];
29
+ readonly enum: readonly ["android", "ios-sim", "ios-device", "none", "web"];
30
+ };
31
+ readonly url: {
32
+ readonly type: "string";
33
+ };
34
+ readonly browserUrl: {
35
+ readonly type: "string";
36
+ };
37
+ readonly targetId: {
38
+ readonly type: "string";
28
39
  };
29
40
  readonly device: {
30
41
  readonly type: "string";
@@ -108,7 +119,9 @@ export type ConnectToolResult = {
108
119
  /** Present only on the QR path: what the agent must do before waiting. See {@link QR_INSTRUCTIONS}. */
109
120
  instructions?: string;
110
121
  };
111
- export declare const handleConnectTool: (rawArgs: unknown, deps: ConnectToolDeps) => Promise<ConnectToolResult>;
122
+ export declare const handleConnectTool: (rawArgs: unknown, deps: ConnectToolDeps) => Promise<ConnectToolResult | WebLinkResult | (WebAttachResult & {
123
+ attached: true;
124
+ })>;
112
125
  export type WaitForSessionToolDeps = {
113
126
  stateDir: string;
114
127
  spawn?: SpawnFn;
@@ -12,12 +12,40 @@
12
12
  import { type AppEvent } from "@appduct/shared";
13
13
  import { type SpawnFn } from "../rpc/client.js";
14
14
  import type { DaemonCall } from "./daemon-tools.js";
15
+ export declare const LIST_EVENTS_TOOL_NAME = "appduct_list_events";
15
16
  export declare const EVENTS_TOOL_NAME = "appduct_events";
16
17
  export declare const WAIT_FOR_EVENT_TOOL_NAME = "appduct_wait_for_event";
17
18
  /** Server-side cap on `timeoutMs` (issue #6's "Cap timeoutMs server-side below the idle window
18
19
  * rather than trusting the caller") — a pure function so the cap's boundary behavior is
19
20
  * unit-testable without spinning up a daemon or actually waiting out either bound. */
20
21
  export declare const clampWaitForEventTimeoutMs: (requestedMs: number | undefined) => number;
22
+ export declare const LIST_EVENTS_TOOL_DESCRIPTOR: {
23
+ readonly name: "appduct_list_events";
24
+ readonly description: string;
25
+ readonly inputSchema: {
26
+ readonly type: "object";
27
+ readonly properties: {
28
+ readonly selector: {
29
+ readonly type: "string";
30
+ readonly minLength: 1;
31
+ readonly description: "Session alias or id. Omit to target the sole active/suspended session.";
32
+ };
33
+ readonly name: {
34
+ readonly type: "string";
35
+ readonly minLength: 1;
36
+ };
37
+ readonly limit: {
38
+ readonly type: "integer";
39
+ readonly exclusiveMinimum: 0;
40
+ };
41
+ readonly offset: {
42
+ readonly type: "integer";
43
+ readonly minimum: 0;
44
+ };
45
+ };
46
+ readonly additionalProperties: false;
47
+ };
48
+ };
21
49
  export declare const EVENTS_TOOL_DESCRIPTOR: {
22
50
  readonly name: "appduct_events";
23
51
  readonly description: string;
@@ -78,6 +106,17 @@ export declare const WAIT_FOR_EVENT_TOOL_DESCRIPTOR: {
78
106
  export type EventsToolDeps = {
79
107
  call: DaemonCall;
80
108
  };
109
+ export declare const handleListEventsTool: (rawArgs: unknown, call: DaemonCall) => Promise<{
110
+ session: string;
111
+ total: number;
112
+ limit: {};
113
+ events: {
114
+ payload_schema?: import("@appduct/shared").EventSchemaDescriptor | undefined;
115
+ name: string;
116
+ signature: string;
117
+ description: string;
118
+ }[];
119
+ }>;
81
120
  export type EventsToolResult = {
82
121
  events: AppEvent[];
83
122
  cursor: number;
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Static-file scheme discovery for plain iOS (Xcode) and Android (Gradle) apps
3
- * (docs/tasks/20-cli-native-scheme-discovery.md, phase 3 of issue #48).
2
+ * Static-file scheme discovery for plain iOS (Xcode) and Android (Gradle) apps.
4
3
  *
5
4
  * `scheme.ts`'s `resolveScheme` treats this module as one more thing to try after `<cwd>/app.json`
6
5
  * — it never walks up (an app root is where these commands run, same rule as `app.json`), and,
@@ -8,6 +7,8 @@
8
7
  * `xcodebuild`, no Gradle evaluation. Every probe is a plain read of a well-known project file,
9
8
  * parsed defensively:
10
9
  *
10
+ * - Flutter (a root with a `pubspec.yaml`): the Android probes below read `android/app/…` instead
11
+ * of `app/…`; the iOS walk already reaches `ios/Runner` and `macos/Runner`.
11
12
  * - Android: `app/build.gradle.kts` / `app/build.gradle` for a `manifestPlaceholders["appductScheme"]`
12
13
  * (or `.appductScheme =`) assignment, then `app/src/main/AndroidManifest.xml` for the first
13
14
  * `<data android:scheme>` inside an intent filter that also declares
@@ -35,19 +36,22 @@ export type NativeSchemeProbeResult = {
35
36
  origin: string;
36
37
  };
37
38
  export type NativeSchemeDiscovery = {
38
- /** `undefined` when no probe resolved a scheme. */
39
- result?: NativeSchemeProbeResult;
39
+ /** Every probe that resolved a scheme, in probe order (all agree on the scheme, or discovery
40
+ * throws). Empty when none did. */
41
+ results: NativeSchemeProbeResult[];
40
42
  /** Every probe's human-readable description, in order, for {@link describeMissingScheme}-style
41
43
  * reporting — always as many entries as there are probes, whether or not any of them hit. */
42
44
  tried: string[];
43
45
  };
46
+ /** Whether `root` is a Flutter project root: it has a `pubspec.yaml`. */
47
+ export declare const isFlutterProject: (root: string) => Promise<boolean>;
44
48
  /**
45
49
  * Runs every static-file probe against `root` (never walking up — same rule as `app.json`) and
46
50
  * decides the result:
47
51
  *
48
- * - No probe resolves a scheme: `result` is `undefined`.
49
- * - Every probe that resolves one agrees: that scheme wins, attributed to whichever probe ran
50
- * first (the order in {@link PROBES}).
52
+ * - No probe resolves a scheme: `results` is empty.
53
+ * - Every probe that resolves one agrees: `results` holds each of them in the order of
54
+ * {@link PROBES}, so a caller can see every platform the project spans, not one winner.
51
55
  * - Two probes resolve *different* schemes: throws a usage error naming both sources and their
52
56
  * values, since guessing which one is right would silently target the wrong app on one
53
57
  * platform.
package/dist/scheme.d.ts CHANGED
@@ -39,7 +39,7 @@
39
39
  * redirects the state directory — `--state-dir` / `APPDUCT_STATE_DIR` remain the only way to do
40
40
  * that — so a project file can never move the daemon's key, socket or audit log.
41
41
  */
42
- import { type NativeSchemeSource } from "./native-scheme.js";
42
+ import { type NativeSchemeProbeResult, type NativeSchemeSource } from "./native-scheme.js";
43
43
  /** The directory a project-level config lives in, relative to an app root. */
44
44
  export declare const PROJECT_CONFIG_DIR = ".appduct";
45
45
  /** The project-level config filename, inside {@link PROJECT_CONFIG_DIR}. */
@@ -85,6 +85,10 @@ export type StaticProjectSchemeDiscovery = {
85
85
  * it) — distinct from `tried` below, which lists every location whether or not it hit. Only set
86
86
  * alongside `scheme`. */
87
87
  origin?: string;
88
+ /** Every native-project probe that resolved a scheme, in probe order — `scheme`, `source` and
89
+ * `origin` are the first of them. Empty when `app.json` answered (the native probes do not run)
90
+ * or nothing did; the platforms a project spans are read from here. */
91
+ nativeResults: NativeSchemeProbeResult[];
88
92
  /** Every location this step consulted, in order: `app.json` first (unchanged from pre-#48), then
89
93
  * every `native-scheme.ts` probe — always present, hit or miss, one entry each. */
90
94
  tried: string[];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "appduct",
3
- "version": "0.13.0",
4
- "description": "Driving React Native tools without in-app debug UI.",
3
+ "version": "0.15.0",
4
+ "description": "Call functions inside your running iOS, Android or React Native app from a terminal, test or AI agent.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -24,6 +24,8 @@
24
24
  },
25
25
  "keywords": [
26
26
  "appduct",
27
+ "ios",
28
+ "android",
27
29
  "react-native",
28
30
  "agent"
29
31
  ],
@@ -45,14 +47,22 @@
45
47
  "reflect-metadata": "^0.2.2",
46
48
  "toqr": "^0.1.1",
47
49
  "ws": "^8.18.3",
48
- "@appduct/shared": "0.13.0"
50
+ "@appduct/shared": "0.15.0"
49
51
  },
50
52
  "devDependencies": {
51
53
  "@types/node": "^22.10.0",
54
+ "@types/react": "~19.1.17",
52
55
  "@types/ws": "^8.18.1",
53
56
  "esbuild": "^0.25.12",
54
57
  "eslint": "^10",
55
- "typescript": "~5.9.2"
58
+ "playwright-core": "1.56.1",
59
+ "react": "19.1.0",
60
+ "react-dom": "19.1.0",
61
+ "typescript": "~5.9.2",
62
+ "vite": "^8.1.4",
63
+ "webpack": "^5.111.1",
64
+ "@appduct/react-native": "0.15.0",
65
+ "@appduct/web": "0.15.0"
56
66
  },
57
67
  "files": [
58
68
  "bin.js",
@@ -62,7 +72,7 @@
62
72
  "build": "tsc -p tsconfig.build.json && node scripts/bundle.mjs",
63
73
  "clean": "rm -rf dist",
64
74
  "lint": "eslint src",
65
- "test": "vitest run --config ../../vitest.config.ts src/__tests__",
75
+ "test": "vitest run src/__tests__",
66
76
  "typecheck": "tsc -p tsconfig.json"
67
77
  }
68
78
  }