tapline-host 1.5.0 → 1.6.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.
Files changed (110) hide show
  1. package/dist/accounts.d.ts +72 -0
  2. package/dist/accounts.js +208 -0
  3. package/dist/accounts.js.map +1 -0
  4. package/dist/actionlog.d.ts +20 -0
  5. package/dist/actionlog.js.map +1 -1
  6. package/dist/animations.d.ts +39 -0
  7. package/dist/animations.js +124 -0
  8. package/dist/animations.js.map +1 -0
  9. package/dist/api.d.ts +43 -0
  10. package/dist/api.js +154 -0
  11. package/dist/api.js.map +1 -0
  12. package/dist/audit.d.ts +1 -0
  13. package/dist/bin.d.ts +2 -0
  14. package/dist/bin.js +19 -0
  15. package/dist/bin.js.map +1 -0
  16. package/dist/brief.d.ts +41 -0
  17. package/dist/brief.js +78 -0
  18. package/dist/brief.js.map +1 -0
  19. package/dist/cli.d.ts +41 -1
  20. package/dist/cli.js +97 -54
  21. package/dist/cli.js.map +1 -1
  22. package/dist/conditions.d.ts +36 -0
  23. package/dist/conditions.js +67 -0
  24. package/dist/conditions.js.map +1 -0
  25. package/dist/confirm.d.ts +21 -2
  26. package/dist/confirm.js +39 -6
  27. package/dist/confirm.js.map +1 -1
  28. package/dist/dispatch.d.ts +27 -0
  29. package/dist/dispatch.js +114 -0
  30. package/dist/dispatch.js.map +1 -0
  31. package/dist/doctor.d.ts +5 -0
  32. package/dist/doctor.js +20 -0
  33. package/dist/doctor.js.map +1 -1
  34. package/dist/drive.d.ts +184 -0
  35. package/dist/drive.js +908 -0
  36. package/dist/drive.js.map +1 -0
  37. package/dist/elicit.d.ts +90 -0
  38. package/dist/elicit.js +179 -0
  39. package/dist/elicit.js.map +1 -0
  40. package/dist/format.d.ts +12 -0
  41. package/dist/format.js +20 -4
  42. package/dist/format.js.map +1 -1
  43. package/dist/images.d.ts +8 -0
  44. package/dist/images.js +26 -6
  45. package/dist/images.js.map +1 -1
  46. package/dist/inspect.d.ts +1 -0
  47. package/dist/journey.d.ts +18 -1
  48. package/dist/journey.js +32 -0
  49. package/dist/journey.js.map +1 -1
  50. package/dist/journeyreport.js +4 -2
  51. package/dist/journeyreport.js.map +1 -1
  52. package/dist/mapcmd.d.ts +200 -0
  53. package/dist/mapcmd.js +412 -0
  54. package/dist/mapcmd.js.map +1 -0
  55. package/dist/mapfile.d.ts +155 -0
  56. package/dist/mapfile.js +575 -0
  57. package/dist/mapfile.js.map +1 -0
  58. package/dist/mcp.d.ts +14 -12
  59. package/dist/mcp.js +50 -344
  60. package/dist/mcp.js.map +1 -1
  61. package/dist/media.d.ts +2 -0
  62. package/dist/media.js +21 -2
  63. package/dist/media.js.map +1 -1
  64. package/dist/nochange.d.ts +77 -0
  65. package/dist/nochange.js +77 -0
  66. package/dist/nochange.js.map +1 -0
  67. package/dist/places.d.ts +239 -0
  68. package/dist/places.js +823 -0
  69. package/dist/places.js.map +1 -0
  70. package/dist/policy.d.ts +18 -0
  71. package/dist/policy.js +52 -0
  72. package/dist/policy.js.map +1 -1
  73. package/dist/protocol.d.ts +23 -1
  74. package/dist/protocol.js +23 -0
  75. package/dist/protocol.js.map +1 -1
  76. package/dist/reflexes.d.ts +59 -0
  77. package/dist/reflexes.js +133 -0
  78. package/dist/reflexes.js.map +1 -0
  79. package/dist/report.d.ts +32 -2
  80. package/dist/report.js +154 -9
  81. package/dist/report.js.map +1 -1
  82. package/dist/routes.d.ts +115 -0
  83. package/dist/routes.js +287 -0
  84. package/dist/routes.js.map +1 -0
  85. package/dist/run.d.ts +28 -1
  86. package/dist/run.js +32 -3
  87. package/dist/run.js.map +1 -1
  88. package/dist/runtime.d.ts +60 -0
  89. package/dist/runtime.js +255 -0
  90. package/dist/runtime.js.map +1 -0
  91. package/dist/script.d.ts +114 -10
  92. package/dist/script.js +30 -2
  93. package/dist/script.js.map +1 -1
  94. package/dist/server.d.ts +46 -2
  95. package/dist/server.js +80 -13
  96. package/dist/server.js.map +1 -1
  97. package/dist/thincall.d.ts +13 -0
  98. package/dist/thincall.js +335 -0
  99. package/dist/thincall.js.map +1 -0
  100. package/dist/tool-text-data.d.ts +7 -5
  101. package/dist/tool-text-data.js +8 -6
  102. package/dist/tool-text-data.js.map +1 -1
  103. package/dist/tool-text.d.ts +1 -1
  104. package/dist/tools.d.ts +373 -6
  105. package/dist/tools.js +1889 -195
  106. package/dist/tools.js.map +1 -1
  107. package/dist/uxcheck.d.ts +1 -0
  108. package/package.json +2 -2
  109. package/skills/tapline/SKILL.md +50 -11
  110. package/skills/tapline/references/setup.md +6 -1
