tapline-host 1.5.0 → 1.6.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.
- package/dist/accounts.d.ts +72 -0
- package/dist/accounts.js +208 -0
- package/dist/accounts.js.map +1 -0
- package/dist/actionlog.d.ts +20 -0
- package/dist/actionlog.js.map +1 -1
- package/dist/animations.d.ts +39 -0
- package/dist/animations.js +124 -0
- package/dist/animations.js.map +1 -0
- package/dist/api.d.ts +43 -0
- package/dist/api.js +154 -0
- package/dist/api.js.map +1 -0
- package/dist/audit.d.ts +1 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +19 -0
- package/dist/bin.js.map +1 -0
- package/dist/brief.d.ts +41 -0
- package/dist/brief.js +78 -0
- package/dist/brief.js.map +1 -0
- package/dist/cli.d.ts +41 -1
- package/dist/cli.js +97 -54
- package/dist/cli.js.map +1 -1
- package/dist/conditions.d.ts +36 -0
- package/dist/conditions.js +67 -0
- package/dist/conditions.js.map +1 -0
- package/dist/confirm.d.ts +21 -2
- package/dist/confirm.js +39 -6
- package/dist/confirm.js.map +1 -1
- package/dist/dispatch.d.ts +27 -0
- package/dist/dispatch.js +114 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +20 -0
- package/dist/doctor.js.map +1 -1
- package/dist/drive.d.ts +184 -0
- package/dist/drive.js +908 -0
- package/dist/drive.js.map +1 -0
- package/dist/elicit.d.ts +90 -0
- package/dist/elicit.js +179 -0
- package/dist/elicit.js.map +1 -0
- package/dist/format.d.ts +12 -0
- package/dist/format.js +20 -4
- package/dist/format.js.map +1 -1
- package/dist/images.d.ts +8 -0
- package/dist/images.js +26 -6
- package/dist/images.js.map +1 -1
- package/dist/inspect.d.ts +1 -0
- package/dist/journey.d.ts +18 -1
- package/dist/journey.js +32 -0
- package/dist/journey.js.map +1 -1
- package/dist/journeyreport.js +4 -2
- package/dist/journeyreport.js.map +1 -1
- package/dist/mapcmd.d.ts +200 -0
- package/dist/mapcmd.js +412 -0
- package/dist/mapcmd.js.map +1 -0
- package/dist/mapfile.d.ts +155 -0
- package/dist/mapfile.js +575 -0
- package/dist/mapfile.js.map +1 -0
- package/dist/mcp.d.ts +14 -12
- package/dist/mcp.js +50 -344
- package/dist/mcp.js.map +1 -1
- package/dist/media.d.ts +2 -0
- package/dist/media.js +21 -2
- package/dist/media.js.map +1 -1
- package/dist/nochange.d.ts +77 -0
- package/dist/nochange.js +77 -0
- package/dist/nochange.js.map +1 -0
- package/dist/places.d.ts +239 -0
- package/dist/places.js +823 -0
- package/dist/places.js.map +1 -0
- package/dist/policy.d.ts +18 -0
- package/dist/policy.js +52 -0
- package/dist/policy.js.map +1 -1
- package/dist/protocol.d.ts +23 -1
- package/dist/protocol.js +23 -0
- package/dist/protocol.js.map +1 -1
- package/dist/reflexes.d.ts +59 -0
- package/dist/reflexes.js +133 -0
- package/dist/reflexes.js.map +1 -0
- package/dist/report.d.ts +32 -2
- package/dist/report.js +154 -9
- package/dist/report.js.map +1 -1
- package/dist/routes.d.ts +115 -0
- package/dist/routes.js +287 -0
- package/dist/routes.js.map +1 -0
- package/dist/run.d.ts +28 -1
- package/dist/run.js +32 -3
- package/dist/run.js.map +1 -1
- package/dist/runtime.d.ts +60 -0
- package/dist/runtime.js +255 -0
- package/dist/runtime.js.map +1 -0
- package/dist/script.d.ts +114 -10
- package/dist/script.js +30 -2
- package/dist/script.js.map +1 -1
- package/dist/server.d.ts +46 -2
- package/dist/server.js +80 -13
- package/dist/server.js.map +1 -1
- package/dist/thincall.d.ts +13 -0
- package/dist/thincall.js +335 -0
- package/dist/thincall.js.map +1 -0
- package/dist/tool-text-data.d.ts +7 -5
- package/dist/tool-text-data.js +8 -6
- package/dist/tool-text-data.js.map +1 -1
- package/dist/tool-text.d.ts +1 -1
- package/dist/tools.d.ts +373 -6
- package/dist/tools.js +1889 -195
- package/dist/tools.js.map +1 -1
- package/dist/uxcheck.d.ts +1 -0
- package/package.json +2 -2
- package/skills/tapline/SKILL.md +50 -11
- 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
|
package/dist/api.js.map
ADDED
|
@@ -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
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
|
package/dist/bin.js.map
ADDED
|
@@ -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"}
|
package/dist/brief.d.ts
ADDED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|