package/dist/api.js ADDED
@@ -0,0 +1,154 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ /** The most a call's JSON may be. */
3
+ export const MAX_API_BODY = 1024 * 1024;
4
+ export function safeEqual(a, b) {
5
+ const left = Buffer.from(a, "utf8");
6
+ const right = Buffer.from(b, "utf8");
7
+ if (left.length !== right.length)
8
+ return false;
9
+ return timingSafeEqual(left, right);
10
+ }
11
+ /** An address as one spelling: no brackets, lower case, and an IPv4-mapped IPv6 address as plain IPv4. */
12
+ export function normalizeAddress(address) {
13
+ const plain = address.toLowerCase().replace(/^\[|\]$/g, "");
14
+ return plain.startsWith("::ffff:") && plain.includes(".") ? plain.slice("::ffff:".length) : plain;
15
+ }
16
+ /**
17
+ * The host's proof that it holds the token: an HMAC of the caller's fresh challenge and of the address and port the request
18
+ * arrived on, keyed by the token. They are in it so that a listener on another port, or on another address with the same
19
+ * port (::1 against 127.0.0.1), can't relay the probe to the real host and hand its proof back: a proof made for one
20
+ * endpoint never verifies for another. The token itself never leaves; thincall.ts computes the same value from the socket
21
+ * it connected to and sends the token only to a host that proves it for that endpoint.
22
+ */
23
+ export function hostProof(token, challenge, address, port) {
24
+ return createHmac("sha256", token).update(`tapline-proof-v1\n${challenge}\n${normalizeAddress(address)}\n${port}`).digest("hex");
25
+ }
26
+ export function toApiAnswer(result) {
27
+ const parts = [];
28
+ let image;
29
+ for (const block of result.content) {
30
+ if (block.type === "text")
31
+ parts.push(block.text);
32
+ else if (block.type === "image" && image === undefined)
33
+ image = { mimeType: block.mimeType, dataBase64: block.data };
34
+ }
35
+ return { text: parts.join("\n"), isError: result.isError === true, ...(image !== undefined ? { image } : {}) };
36
+ }
37
+ function plain(res, status, text) {
38
+ res.writeHead(status, { "content-type": "text/plain; charset=utf-8" });
39
+ res.end(`${text}\n`);
40
+ }
41
+ function clientOf(req) {
42
+ const raw = req.headers["x-agent-client"];
43
+ const value = (Array.isArray(raw) ? raw[0] : raw)?.replace(/[^\w .@/-]/g, "").slice(0, 60);
44
+ return value !== undefined && value !== "" ? value : undefined;
45
+ }
46
+ /** Answers `/api/<tool>`; false for any other path. Checks run in the order this computer, method, token. */
47
+ export function handleApi(req, res, context) {
48
+ const path = new URL(req.url ?? "/", "http://host").pathname;
49
+ if (!path.startsWith("/api/"))
50
+ return false;
51
+ if (!context.loopback) {
52
+ plain(res, 403, "Only from this computer.");
53
+ return true;
54
+ }
55
+ if (context.handler === undefined) {
56
+ plain(res, 404, 'The thin call is served by "tapline host" only, not by an agent\'s MCP server.');
57
+ return true;
58
+ }
59
+ if (req.method !== "POST") {
60
+ // A thin call asks who it is talking to before it sends the token: only a host that holds the token can answer.
61
+ const challenge = req.headers["x-tapline-challenge"];
62
+ const port = req.socket.localPort;
63
+ const address = req.socket.localAddress;
64
+ const proof = typeof challenge === "string" && port !== undefined && address !== undefined && /^[0-9a-f]{16,128}$/.test(challenge)
65
+ ? hostProof(context.token, challenge, address, port)
66
+ : undefined;
67
+ res.writeHead(405, { "content-type": "text/plain; charset=utf-8", ...(proof !== undefined ? { "x-tapline-proof": proof } : {}) });
68
+ res.end("POST only.\n");
69
+ return true;
70
+ }
71
+ const auth = req.headers.authorization;
72
+ if (typeof auth !== "string" || !auth.startsWith("Bearer ") || !safeEqual(auth.slice(7).trim(), context.token)) {
73
+ plain(res, 401, "Bearer token required.");
74
+ return true;
75
+ }
76
+ const handler = context.handler;
77
+ let tool;
78
+ try {
79
+ tool = decodeURIComponent(path.slice("/api/".length));
80
+ }
81
+ catch {
82
+ tool = "";
83
+ }
84
+ const client = clientOf(req);
85
+ const chunks = [];
86
+ let size = 0;
87
+ let tooLong = false;
88
+ let gone = false;
89
+ // A client that leaves before its answer is cancelled: queued work for it is never started.
90
+ const cancel = new AbortController();
91
+ res.on("close", () => {
92
+ if (!res.writableEnded)
93
+ cancel.abort();
94
+ });
95
+ // A peer that drops mid-body or resets: stop quietly, never throw an unhandled error event.
96
+ req.on("error", () => {
97
+ gone = true;
98
+ cancel.abort();
99
+ if (!res.writableEnded)
100
+ res.destroy();
101
+ });
102
+ req.on("aborted", () => {
103
+ gone = true;
104
+ cancel.abort();
105
+ });
106
+ req.on("data", (chunk) => {
107
+ size += chunk.length;
108
+ if (size > MAX_API_BODY)
109
+ tooLong = true;
110
+ else
111
+ chunks.push(chunk);
112
+ });
113
+ req.on("end", () => {
114
+ if (gone || (req.destroyed && !req.complete))
115
+ return;
116
+ let args;
117
+ try {
118
+ const text = Buffer.concat(chunks).toString("utf8").trim();
119
+ const parsed = tooLong ? undefined : text === "" ? {} : JSON.parse(text);
120
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed))
121
+ args = parsed;
122
+ }
123
+ catch {
124
+ args = undefined;
125
+ }
126
+ if (args === undefined) {
127
+ plain(res, 400, "Body: a JSON object of at most 1 MB.");
128
+ return;
129
+ }
130
+ handler(tool, args, client, cancel.signal).then((result) => {
131
+ if (cancel.signal.aborted)
132
+ return; // nobody is waiting for the answer
133
+ if (result === undefined) {
134
+ plain(res, 404, `Unknown tool "${tool}".`);
135
+ return;
136
+ }
137
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
138
+ res.end(JSON.stringify(toApiAnswer(result)));
139
+ }, (error) => {
140
+ if (cancel.signal.aborted)
141
+ return;
142
+ if (res.headersSent) {
143
+ res.end();
144
+ return;
145
+ }
146
+ // Same shape as a tool's own failure, so a caller reads one kind of answer.
147
+ const message = error instanceof Error ? error.message : String(error);
148
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
149
+ res.end(JSON.stringify({ text: `Error internal: ${message}`, isError: true }));
150
+ });
151
+ });
152
+ return true;
153
+ }
154
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
package/dist/audit.d.ts CHANGED
@@ -26,6 +26,7 @@ export declare const auditResultSchema: z.ZodObject<{
26
26
  disabled: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
27
27
  selected: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
28
28
  checked: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>>;
29
+ row: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
29
30
  }, z.core.$strip>>>;
30
31
  where: z.ZodOptional<z.ZodObject<{
31
32
  frame: z.ZodOptional<z.ZodObject<{
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ // The `tapline` command. A thin call must start in about 50 ms, so this file loads nothing at the top: `call` goes
3
+ // to thincall.ts (node: modules only) and every other command loads cli.ts (the host, MCP, qrcode, zod) lazily.
4
+ const argv = process.argv.slice(2);
5
+ async function start() {
6
+ if (argv[0] === "call") {
7
+ const { runThinCall } = await import("./thincall.js");
8
+ process.exitCode = await runThinCall(argv.slice(1));
9
+ return;
10
+ }
11
+ const { runMain } = await import("./cli.js");
12
+ runMain(argv);
13
+ }
14
+ start().catch((error) => {
15
+ process.stderr.write(`tapline: ${error instanceof Error ? error.message : String(error)}\n`);
16
+ process.exit(1);
17
+ });
18
+ export {};
19
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,mHAAmH;AACnH,gHAAgH;AAChH,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,KAAK,UAAU,KAAK;IAClB,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC;QACvB,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAC;QACtD,OAAO,CAAC,QAAQ,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACpD,OAAO;IACT,CAAC;IACD,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC;IAC7C,OAAO,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC/B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC7F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,41 @@
1
+ import type { TreeElement } from "./protocol.js";
2
+ /** The most lines a brief answer shows of a change block, or of a new screen (AGENT-TOOLS 1.13 §13.4). */
3
+ export declare const BRIEF_MAX_LINES = 12;
4
+ export interface BriefInput {
5
+ /** The answer's first line. */
6
+ first: string;
7
+ /** The `Place: …` line, when known (task 12 supplies it). */
8
+ place?: string;
9
+ /** `Same screen. Changes:`, shown above the block (not counted in the 12). */
10
+ label?: string;
11
+ /** The change block's lines, in order. */
12
+ changes: string[];
13
+ /** A new screen: its lines replace the change block. */
14
+ screen?: {
15
+ headers: string[];
16
+ actionable: string[];
17
+ newText: string[];
18
+ };
19
+ /** Never cut: `Alert:`, `Network:`, `Keyboard:`, `Place:`, `Note:`, `Error`, `Reflex:` lines and the off-screen count. */
20
+ fixed: string[];
21
+ }
22
+ /** A cut change block keeps `+` text lines ahead of `~` and `↕` lines; the rest keep their order. */
23
+ export declare function orderForCut(changes: string[]): string[];
24
+ /** Builds the brief answer's lines. `…<n> more: look shows them.` ends a cut block. */
25
+ export declare function briefLines(input: BriefInput): string[];
26
+ /**
27
+ * Pressable elements count as actionable: buttons, inputs, switches, links, tabs, list rows, radios, checkboxes,
28
+ * and any element that takes a tap, a toggle or typing (AGENT-TOOLS 1.13.1 item 1).
29
+ */
30
+ export declare function isActionable(element: TreeElement): boolean;
31
+ /**
32
+ * A new screen in brief: its headers, its actionable elements and the text that wasn't in the list the agent had
33
+ * (`previous`). One line each, as a full list writes them, so refs keep working.
34
+ */
35
+ export declare function briefScreen(elements: TreeElement[], previous: TreeElement[], options?: {
36
+ eyes?: boolean;
37
+ }): {
38
+ headers: string[];
39
+ actionable: string[];
40
+ newText: string[];
41
+ };
package/dist/brief.js ADDED
@@ -0,0 +1,78 @@
1
+ import { actionsOf, formatElement, roleOf } from "./format.js";
2
+ /** The most lines a brief answer shows of a change block, or of a new screen (AGENT-TOOLS 1.13 §13.4). */
3
+ export const BRIEF_MAX_LINES = 12;
4
+ /** Lines that close an answer; the others (`Alert:`, `Keyboard:`, `Network:`, `Place:`, the off-screen count) stand above the block. */
5
+ const TAIL = /^(Note\b|Error\b|Reflex:)/;
6
+ /** A `+` line for a text element: `+ e12 text "Wrong password" …`. */
7
+ const ADDED_TEXT = /^\+ (?:e\d+ )?text(?: |$)/;
8
+ /** A cut change block keeps `+` text lines ahead of `~` and `↕` lines; the rest keep their order. */
9
+ export function orderForCut(changes) {
10
+ return [...changes.filter((line) => ADDED_TEXT.test(line)), ...changes.filter((line) => !ADDED_TEXT.test(line))];
11
+ }
12
+ /** The line that ends a cut block. */
13
+ function moreLine(count) {
14
+ return `…${count} more: look shows them.`;
15
+ }
16
+ /** At most `BRIEF_MAX_LINES` of `lines` (already in cut order), then how many were left out. */
17
+ function cut(lines) {
18
+ if (lines.length <= BRIEF_MAX_LINES)
19
+ return lines;
20
+ return [...lines.slice(0, BRIEF_MAX_LINES), moreLine(lines.length - BRIEF_MAX_LINES)];
21
+ }
22
+ /** Builds the brief answer's lines. `…<n> more: look shows them.` ends a cut block. */
23
+ export function briefLines(input) {
24
+ const out = [input.first];
25
+ if (input.place !== undefined)
26
+ out.push(input.place);
27
+ const above = input.fixed.filter((line) => !TAIL.test(line));
28
+ const below = input.fixed.filter((line) => TAIL.test(line));
29
+ if (input.screen !== undefined) {
30
+ // New text first: it is what says an action failed ("Wrong password"), so a cut never takes it.
31
+ out.push(...above, ...cut([...input.screen.newText, ...input.screen.headers, ...input.screen.actionable]));
32
+ }
33
+ else {
34
+ if (input.label !== undefined)
35
+ out.push(input.label);
36
+ out.push(...above);
37
+ out.push(...cut(input.changes.length > BRIEF_MAX_LINES ? orderForCut(input.changes) : input.changes));
38
+ }
39
+ out.push(...below);
40
+ return out;
41
+ }
42
+ /** Roles a person can act on; list rows, radios and checkboxes count with them (AGENT-TOOLS 1.13.1 item 1). */
43
+ const ACTIONABLE_ROLES = new Set(["button", "input", "switch", "link", "tab", "cell", "radio", "checkbox"]);
44
+ /**
45
+ * Pressable elements count as actionable: buttons, inputs, switches, links, tabs, list rows, radios, checkboxes,
46
+ * and any element that takes a tap, a toggle or typing (AGENT-TOOLS 1.13.1 item 1).
47
+ */
48
+ export function isActionable(element) {
49
+ const role = roleOf(element);
50
+ if (role !== null && ACTIONABLE_ROLES.has(role))
51
+ return true;
52
+ return actionsOf(element).some((action) => action === "tap" || action === "toggle" || action === "type");
53
+ }
54
+ function textOfElement(element) {
55
+ return (element.text ?? "").trim();
56
+ }
57
+ /**
58
+ * A new screen in brief: its headers, its actionable elements and the text that wasn't in the list the agent had
59
+ * (`previous`). One line each, as a full list writes them, so refs keep working.
60
+ */
61
+ export function briefScreen(elements, previous, options = {}) {
62
+ const seen = new Set(previous.map(textOfElement).filter((text) => text !== ""));
63
+ const line = (element) => formatElement(element, { eyes: options.eyes === true });
64
+ const headers = [];
65
+ const actionable = [];
66
+ const newText = [];
67
+ for (const element of elements) {
68
+ const role = roleOf(element);
69
+ if (role === "header")
70
+ headers.push(line(element));
71
+ else if (isActionable(element))
72
+ actionable.push(line(element));
73
+ else if (role === "text" && textOfElement(element) !== "" && !seen.has(textOfElement(element)))
74
+ newText.push(line(element));
75
+ }
76
+ return { headers, actionable, newText };
77
+ }
78
+ //# sourceMappingURL=brief.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brief.js","sourceRoot":"","sources":["../src/brief.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts CHANGED
@@ -1,2 +1,42 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ import { type RuntimeOptions } from "./runtime.js";
3
+ /** The command line's words and options, in one place so a test can read them without starting a host. */
4
+ export declare function parseCli(argv: string[]): {
5
+ values: {
6
+ port?: string | undefined;
7
+ host?: string | undefined;
8
+ scheme?: string | undefined;
9
+ screenshots?: string | undefined;
10
+ viewer?: string | undefined;
11
+ "metro-port"?: string | undefined;
12
+ "https-port"?: string | undefined;
13
+ stop?: boolean | undefined;
14
+ wait?: string | undefined;
15
+ since?: string | undefined;
16
+ junit?: string | undefined;
17
+ "update-references"?: boolean | undefined;
18
+ "no-log"?: boolean | undefined;
19
+ policy?: string | undefined;
20
+ "no-policy"?: boolean | undefined;
21
+ "no-dialog"?: boolean | undefined;
22
+ brief?: boolean | undefined;
23
+ "ux-rules"?: string | undefined;
24
+ accounts?: string | undefined;
25
+ maps?: string | undefined;
26
+ "viewer-link"?: boolean | undefined;
27
+ project?: string | undefined;
28
+ "dry-run"?: boolean | undefined;
29
+ app?: boolean | undefined;
30
+ "no-install"?: boolean | undefined;
31
+ help?: boolean | undefined;
32
+ };
33
+ positionals: string[];
34
+ };
35
+ /** What `mcp` and the standalone host both take, from the parsed options (`--brief` and the rest). */
36
+ export declare function runtimeOptionsOf(values: ReturnType<typeof parseCli>["values"], options: {
37
+ port?: string;
38
+ host?: string;
39
+ }): RuntimeOptions;
40
+ export declare function main(argv: string[]): Promise<number>;
41
+ /** Runs the command line and exits with its code (a negative code means a host keeps running). */
42
+ export declare function runMain(argv: string[]): void;
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import { realpathSync, writeFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
2
4
  import { spawn } from "node:child_process";
3
- import { writeFileSync } from "node:fs";
4
5
  import { join } from "node:path";
5
6
  import { parseArgs } from "node:util";
6
7
  import { toString as qrText } from "qrcode";
@@ -11,6 +12,7 @@ import { formatLogs } from "./format.js";
11
12
  import { runInit } from "./init.js";
12
13
  import { runReport } from "./report.js";
13
14
  import { runMcp } from "./mcp.js";
15
+ import { createRuntime } from "./runtime.js";
14
16
  import { createPairCode, forgetLink, pairingLink } from "./pairing.js";
15
17
  import { failureWithDiff, junitXml } from "./script.js";
16
18
  import { TaplineHost } from "./server.js";
@@ -22,7 +24,10 @@ Usage: tapline <command> [options]
22
24
 
23
25
  Commands:
24
26
  mcp Run the host plus an MCP server on stdio (for Claude Code and other agents)
25
- host Run the host in the foreground and log connections and app logs
27
+ host Run a standalone host in the foreground: apps connect to it, and it serves the live view and
28
+ "tapline call" (it takes the options of mcp)
29
+ call Send one tool call to a running "tapline host": tapline call <tool> '<json>' [--client NAME] [--json]
30
+ (exit 0: an answer, 2: a refusal, 3: no host reachable)
26
31
  pair Print a pairing link and QR code for a phone (needs --scheme)
27
32
  info Show this computer's address and how to add Tapline to an app
28
33
  doctor Check the setup (Node, the Wi-Fi address, Tailscale if used, token, ports, firewall);
@@ -48,11 +53,22 @@ Options:
48
53
  --screenshots <inline|file>
49
54
  For mcp: put screenshots in the tool result (default), or save them in
50
55
  ~/.tapline/shots and return the path (or TAPLINE_SCREENSHOTS)
56
+ --client <name> For call: the agent's name, so its report and journey stay its own (default: the parent process id)
57
+ --json For call: print {"text", "image_path"} instead of the text
51
58
  --no-log For mcp: don't write the action log (~/.tapline/log/actions-YYYY-MM-DD.jsonl)
52
59
  --policy <file> For mcp: the safety policy (JSON: words that need a person's OK, words that are blocked)
53
60
  --no-policy For mcp: no confirmation and nothing blocked (a test device with test data)
61
+ --no-dialog For mcp: don't ask a person's OK in the host's dialog, only in the live view (for a host that
62
+ passes dialogs to the model instead of showing them to a person)
63
+ --brief For mcp and host: actions answer briefly (a call's brief: false overrides it); look is never brief
54
64
  --ux-rules <file> For mcp: the user's UX house rules for ux_check and journeys (JSON: text rules to judge,
55
65
  {"max_taps_to_goal": N}, {"way_back": true})
66
+ --accounts <file> For mcp and host: test accounts (JSON; the tool types an account's username and password so the
67
+ agent never sees them). The file must sit outside any git repo, or be ignored by it.
68
+ A password stays out of pictures only while its field is secure (a "show password" toggle shows it).
69
+ Keep the file outside the folders the agent may read
70
+ --maps <dir> For mcp and host: keep each app's map (its places, roads and links) in <dir>/<app id>/ instead of
71
+ ~/.tapline/maps/<app id>/, so a team and both tools can share one folder with a project
56
72
  --viewer-link For mcp: the viewer tool gives the agent the live view's link (benchmarks, scripts); by
57
73
  default it opens the live view in a browser here and keeps the link (its key) from the agent
58
74
  (or TAPLINE_VIEWER_LINK=1)
@@ -99,43 +115,37 @@ async function printPairing(config, url, scheme) {
99
115
  out.write(`\nScan with the phone's camera, then tap Connect in the app (valid 10 minutes, works once):\n\n${qr}\n`);
100
116
  out.write(`${link}\n\nTo unpair the phone, open ${forgetLink(scheme)} on it.\n`);
101
117
  }
102
- async function runHost(config, scheme) {
103
- const host = new TaplineHost({
104
- token: config.token,
105
- port: config.port,
106
- portFallback: config.portFallback,
107
- hosts: config.hosts,
108
- });
118
+ /**
119
+ * The standalone host: apps connect to it, it serves the live viewer (a person's OK is given there) and the thin
120
+ * call, `POST /api/<tool>` (AGENT-TOOLS 1.13 §18.10). It takes the options `mcp` takes.
121
+ */
122
+ async function runHost(options, scheme) {
109
123
  const stamp = () => new Date().toISOString().slice(11, 19);
110
- host.on("connect", (s) => {
111
- // A phone connected through this host is paired too, as with the MCP server and replay (CodeRabbit, PR #5).
112
- markPaired();
113
- console.log(`${stamp()} connected ${s.id} ${s.app.name} ${s.app.version ?? ""} on ${s.device.name ?? s.device.platform}`);
114
- });
115
- host.on("disconnect", (s, reason) => console.log(`${stamp()} disconnected ${s.id} ${s.app.name}: ${reason}`));
124
+ let runtime;
125
+ try {
126
+ runtime = await createRuntime({ ...options, api: true, log: (message) => console.log(`${stamp()} ${message}`) });
127
+ }
128
+ catch (error) {
129
+ for (const line of (error instanceof Error ? error.message : String(error)).split("\n"))
130
+ console.error(line);
131
+ return false;
132
+ }
133
+ const { host, config } = runtime;
116
134
  host.on("log", (s, entry) => console.log(`${stamp()} [${s.app.name}] ${formatLogs([entry])}`));
117
- host.on("warning", (message) => console.warn(`${stamp()} warning: ${message}`));
118
- const result = await host.listen();
119
- const port = host.boundPort() ?? config.port;
120
- const url = hostUrl({ advertisedHost: reachableHost(config, result.listening) }, port);
121
- console.log("Tapline host running.\n");
122
- console.log(` Apps connect to ${url}`);
123
- for (const note of config.notes)
124
- console.log(`\n Note: ${note}`);
125
- for (const { host: h, port: p } of result.listening)
126
- console.log(` listening on ws://${h}:${p}/`);
127
- for (const { host: h, error } of result.failed)
128
- console.warn(` could not listen on ${h}: ${error}`);
135
+ console.log("\nTapline host running.\n");
136
+ console.log(` Apps connect to ${runtime.url}`);
137
+ console.log(` Agents without an MCP connection: tapline call <tool> '<json>' (port ${runtime.port})`);
129
138
  if (scheme !== undefined)
130
- await printPairing(config, url, scheme);
139
+ await printPairing(config, runtime.url, scheme);
131
140
  else
132
- console.log(`\n Pair a phone: tapline pair --scheme <your app's scheme> --port ${port}`);
141
+ console.log(`\n Pair a phone: tapline pair --scheme <your app's scheme> --port ${runtime.port}`);
133
142
  console.log("\nWaiting for apps. Ctrl+C to stop.\n");
134
143
  const stop = () => {
135
- void host.close().finally(() => process.exit(0));
144
+ void runtime.close().finally(() => process.exit(0));
136
145
  };
137
146
  process.once("SIGINT", stop);
138
147
  process.once("SIGTERM", stop);
148
+ return true;
139
149
  }
140
150
  function portOption(raw, name) {
141
151
  if (raw === undefined)
@@ -229,8 +239,9 @@ async function runReplay(config, scripts, waitRaw, junitPath, updateReferences =
229
239
  await host.close();
230
240
  }
231
241
  }
232
- async function main(argv) {
233
- const { values, positionals } = parseArgs({
242
+ /** The command line's words and options, in one place so a test can read them without starting a host. */
243
+ export function parseCli(argv) {
244
+ return parseArgs({
234
245
  args: argv,
235
246
  allowPositionals: true,
236
247
  options: {
@@ -249,7 +260,11 @@ async function main(argv) {
249
260
  "no-log": { type: "boolean" },
250
261
  policy: { type: "string" },
251
262
  "no-policy": { type: "boolean" },
263
+ "no-dialog": { type: "boolean" },
264
+ brief: { type: "boolean" },
252
265
  "ux-rules": { type: "string" },
266
+ accounts: { type: "string" },
267
+ maps: { type: "string" },
253
268
  "viewer-link": { type: "boolean" },
254
269
  project: { type: "string" },
255
270
  "dry-run": { type: "boolean" },
@@ -258,6 +273,31 @@ async function main(argv) {
258
273
  help: { type: "boolean", short: "h" },
259
274
  },
260
275
  });
276
+ }
277
+ /** What `mcp` and the standalone host both take, from the parsed options (`--brief` and the rest). */
278
+ export function runtimeOptionsOf(values, options) {
279
+ return {
280
+ ...options,
281
+ ...(values.screenshots !== undefined ? { screenshots: values.screenshots } : {}),
282
+ ...(values.viewer !== undefined ? { viewer: values.viewer } : {}),
283
+ ...(values["no-log"] === true ? { noLog: true } : {}),
284
+ ...(values.policy !== undefined ? { policy: values.policy } : {}),
285
+ ...(values["no-policy"] === true ? { noPolicy: true } : {}),
286
+ ...(values["no-dialog"] === true ? { noDialog: true } : {}),
287
+ ...(values.brief === true ? { brief: true } : {}),
288
+ ...(values["ux-rules"] !== undefined ? { uxRules: values["ux-rules"] } : {}),
289
+ ...(values.accounts !== undefined ? { accounts: values.accounts } : {}),
290
+ ...(values.maps !== undefined ? { maps: values.maps } : {}),
291
+ ...(values["viewer-link"] === true ? { viewerLink: true } : {}),
292
+ };
293
+ }
294
+ export async function main(argv) {
295
+ // The thin call has its own light entry (bin.ts); this keeps "node dist/cli.js call" working too.
296
+ if (argv[0] === "call") {
297
+ const { runThinCall } = await import("./thincall.js");
298
+ return runThinCall(argv.slice(1));
299
+ }
300
+ const { values, positionals } = parseCli(argv);
261
301
  const command = positionals[0];
262
302
  if (values.help === true || command === undefined || command === "help") {
263
303
  process.stdout.write(HELP);
@@ -267,23 +307,12 @@ async function main(argv) {
267
307
  ...(values.port !== undefined ? { port: values.port } : {}),
268
308
  ...(values.host !== undefined ? { host: values.host } : {}),
269
309
  };
310
+ const runtimeOptions = runtimeOptionsOf(values, options);
270
311
  switch (command) {
271
312
  case "mcp":
272
- return (await runMcp({
273
- ...options,
274
- ...(values.screenshots !== undefined ? { screenshots: values.screenshots } : {}),
275
- ...(values.viewer !== undefined ? { viewer: values.viewer } : {}),
276
- ...(values["no-log"] === true ? { noLog: true } : {}),
277
- ...(values.policy !== undefined ? { policy: values.policy } : {}),
278
- ...(values["no-policy"] === true ? { noPolicy: true } : {}),
279
- ...(values["ux-rules"] !== undefined ? { uxRules: values["ux-rules"] } : {}),
280
- ...(values["viewer-link"] === true ? { viewerLink: true } : {}),
281
- }))
282
- ? -1 // keep running
283
- : 1;
313
+ return (await runMcp(runtimeOptions)) ? -1 /* keep running */ : 1;
284
314
  case "host":
285
- await runHost(await resolveConfig(options), values.scheme);
286
- return -1;
315
+ return (await runHost(runtimeOptions, values.scheme)) ? -1 : 1;
287
316
  case "pair": {
288
317
  if (values.scheme === undefined) {
289
318
  process.stderr.write(`pair needs the app's URL scheme: tapline pair --scheme myapp\n`);
@@ -298,7 +327,7 @@ async function main(argv) {
298
327
  return 0;
299
328
  }
300
329
  case "doctor": {
301
- const results = await runDoctor();
330
+ const results = await runDoctor(values.accounts !== undefined ? { accountsFile: values.accounts } : {});
302
331
  process.stdout.write(`${formatDoctor(results, needsAPerson(results))}\n`);
303
332
  let failed = results.some((r) => r.status === "fail");
304
333
  if (values.app === true) {
@@ -358,11 +387,25 @@ async function main(argv) {
358
387
  return 1;
359
388
  }
360
389
  }
361
- main(process.argv.slice(2)).then((code) => {
362
- if (code >= 0)
363
- process.exit(code);
364
- }, (error) => {
365
- process.stderr.write(`tapline: ${error instanceof Error ? error.message : String(error)}\n`);
366
- process.exit(1);
367
- });
390
+ /** Runs the command line and exits with its code (a negative code means a host keeps running). */
391
+ export function runMain(argv) {
392
+ main(argv).then((code) => {
393
+ if (code >= 0)
394
+ process.exit(code);
395
+ }, (error) => {
396
+ process.stderr.write(`tapline: ${error instanceof Error ? error.message : String(error)}\n`);
397
+ process.exit(1);
398
+ });
399
+ }
400
+ // "node dist/cli.js ..." still works; the tapline bin is bin.js, which loads this file only for commands other than call.
401
+ const entry = process.argv[1];
402
+ if (entry !== undefined) {
403
+ try {
404
+ if (realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url)))
405
+ runMain(process.argv.slice(2));
406
+ }
407
+ catch {
408
+ // not run as a script
409
+ }
410
+ }
368
411
  //# sourceMappingURL=cli.js.map