lynkeus 0.2.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/lib/__tests__/args.test.d.ts +1 -0
- package/lib/__tests__/args.test.js +69 -0
- package/lib/__tests__/check.test.d.ts +1 -0
- package/lib/__tests__/check.test.js +60 -0
- package/lib/__tests__/init.test.d.ts +1 -0
- package/lib/__tests__/init.test.js +42 -0
- package/lib/__tests__/record.test.d.ts +1 -0
- package/lib/__tests__/record.test.js +44 -0
- package/lib/__tests__/snapshot.test.d.ts +1 -0
- package/lib/__tests__/snapshot.test.js +47 -0
- package/lib/__tests__/targets.test.d.ts +1 -0
- package/lib/__tests__/targets.test.js +127 -0
- package/lib/args.d.ts +30 -0
- package/lib/args.js +86 -0
- package/lib/cases/__tests__/format.test.d.ts +1 -0
- package/lib/cases/__tests__/format.test.js +61 -0
- package/lib/cases/__tests__/runner.test.d.ts +1 -0
- package/lib/cases/__tests__/runner.test.js +188 -0
- package/lib/cases/fixtures.d.ts +13 -0
- package/lib/cases/fixtures.js +100 -0
- package/lib/cases/format.d.ts +37 -0
- package/lib/cases/format.js +169 -0
- package/lib/cases/runner.d.ts +61 -0
- package/lib/cases/runner.js +590 -0
- package/lib/cases/templates.d.ts +20 -0
- package/lib/cases/templates.js +156 -0
- package/lib/cli.d.ts +6 -0
- package/lib/cli.js +173 -0
- package/lib/commands/cases.d.ts +1 -0
- package/lib/commands/cases.js +179 -0
- package/lib/commands/check.d.ts +36 -0
- package/lib/commands/check.js +67 -0
- package/lib/commands/crawl/return.d.ts +20 -0
- package/lib/commands/crawl/return.js +125 -0
- package/lib/commands/crawl/state.d.ts +30 -0
- package/lib/commands/crawl/state.js +56 -0
- package/lib/commands/crawl.d.ts +67 -0
- package/lib/commands/crawl.js +213 -0
- package/lib/commands/device.d.ts +10 -0
- package/lib/commands/device.js +122 -0
- package/lib/commands/doctor.d.ts +3 -0
- package/lib/commands/doctor.js +240 -0
- package/lib/commands/drive.d.ts +1 -0
- package/lib/commands/drive.js +410 -0
- package/lib/commands/flake.d.ts +14 -0
- package/lib/commands/flake.js +42 -0
- package/lib/commands/headless.d.ts +1 -0
- package/lib/commands/headless.js +127 -0
- package/lib/commands/init.d.ts +13 -0
- package/lib/commands/init.js +85 -0
- package/lib/commands/map.d.ts +2 -0
- package/lib/commands/map.js +297 -0
- package/lib/commands/os.d.ts +1 -0
- package/lib/commands/os.js +193 -0
- package/lib/commands/overlay.d.ts +9 -0
- package/lib/commands/overlay.js +12 -0
- package/lib/commands/record.d.ts +5 -0
- package/lib/commands/record.js +55 -0
- package/lib/commands/runs.d.ts +1 -0
- package/lib/commands/runs.js +452 -0
- package/lib/commands/smoke.d.ts +28 -0
- package/lib/commands/smoke.js +59 -0
- package/lib/commands/snapshot.d.ts +27 -0
- package/lib/commands/snapshot.js +69 -0
- package/lib/config.d.ts +72 -0
- package/lib/config.js +44 -0
- package/lib/device/devices.d.ts +11 -0
- package/lib/device/devices.js +33 -0
- package/lib/device/driver.d.ts +53 -0
- package/lib/device/driver.js +169 -0
- package/lib/device/platform.d.ts +7 -0
- package/lib/device/platform.js +59 -0
- package/lib/files.d.ts +7 -0
- package/lib/files.js +29 -0
- package/lib/flow/artifacts.d.ts +19 -0
- package/lib/flow/artifacts.js +49 -0
- package/lib/flow/runner.d.ts +13 -0
- package/lib/flow/runner.js +169 -0
- package/lib/flow/types.d.ts +111 -0
- package/lib/flow/types.js +1 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +11 -0
- package/lib/knowledge/__tests__/edges.test.d.ts +1 -0
- package/lib/knowledge/__tests__/edges.test.js +25 -0
- package/lib/knowledge/__tests__/events.test.d.ts +1 -0
- package/lib/knowledge/__tests__/events.test.js +39 -0
- package/lib/knowledge/__tests__/extract.test.d.ts +1 -0
- package/lib/knowledge/__tests__/extract.test.js +69 -0
- package/lib/knowledge/__tests__/frontier.test.d.ts +1 -0
- package/lib/knowledge/__tests__/frontier.test.js +57 -0
- package/lib/knowledge/__tests__/funnels.test.d.ts +1 -0
- package/lib/knowledge/__tests__/funnels.test.js +40 -0
- package/lib/knowledge/__tests__/lint.test.d.ts +1 -0
- package/lib/knowledge/__tests__/lint.test.js +53 -0
- package/lib/knowledge/__tests__/perf.test.d.ts +1 -0
- package/lib/knowledge/__tests__/perf.test.js +34 -0
- package/lib/knowledge/__tests__/plan.test.d.ts +1 -0
- package/lib/knowledge/__tests__/plan.test.js +35 -0
- package/lib/knowledge/__tests__/query.test.d.ts +1 -0
- package/lib/knowledge/__tests__/query.test.js +55 -0
- package/lib/knowledge/__tests__/screens.test.d.ts +1 -0
- package/lib/knowledge/__tests__/screens.test.js +30 -0
- package/lib/knowledge/__tests__/why.test.d.ts +1 -0
- package/lib/knowledge/__tests__/why.test.js +28 -0
- package/lib/knowledge/edges.d.ts +30 -0
- package/lib/knowledge/edges.js +68 -0
- package/lib/knowledge/errors.d.ts +8 -0
- package/lib/knowledge/errors.js +62 -0
- package/lib/knowledge/events.d.ts +23 -0
- package/lib/knowledge/events.js +51 -0
- package/lib/knowledge/extract.d.ts +26 -0
- package/lib/knowledge/extract.js +365 -0
- package/lib/knowledge/frontier.d.ts +26 -0
- package/lib/knowledge/frontier.js +60 -0
- package/lib/knowledge/funnels.d.ts +46 -0
- package/lib/knowledge/funnels.js +103 -0
- package/lib/knowledge/lint.d.ts +16 -0
- package/lib/knowledge/lint.js +89 -0
- package/lib/knowledge/perf.d.ts +32 -0
- package/lib/knowledge/perf.js +91 -0
- package/lib/knowledge/plan.d.ts +22 -0
- package/lib/knowledge/plan.js +81 -0
- package/lib/knowledge/problems.d.ts +5 -0
- package/lib/knowledge/problems.js +8 -0
- package/lib/knowledge/query.d.ts +36 -0
- package/lib/knowledge/query.js +127 -0
- package/lib/knowledge/route-params.d.ts +6 -0
- package/lib/knowledge/route-params.js +42 -0
- package/lib/knowledge/screens.d.ts +30 -0
- package/lib/knowledge/screens.js +125 -0
- package/lib/knowledge/tree.d.ts +15 -0
- package/lib/knowledge/tree.js +63 -0
- package/lib/knowledge/types.d.ts +62 -0
- package/lib/knowledge/types.js +6 -0
- package/lib/knowledge/why.d.ts +24 -0
- package/lib/knowledge/why.js +153 -0
- package/lib/mcp/server.d.ts +4 -0
- package/lib/mcp/server.js +111 -0
- package/lib/registry.d.ts +78 -0
- package/lib/registry.js +113 -0
- package/lib/runtime.d.ts +12 -0
- package/lib/runtime.js +56 -0
- package/lib/targets.d.ts +17 -0
- package/lib/targets.js +43 -0
- package/package.json +47 -0
- package/src/__tests__/args.test.ts +82 -0
- package/src/__tests__/check.test.ts +71 -0
- package/src/__tests__/init.test.ts +52 -0
- package/src/__tests__/record.test.ts +49 -0
- package/src/__tests__/snapshot.test.ts +51 -0
- package/src/__tests__/targets.test.ts +132 -0
- package/src/args.ts +111 -0
- package/src/cases/__tests__/format.test.ts +76 -0
- package/src/cases/__tests__/runner.test.ts +230 -0
- package/src/cases/fixtures.ts +104 -0
- package/src/cases/format.ts +183 -0
- package/src/cases/runner.ts +575 -0
- package/src/cases/templates.ts +159 -0
- package/src/cli.ts +176 -0
- package/src/commands/cases.ts +170 -0
- package/src/commands/check.ts +83 -0
- package/src/commands/crawl/return.ts +130 -0
- package/src/commands/crawl/state.ts +70 -0
- package/src/commands/crawl.ts +262 -0
- package/src/commands/device.ts +119 -0
- package/src/commands/doctor.ts +237 -0
- package/src/commands/drive.ts +413 -0
- package/src/commands/flake.ts +48 -0
- package/src/commands/headless.ts +131 -0
- package/src/commands/init.ts +98 -0
- package/src/commands/map.ts +298 -0
- package/src/commands/os.ts +195 -0
- package/src/commands/overlay.ts +20 -0
- package/src/commands/record.ts +46 -0
- package/src/commands/runs.ts +475 -0
- package/src/commands/smoke.ts +86 -0
- package/src/commands/snapshot.ts +87 -0
- package/src/config.ts +103 -0
- package/src/device/devices.ts +44 -0
- package/src/device/driver.ts +197 -0
- package/src/device/platform.ts +74 -0
- package/src/files.ts +35 -0
- package/src/flow/artifacts.ts +61 -0
- package/src/flow/runner.ts +182 -0
- package/src/flow/types.ts +55 -0
- package/src/index.ts +18 -0
- package/src/knowledge/__tests__/edges.test.ts +34 -0
- package/src/knowledge/__tests__/events.test.ts +45 -0
- package/src/knowledge/__tests__/extract.test.ts +88 -0
- package/src/knowledge/__tests__/frontier.test.ts +77 -0
- package/src/knowledge/__tests__/funnels.test.ts +58 -0
- package/src/knowledge/__tests__/lint.test.ts +66 -0
- package/src/knowledge/__tests__/perf.test.ts +41 -0
- package/src/knowledge/__tests__/plan.test.ts +52 -0
- package/src/knowledge/__tests__/query.test.ts +74 -0
- package/src/knowledge/__tests__/screens.test.ts +37 -0
- package/src/knowledge/__tests__/why.test.ts +40 -0
- package/src/knowledge/edges.ts +91 -0
- package/src/knowledge/errors.ts +73 -0
- package/src/knowledge/events.ts +60 -0
- package/src/knowledge/extract.ts +379 -0
- package/src/knowledge/frontier.ts +82 -0
- package/src/knowledge/funnels.ts +126 -0
- package/src/knowledge/lint.ts +99 -0
- package/src/knowledge/perf.ts +114 -0
- package/src/knowledge/plan.ts +95 -0
- package/src/knowledge/problems.ts +12 -0
- package/src/knowledge/query.ts +146 -0
- package/src/knowledge/route-params.ts +39 -0
- package/src/knowledge/screens.ts +139 -0
- package/src/knowledge/tree.ts +66 -0
- package/src/knowledge/types.ts +60 -0
- package/src/knowledge/why.ts +189 -0
- package/src/mcp/server.ts +109 -0
- package/src/registry.ts +191 -0
- package/src/runtime.ts +54 -0
- package/src/targets.ts +44 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The structure of each route the last time it was read, under
|
|
3
|
+
* .lynkeus/screens/, and in changes.jsonl every testID that appeared,
|
|
4
|
+
* disappeared or moved since: a suite that never asserts on an element still
|
|
5
|
+
* tells you the day it vanished.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { diffScreens } from '../flow/artifacts.js';
|
|
10
|
+
import { appendJsonl, readJsonl, today } from '../files.js';
|
|
11
|
+
// Sub-pixel rounding and font hinting move an element a point or two without any layout change.
|
|
12
|
+
const GEOMETRY_TOLERANCE = 4;
|
|
13
|
+
const presented = (e) => !e.hidden || e.hidden === 'inert';
|
|
14
|
+
const framesOf = (screen) => {
|
|
15
|
+
const out = {};
|
|
16
|
+
for (const e of screen.elements.filter(presented)) {
|
|
17
|
+
if (!e.testId || !e.frame || out[e.testId])
|
|
18
|
+
continue;
|
|
19
|
+
out[e.testId] = { x: Math.round(e.frame.x), y: Math.round(e.frame.y), w: Math.round(e.frame.w), h: Math.round(e.frame.h) };
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
};
|
|
23
|
+
export const diffGeometry = (before, after, tolerance = GEOMETRY_TOLERANCE) => {
|
|
24
|
+
const lines = [];
|
|
25
|
+
for (const [id, a] of Object.entries(after)) {
|
|
26
|
+
const b = before[id];
|
|
27
|
+
if (!b)
|
|
28
|
+
continue;
|
|
29
|
+
const moved = Math.abs(a.x - b.x) > tolerance || Math.abs(a.y - b.y) > tolerance;
|
|
30
|
+
const resized = Math.abs(a.w - b.w) > tolerance || Math.abs(a.h - b.h) > tolerance;
|
|
31
|
+
const collapsed = (a.w === 0 || a.h === 0) && b.w > 0 && b.h > 0;
|
|
32
|
+
if (collapsed)
|
|
33
|
+
lines.push(`#${id} collapsed to ${a.w}×${a.h} (was ${b.w}×${b.h})`);
|
|
34
|
+
else if (moved && resized)
|
|
35
|
+
lines.push(`#${id} moved (${b.x},${b.y})→(${a.x},${a.y}) and resized ${b.w}×${b.h}→${a.w}×${a.h}`);
|
|
36
|
+
else if (moved)
|
|
37
|
+
lines.push(`#${id} moved (${b.x},${b.y})→(${a.x},${a.y})`);
|
|
38
|
+
else if (resized)
|
|
39
|
+
lines.push(`#${id} resized ${b.w}×${b.h}→${a.w}×${a.h}`);
|
|
40
|
+
}
|
|
41
|
+
return lines;
|
|
42
|
+
};
|
|
43
|
+
const dirOf = (lynkeusDir) => path.join(lynkeusDir, 'screens');
|
|
44
|
+
const fileOf = (lynkeusDir, route) => path.join(dirOf(lynkeusDir), `${route.replace(/[^\w.-]/g, '_')}.json`);
|
|
45
|
+
const changesFile = (lynkeusDir) => path.join(dirOf(lynkeusDir), 'changes.jsonl');
|
|
46
|
+
const strip = (screen) => screen.elements.filter(presented).map((e) => {
|
|
47
|
+
const { frame: _frame, ...rest } = e;
|
|
48
|
+
return rest;
|
|
49
|
+
});
|
|
50
|
+
const label = (e) => `${e.kind}${e.testId ? ` #${e.testId}` : ''}${e.text ? ` ${JSON.stringify(e.text.slice(0, 60))}` : ''}`;
|
|
51
|
+
/** Stores the screen under its route and returns what changed since the previous read. */
|
|
52
|
+
export const record = (lynkeusDir, screen) => {
|
|
53
|
+
if (!screen.route)
|
|
54
|
+
return null;
|
|
55
|
+
fs.mkdirSync(dirOf(lynkeusDir), { recursive: true });
|
|
56
|
+
const file = fileOf(lynkeusDir, screen.route);
|
|
57
|
+
const date = today();
|
|
58
|
+
const elements = strip(screen);
|
|
59
|
+
let previous = null;
|
|
60
|
+
try {
|
|
61
|
+
previous = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
const frames = framesOf(screen);
|
|
65
|
+
const next = { route: screen.route, path: screen.path, seen: (previous?.seen ?? 0) + 1, date, elements, frames };
|
|
66
|
+
fs.writeFileSync(file, JSON.stringify(next, null, 1));
|
|
67
|
+
if (!previous)
|
|
68
|
+
return null;
|
|
69
|
+
const diff = diffScreens({ ...screen, elements: previous.elements }, { ...screen, elements });
|
|
70
|
+
// Texts carry values that change every run; only a testID appearing or disappearing is worth a line.
|
|
71
|
+
const added = diff.added.filter((e) => e.testId).map(label);
|
|
72
|
+
const removed = diff.removed.filter((e) => e.testId).map(label);
|
|
73
|
+
const moved = previous.frames ? diffGeometry(previous.frames, frames) : [];
|
|
74
|
+
if (added.length === 0 && removed.length === 0 && moved.length === 0)
|
|
75
|
+
return null;
|
|
76
|
+
const change = {
|
|
77
|
+
route: screen.route,
|
|
78
|
+
date,
|
|
79
|
+
added,
|
|
80
|
+
removed,
|
|
81
|
+
moved,
|
|
82
|
+
changedText: diff.changedText.map((c) => `#${c.after.testId}: ${JSON.stringify(c.before.text ?? '')} → ${JSON.stringify(c.after.text ?? '')}`).slice(0, 10),
|
|
83
|
+
};
|
|
84
|
+
appendJsonl(changesFile(lynkeusDir), change);
|
|
85
|
+
return change;
|
|
86
|
+
};
|
|
87
|
+
/** What runs have seen, by route: the routes and interpolated testIDs a static parser cannot find. */
|
|
88
|
+
export const observed = (lynkeusDir) => {
|
|
89
|
+
const out = new Map();
|
|
90
|
+
let files = [];
|
|
91
|
+
try {
|
|
92
|
+
files = fs.readdirSync(dirOf(lynkeusDir)).filter((f) => f.endsWith('.json'));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
let stored;
|
|
99
|
+
try {
|
|
100
|
+
stored = JSON.parse(fs.readFileSync(path.join(dirOf(lynkeusDir), file), 'utf8'));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (!stored.route)
|
|
106
|
+
continue;
|
|
107
|
+
const testIds = [];
|
|
108
|
+
for (const e of stored.elements ?? []) {
|
|
109
|
+
if (!e.testId || e.hidden)
|
|
110
|
+
continue;
|
|
111
|
+
if (!testIds.includes(e.testId))
|
|
112
|
+
testIds.push(e.testId);
|
|
113
|
+
}
|
|
114
|
+
out.set(stored.route, { route: stored.route, path: stored.path ?? [], seen: stored.seen ?? 1, date: stored.date, testIds });
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
};
|
|
118
|
+
export const changes = (lynkeusDir, last = 50) => readJsonl(changesFile(lynkeusDir)).slice(-last);
|
|
119
|
+
export const describeChange = (c) => [
|
|
120
|
+
`${c.date} ${c.route}`,
|
|
121
|
+
...c.added.map((a) => ` + ${a}`),
|
|
122
|
+
...c.removed.map((r) => ` - ${r}`),
|
|
123
|
+
...(c.moved ?? []).map((m) => ` ⇢ ${m}`),
|
|
124
|
+
...c.changedText.map((t) => ` ~ ${t}`),
|
|
125
|
+
].join('\n');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The screen as nested lines of meaning (kinds, labels, testIDs, values,
|
|
3
|
+
* states) and no geometry: a spacing tweak changes nothing, a renamed heading
|
|
4
|
+
* or a lost button is a changed line. Diffed like code.
|
|
5
|
+
*/
|
|
6
|
+
import type { Screen } from 'lynkeus-protocol';
|
|
7
|
+
export declare const screenTree: (screen: Screen) => string;
|
|
8
|
+
export type TreeDiff = {
|
|
9
|
+
added: string[];
|
|
10
|
+
removed: string[];
|
|
11
|
+
same: number;
|
|
12
|
+
};
|
|
13
|
+
/** The lines only one side has. */
|
|
14
|
+
export declare const diffTrees: (before: string, after: string) => TreeDiff;
|
|
15
|
+
export declare const describeTreeDiff: (d: TreeDiff) => string;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const quote = (s) => JSON.stringify(s);
|
|
2
|
+
const node = (e) => {
|
|
3
|
+
const parts = [e.kind];
|
|
4
|
+
const label = e.text ?? e.accessibilityLabel;
|
|
5
|
+
if (label)
|
|
6
|
+
parts.push(quote(label.length > 80 ? `${label.slice(0, 77)}…` : label));
|
|
7
|
+
if (e.testId)
|
|
8
|
+
parts.push(`#${e.testId}`);
|
|
9
|
+
if (e.value !== undefined && e.value !== '')
|
|
10
|
+
parts.push(`value=${quote(e.value)}`);
|
|
11
|
+
if (e.placeholder)
|
|
12
|
+
parts.push(`placeholder=${quote(e.placeholder)}`);
|
|
13
|
+
const state = [!e.enabled ? 'disabled' : '', e.covered ? 'covered' : '', e.hidden ?? '', e.focused ? 'focused' : ''].filter(Boolean);
|
|
14
|
+
if (state.length)
|
|
15
|
+
parts.push(`[${state.join(' ')}]`);
|
|
16
|
+
return parts.join(' ');
|
|
17
|
+
};
|
|
18
|
+
const meaningful = (e) => e.kind !== 'view' || e.accessibilityLabel !== undefined || e.testId !== undefined;
|
|
19
|
+
export const screenTree = (screen) => {
|
|
20
|
+
const lines = [`- screen ${screen.route ?? '?'}${screen.path.length > 1 ? ` (${screen.path.join(' > ')})` : ''}`];
|
|
21
|
+
// Relative to the nearest listed ancestor, so the layout views left out leave no gaps.
|
|
22
|
+
const level = new Map();
|
|
23
|
+
for (const e of screen.elements) {
|
|
24
|
+
const parentLevel = e.parent !== undefined && e.parent >= 0 ? (level.get(e.parent) ?? 0) : 0;
|
|
25
|
+
if (!meaningful(e)) {
|
|
26
|
+
level.set(e.i, parentLevel);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const mine = parentLevel + 1;
|
|
30
|
+
level.set(e.i, mine);
|
|
31
|
+
lines.push(`${' '.repeat(mine)}- ${node(e)}`);
|
|
32
|
+
}
|
|
33
|
+
return `${lines.join('\n')}\n`;
|
|
34
|
+
};
|
|
35
|
+
/** The lines only one side has. */
|
|
36
|
+
export const diffTrees = (before, after) => {
|
|
37
|
+
const a = before.split('\n').filter(Boolean);
|
|
38
|
+
const b = after.split('\n').filter(Boolean);
|
|
39
|
+
const countA = new Map();
|
|
40
|
+
for (const l of a)
|
|
41
|
+
countA.set(l, (countA.get(l) ?? 0) + 1);
|
|
42
|
+
const added = [];
|
|
43
|
+
let same = 0;
|
|
44
|
+
for (const l of b) {
|
|
45
|
+
const n = countA.get(l) ?? 0;
|
|
46
|
+
if (n > 0) {
|
|
47
|
+
countA.set(l, n - 1);
|
|
48
|
+
same += 1;
|
|
49
|
+
}
|
|
50
|
+
else
|
|
51
|
+
added.push(l);
|
|
52
|
+
}
|
|
53
|
+
const removed = [];
|
|
54
|
+
for (const [l, n] of countA)
|
|
55
|
+
for (let i = 0; i < n; i++)
|
|
56
|
+
removed.push(l);
|
|
57
|
+
return { added, removed, same };
|
|
58
|
+
};
|
|
59
|
+
export const describeTreeDiff = (d) => {
|
|
60
|
+
if (!d.added.length && !d.removed.length)
|
|
61
|
+
return ' (the tree did not change)';
|
|
62
|
+
return [...d.removed.map((l) => `- ${l.trim()}`), ...d.added.map((l) => `+ ${l.trim()}`)].map((l) => ` ${l}`).join('\n');
|
|
63
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What lynkeus reads from an app's source. Facts are kept per file and
|
|
3
|
+
* attributed to screens by traversal: most testIDs and navigations live in
|
|
4
|
+
* shared components, so attributing them at extraction would always miss some.
|
|
5
|
+
*/
|
|
6
|
+
/** As declared to the navigator, e.g. `Settings.Profile`. */
|
|
7
|
+
export type RouteName = string;
|
|
8
|
+
/** Relative to the app's root. */
|
|
9
|
+
export type SourcePath = string;
|
|
10
|
+
/** A testID written as a literal; one built at runtime is only found on the device. */
|
|
11
|
+
export type Selector = {
|
|
12
|
+
id: string;
|
|
13
|
+
line: number;
|
|
14
|
+
};
|
|
15
|
+
/** What the user touches to cause a navigation. */
|
|
16
|
+
export type NavigationTrigger = {
|
|
17
|
+
kind: 'testId';
|
|
18
|
+
testId: string;
|
|
19
|
+
}
|
|
20
|
+
/** The element has a testID built at runtime. */
|
|
21
|
+
| {
|
|
22
|
+
kind: 'dynamicTestId';
|
|
23
|
+
expression: string;
|
|
24
|
+
}
|
|
25
|
+
/** Nothing to attribute it to: conditional routing, effects, deep links. */
|
|
26
|
+
| {
|
|
27
|
+
kind: 'unknown';
|
|
28
|
+
};
|
|
29
|
+
export type NavigationEdge = {
|
|
30
|
+
/** The route when it is a literal, otherwise the source expression. */
|
|
31
|
+
to: string;
|
|
32
|
+
line: number;
|
|
33
|
+
confidence: 'static' | 'dynamic';
|
|
34
|
+
via: NavigationTrigger;
|
|
35
|
+
/** `element` when the call sits in the element's own handler, `handler` when it was reached through a named function. */
|
|
36
|
+
linkage?: 'element' | 'handler';
|
|
37
|
+
};
|
|
38
|
+
export type FileFacts = {
|
|
39
|
+
selectors: Selector[];
|
|
40
|
+
navigates: NavigationEdge[];
|
|
41
|
+
/** First-party imports; a name imported through a barrel points at the file that defines it. */
|
|
42
|
+
imports: SourcePath[];
|
|
43
|
+
/** The barrels those names came through: a dependency only when the barrel itself changes. */
|
|
44
|
+
barrels?: SourcePath[];
|
|
45
|
+
};
|
|
46
|
+
export type ScreenNode = {
|
|
47
|
+
route: RouteName;
|
|
48
|
+
/** The component bound in the navigator. */
|
|
49
|
+
component: string;
|
|
50
|
+
/** Null when the component could not be resolved to a file. */
|
|
51
|
+
file: SourcePath | null;
|
|
52
|
+
declaredIn: SourcePath;
|
|
53
|
+
};
|
|
54
|
+
export type ScreenGraph = {
|
|
55
|
+
app: string;
|
|
56
|
+
root: string;
|
|
57
|
+
extractedAt: string;
|
|
58
|
+
screens: Record<RouteName, ScreenNode>;
|
|
59
|
+
files: Record<SourcePath, FileFacts>;
|
|
60
|
+
/** What could not be read, so the graph is never silently wrong. */
|
|
61
|
+
warnings: string[];
|
|
62
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The screen, the last requests and the trace after a failure, read the way a
|
|
3
|
+
* person would: a dead navigator, a gone session, a 5xx, a covered target, a
|
|
4
|
+
* wording difference, a screen still loading. Each rule names what to do next.
|
|
5
|
+
*/
|
|
6
|
+
import type { RequestRecord, Screen, TraceEvent } from 'lynkeus-protocol';
|
|
7
|
+
export type Evidence = {
|
|
8
|
+
screen: Screen;
|
|
9
|
+
requests: RequestRecord[];
|
|
10
|
+
inFlight: number;
|
|
11
|
+
trace?: TraceEvent[];
|
|
12
|
+
/** What the failed command was looking for. */
|
|
13
|
+
target?: {
|
|
14
|
+
testId?: string;
|
|
15
|
+
text?: string;
|
|
16
|
+
route?: string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export type Finding = {
|
|
20
|
+
rule: string;
|
|
21
|
+
message: string;
|
|
22
|
+
};
|
|
23
|
+
export declare const explain: (e: Evidence) => Finding[];
|
|
24
|
+
export declare const describeWhy: (findings: Finding[]) => string;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { pathOf } from './problems.js';
|
|
2
|
+
const fold = (t) => t
|
|
3
|
+
.normalize('NFKD')
|
|
4
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.trim();
|
|
7
|
+
const distance = (a, b) => {
|
|
8
|
+
const m = a.length;
|
|
9
|
+
const n = b.length;
|
|
10
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
11
|
+
for (let i = 1; i <= m; i++) {
|
|
12
|
+
const cur = [i];
|
|
13
|
+
for (let j = 1; j <= n; j++)
|
|
14
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
15
|
+
prev = cur;
|
|
16
|
+
}
|
|
17
|
+
return prev[n];
|
|
18
|
+
};
|
|
19
|
+
const similar = (a, b) => {
|
|
20
|
+
if (a.includes(b) || b.includes(a))
|
|
21
|
+
return true;
|
|
22
|
+
if (a.length > 200 || b.length > 200)
|
|
23
|
+
return false;
|
|
24
|
+
return distance(a, b) <= Math.max(2, Math.floor(Math.max(a.length, b.length) * 0.25));
|
|
25
|
+
};
|
|
26
|
+
export const explain = (e) => {
|
|
27
|
+
const out = [];
|
|
28
|
+
const els = e.screen.elements;
|
|
29
|
+
const done = e.requests.filter((r) => r.status !== undefined || r.error);
|
|
30
|
+
const status = (n) => done.filter((r) => r.status === n);
|
|
31
|
+
if (!e.screen.route && els.length === 0) {
|
|
32
|
+
const crash = e.trace
|
|
33
|
+
?.slice()
|
|
34
|
+
.reverse()
|
|
35
|
+
.find((t) => t.kind === 'crash');
|
|
36
|
+
out.push(crash && crash.kind === 'crash'
|
|
37
|
+
? {
|
|
38
|
+
rule: 'dead-navigator',
|
|
39
|
+
message: `the screen is empty and has no route: a component threw and took the tree with it — ${crash.message}${crash.stack ? `\n ${crash.stack.split('\n').slice(1, 4).join('\n ')}` : ''}`,
|
|
40
|
+
}
|
|
41
|
+
: {
|
|
42
|
+
rule: 'dead-navigator',
|
|
43
|
+
message: 'the screen is empty and has no route: a component threw while rendering. Reset the app and look for the render error in the host log.',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (done.length && done.every((r) => r.status === 401))
|
|
47
|
+
out.push({
|
|
48
|
+
rule: 'session-gone',
|
|
49
|
+
message: 'every request answered 401: the session is gone (a logout, a reset, or a key revoked elsewhere). Log in again.',
|
|
50
|
+
});
|
|
51
|
+
const forbidden = status(403);
|
|
52
|
+
if (forbidden.length >= 2)
|
|
53
|
+
out.push({
|
|
54
|
+
rule: 'forbidden',
|
|
55
|
+
message: `the backend refuses ${[...new Set(forbidden.map((r) => pathOf(r.url)))].slice(0, 3).join(', ')} with 403: an account state or a permission, not the screen. Check what the case expects of this user.`,
|
|
56
|
+
});
|
|
57
|
+
const five = done.find((r) => (r.status ?? 0) >= 500);
|
|
58
|
+
if (five)
|
|
59
|
+
out.push({
|
|
60
|
+
rule: 'backend-error',
|
|
61
|
+
message: `the backend answered ${five.status} on ${five.method} ${pathOf(five.url)}: a server error, not the app. Reproduce it with curl before touching the case.`,
|
|
62
|
+
});
|
|
63
|
+
if (status(429).length)
|
|
64
|
+
out.push({
|
|
65
|
+
rule: 'rate-limited',
|
|
66
|
+
message: 'a request answered 429: the backend is rate limiting this client. Space the runs out or raise the limit locally.',
|
|
67
|
+
});
|
|
68
|
+
const stall = e.trace
|
|
69
|
+
?.slice()
|
|
70
|
+
.reverse()
|
|
71
|
+
.find((t) => t.kind === 'stall');
|
|
72
|
+
if (stall && stall.kind === 'stall')
|
|
73
|
+
out.push({
|
|
74
|
+
rule: 'render-loop',
|
|
75
|
+
message: `the host broke a render loop${stall.component ? ` in ${stall.component}` : ''} (${stall.renders} updates in ${stall.windowMs}ms): the screen is dead until a reset. A component whose effect and store feed each other.`,
|
|
76
|
+
});
|
|
77
|
+
if (e.target) {
|
|
78
|
+
const { testId, text, route } = e.target;
|
|
79
|
+
const match = (el) => (testId && el.testId === testId) || (text && (el.text === text || el.accessibilityLabel === text));
|
|
80
|
+
const found = els.find(match);
|
|
81
|
+
if (found?.hidden) {
|
|
82
|
+
out.push({
|
|
83
|
+
rule: 'hidden',
|
|
84
|
+
message: `${testId ? `#${testId}` : JSON.stringify(text)} is in the tree but not presented (${found.hidden}): it belongs to a screen the app is not showing. Close what is open, or target something inside it.`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (found?.covered)
|
|
88
|
+
out.push({
|
|
89
|
+
rule: 'covered',
|
|
90
|
+
message: `${testId ? `#${testId}` : JSON.stringify(text)} is on the screen but covered: a sheet or overlay is over it. Close it, or press what covers it.`,
|
|
91
|
+
});
|
|
92
|
+
if (found && !found.enabled)
|
|
93
|
+
out.push({
|
|
94
|
+
rule: 'disabled',
|
|
95
|
+
message: `${testId ? `#${testId}` : JSON.stringify(text)} is on the screen but disabled: a form is not valid yet, or a request it depends on is in flight.`,
|
|
96
|
+
});
|
|
97
|
+
if (!found && text) {
|
|
98
|
+
const want = fold(text);
|
|
99
|
+
const near = els
|
|
100
|
+
.map((el) => el.text ?? el.accessibilityLabel)
|
|
101
|
+
.filter((t) => !!t)
|
|
102
|
+
.find((t) => similar(fold(t), want));
|
|
103
|
+
if (near && near !== text)
|
|
104
|
+
out.push({ rule: 'near-match', message: `the text is not there, but ${JSON.stringify(near)} is: a wording, accent or case difference?` });
|
|
105
|
+
const listed = els.filter((el) => el.testId && /-\d+-item$/.test(el.testId));
|
|
106
|
+
const prefix = want.slice(0, 4);
|
|
107
|
+
if (listed.length >= 10 && prefix && listed.filter((el) => fold(el.text ?? '').includes(prefix)).length >= listed.length / 2)
|
|
108
|
+
out.push({
|
|
109
|
+
rule: 'capped-list',
|
|
110
|
+
message: `the list shows ${listed.length} near matches and yours is not among them: the data has accumulated look-alikes and the list is capped. Search by something unique, or reset the data.`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (route && e.screen.route && e.screen.route !== route) {
|
|
114
|
+
const recent = e.trace
|
|
115
|
+
?.filter((t) => t.kind === 'route')
|
|
116
|
+
.slice(-3)
|
|
117
|
+
.map((t) => (t.kind === 'route' ? t.route : undefined))
|
|
118
|
+
.filter(Boolean) ?? [];
|
|
119
|
+
if (recent.includes(route))
|
|
120
|
+
out.push({
|
|
121
|
+
rule: 'route-left',
|
|
122
|
+
message: `the app was on ${route} and moved to ${e.screen.route}: something navigated after the step (a redirect, a gate, a modal). The trace shows the order.`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (e.inFlight > 0)
|
|
127
|
+
out.push({
|
|
128
|
+
rule: 'in-flight',
|
|
129
|
+
message: `${e.inFlight} request(s) still in flight when the command gave up: the screen had not finished loading. A longer timeout, or wait for the request it needs.`,
|
|
130
|
+
});
|
|
131
|
+
if (done.length === 0 && e.inFlight === 0 && e.target?.route)
|
|
132
|
+
out.push({
|
|
133
|
+
rule: 'no-requests',
|
|
134
|
+
message: 'no request left the app while this ran: nothing was fetched. Is the user logged in, and is the app talking to the backend it should?',
|
|
135
|
+
});
|
|
136
|
+
// Hosts the sandbox refuses are usually third parties nobody needs: named only when nothing else explains it.
|
|
137
|
+
const refused = done.filter((r) => r.status === 0 && r.error && /networkHosts|is off/.test(r.error));
|
|
138
|
+
if (refused.length && e.target && out.length === 0)
|
|
139
|
+
out.push({
|
|
140
|
+
rule: 'host-refused',
|
|
141
|
+
message: `${refused.length} request(s) were refused by the host (${[...new Set(refused.map((r) => r.url.replace(/^https?:\/\//, '').split('/')[0]))].slice(0, 3).join(', ')}): if the screen needs one of them, allow the host in networkHosts or mock it.`,
|
|
142
|
+
});
|
|
143
|
+
const lastCommand = e.trace
|
|
144
|
+
?.slice()
|
|
145
|
+
.reverse()
|
|
146
|
+
.find((t) => t.kind === 'command' && t.error);
|
|
147
|
+
if (lastCommand && lastCommand.kind === 'command' && out.length === 0)
|
|
148
|
+
out.push({ rule: 'last-error', message: `the last command that failed was ${lastCommand.method}: ${lastCommand.error}` });
|
|
149
|
+
return out;
|
|
150
|
+
};
|
|
151
|
+
export const describeWhy = (findings) => findings.length
|
|
152
|
+
? findings.map((f) => ` • ${f.message}`).join('\n')
|
|
153
|
+
: ' (nothing in the evidence explains it: the screen, requests and trace look ordinary)';
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { commands, overMcp } from '../registry.js';
|
|
6
|
+
import { execute } from '../runtime.js';
|
|
7
|
+
import { TARGET_HELP } from '../targets.js';
|
|
8
|
+
const text = (body) => ({ content: [{ type: 'text', text: body }] });
|
|
9
|
+
const propertyFor = (f) => ({
|
|
10
|
+
type: f.type === 'list' ? 'array' : f.type,
|
|
11
|
+
...(f.type === 'list' ? { items: { type: 'string' } } : {}),
|
|
12
|
+
description: f.help + (f.default !== undefined ? ` (default ${f.default})` : ''),
|
|
13
|
+
});
|
|
14
|
+
const describe = (c) => {
|
|
15
|
+
const details = c.mcpDetails ?? c.details;
|
|
16
|
+
return details ? `${c.summary}\n\n${details}` : c.summary;
|
|
17
|
+
};
|
|
18
|
+
const identifier = (name) => name.replace(/[^a-z0-9]+/gi, '_');
|
|
19
|
+
// Written by hand: a schema library adds fields to every tool that a model pays for on every turn.
|
|
20
|
+
const schemaFor = (c) => {
|
|
21
|
+
const properties = {};
|
|
22
|
+
const required = [];
|
|
23
|
+
for (const p of c.positionals ?? []) {
|
|
24
|
+
const key = identifier(p.name);
|
|
25
|
+
properties[key] = p.rest ? { type: 'array', items: { type: 'string' }, description: p.help } : { type: 'string', description: p.help };
|
|
26
|
+
if (p.required)
|
|
27
|
+
required.push(key);
|
|
28
|
+
}
|
|
29
|
+
for (const [name, f] of Object.entries(c.flags ?? {}))
|
|
30
|
+
if (!c.mcpOmit?.includes(name))
|
|
31
|
+
properties[identifier(name)] = propertyFor(f);
|
|
32
|
+
return { type: 'object', properties, ...(required.length ? { required } : {}) };
|
|
33
|
+
};
|
|
34
|
+
/** Back to an argv, so the same runtime serves both surfaces. */
|
|
35
|
+
const argvFor = (c, input) => {
|
|
36
|
+
const av = c.name.split(' ');
|
|
37
|
+
for (const p of c.positionals ?? []) {
|
|
38
|
+
const v = input[identifier(p.name)];
|
|
39
|
+
if (v === undefined)
|
|
40
|
+
continue;
|
|
41
|
+
if (Array.isArray(v))
|
|
42
|
+
av.push(...v.map(String));
|
|
43
|
+
else
|
|
44
|
+
av.push(String(v));
|
|
45
|
+
}
|
|
46
|
+
for (const [name, f] of Object.entries(c.flags ?? {})) {
|
|
47
|
+
const v = input[identifier(name)];
|
|
48
|
+
if (v === undefined)
|
|
49
|
+
continue;
|
|
50
|
+
if (f.type === 'boolean') {
|
|
51
|
+
if (v)
|
|
52
|
+
av.push(`--${name}`);
|
|
53
|
+
}
|
|
54
|
+
else if (f.type === 'list')
|
|
55
|
+
for (const item of v)
|
|
56
|
+
av.push(`--${name}`, item);
|
|
57
|
+
else
|
|
58
|
+
av.push(`--${name}`, String(v));
|
|
59
|
+
}
|
|
60
|
+
return av;
|
|
61
|
+
};
|
|
62
|
+
export const serve = async (session, options = {}) => {
|
|
63
|
+
const { version } = createRequire(import.meta.url)('../../package.json');
|
|
64
|
+
const server = new Server({ name: 'lynkeus', version }, { capabilities: { tools: {} } });
|
|
65
|
+
const served = commands().filter((c) => overMcp(c, !!options.full));
|
|
66
|
+
const byName = new Map(served.map((c) => [identifier(c.name), c]));
|
|
67
|
+
// So a model reading the app's screens knows their language before it starts guessing.
|
|
68
|
+
const { config } = session.ctx;
|
|
69
|
+
const locale = config.language || config.locale ? JSON.stringify({ language: config.language, ...config.locale }, null, 2) : undefined;
|
|
70
|
+
// The grammar of a target is spelled where a model meets it first; after that it is a word.
|
|
71
|
+
let spelled = false;
|
|
72
|
+
const once = (help) => {
|
|
73
|
+
if (!help.includes(TARGET_HELP))
|
|
74
|
+
return help;
|
|
75
|
+
const out = spelled ? help.replace(TARGET_HELP, 'target') : help;
|
|
76
|
+
spelled = true;
|
|
77
|
+
return out;
|
|
78
|
+
};
|
|
79
|
+
const tools = served.map((c) => {
|
|
80
|
+
const schema = schemaFor(c);
|
|
81
|
+
for (const property of Object.values(schema.properties))
|
|
82
|
+
property.description = once(property.description);
|
|
83
|
+
return { name: identifier(c.name), description: describe(c), inputSchema: schema };
|
|
84
|
+
});
|
|
85
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
86
|
+
tools: [
|
|
87
|
+
...(locale
|
|
88
|
+
? [
|
|
89
|
+
{
|
|
90
|
+
name: 'app_locale',
|
|
91
|
+
description: 'The language and region this app is built for, as its own configuration declares them.',
|
|
92
|
+
inputSchema: { type: 'object', properties: {} },
|
|
93
|
+
},
|
|
94
|
+
]
|
|
95
|
+
: []),
|
|
96
|
+
...tools,
|
|
97
|
+
],
|
|
98
|
+
}));
|
|
99
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
100
|
+
if (request.params.name === 'app_locale' && locale)
|
|
101
|
+
return text(locale);
|
|
102
|
+
const c = byName.get(request.params.name);
|
|
103
|
+
if (!c)
|
|
104
|
+
return { ...text(`✗ no tool ${request.params.name}`), isError: true };
|
|
105
|
+
const r = await execute(argvFor(c, (request.params.arguments ?? {})), session.ctx);
|
|
106
|
+
if (r.error)
|
|
107
|
+
return { ...text(`✗ ${r.error}${r.why ? `\n\nwhy:\n${r.why}` : ''}`), isError: true };
|
|
108
|
+
return text(r.text || (r.json !== undefined ? JSON.stringify(r.json, null, 2) : 'ok'));
|
|
109
|
+
});
|
|
110
|
+
await server.connect(new StdioServerTransport());
|
|
111
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One definition per command, read by every surface: the terminal's parser and
|
|
3
|
+
* usage, session lines, the MCP tools and the reference document.
|
|
4
|
+
*/
|
|
5
|
+
import type { LynkeusConfig } from './config.js';
|
|
6
|
+
import type { Device } from './device/driver.js';
|
|
7
|
+
import type { ScreenGraph } from './knowledge/types.js';
|
|
8
|
+
import { type FlagSpec, type FlagValues, type PositionalSpec } from './args.js';
|
|
9
|
+
export type Group = 'drive' | 'os' | 'map' | 'runs' | 'headless';
|
|
10
|
+
/** What a command runs against: one per process, or per session or MCP server. */
|
|
11
|
+
export type Base = {
|
|
12
|
+
root: string;
|
|
13
|
+
config: LynkeusConfig;
|
|
14
|
+
/** The live app, connected on first use. */
|
|
15
|
+
device: () => Promise<Device>;
|
|
16
|
+
/** The static graph; throws with advice when there is none. */
|
|
17
|
+
graph: () => ScreenGraph;
|
|
18
|
+
/** Progress and diagnostics, on stderr. */
|
|
19
|
+
err: (line: string) => void;
|
|
20
|
+
/** Inside `lynkeus session` and over MCP: one connection, many commands. */
|
|
21
|
+
attached: boolean;
|
|
22
|
+
};
|
|
23
|
+
export type Ctx = Base & {
|
|
24
|
+
args: string[];
|
|
25
|
+
flags: FlagValues;
|
|
26
|
+
passthrough: string[];
|
|
27
|
+
};
|
|
28
|
+
export type Result = {
|
|
29
|
+
/** What a person reads; the JSON on one line when left out. */
|
|
30
|
+
text?: string;
|
|
31
|
+
/** What a program reads; printed instead of the text under --json. */
|
|
32
|
+
json?: unknown;
|
|
33
|
+
code?: number;
|
|
34
|
+
};
|
|
35
|
+
export type Command = {
|
|
36
|
+
/** `press`, or `map build` for a grouped command. */
|
|
37
|
+
name: string;
|
|
38
|
+
group: Group;
|
|
39
|
+
summary: string;
|
|
40
|
+
/** Longer help, for `lynkeus help <command>` and the reference. */
|
|
41
|
+
details?: string;
|
|
42
|
+
positionals?: PositionalSpec[];
|
|
43
|
+
flags?: Record<string, FlagSpec>;
|
|
44
|
+
/** Other spellings it answers to, left out of the usage. */
|
|
45
|
+
aliases?: string[];
|
|
46
|
+
needs: 'nothing' | 'graph' | 'app';
|
|
47
|
+
/** `core` is served to every MCP client; other commands that need the app or the graph only with `mcp --full`. */
|
|
48
|
+
mcp?: boolean | 'core';
|
|
49
|
+
/** Flags a model has no use for (artifacts, timing knobs): left out of the MCP schema. */
|
|
50
|
+
mcpOmit?: string[];
|
|
51
|
+
/** What a model is told instead of `details`. */
|
|
52
|
+
mcpDetails?: string;
|
|
53
|
+
/** Runnable from a session line (default: when it needs the app or the graph). */
|
|
54
|
+
session?: boolean;
|
|
55
|
+
/** The usage line, when the generated one would mislead. */
|
|
56
|
+
example?: string;
|
|
57
|
+
run: (ctx: Ctx) => Promise<Result | string | undefined>;
|
|
58
|
+
};
|
|
59
|
+
export declare const define: (c: Command) => Command;
|
|
60
|
+
export declare const register: (commands: Command[]) => void;
|
|
61
|
+
export declare const commands: () => Command[];
|
|
62
|
+
export declare const find: (name: string) => Command | undefined;
|
|
63
|
+
/** The command an argv names, and the rest of the argv. */
|
|
64
|
+
export declare const resolve: (argv: string[]) => {
|
|
65
|
+
command: Command;
|
|
66
|
+
rest: string[];
|
|
67
|
+
} | undefined;
|
|
68
|
+
export declare const parseFor: (command: Command, rest: string[]) => {
|
|
69
|
+
args: string[];
|
|
70
|
+
flags: FlagValues;
|
|
71
|
+
passthrough: string[];
|
|
72
|
+
};
|
|
73
|
+
export declare const inSession: (c: Command) => boolean;
|
|
74
|
+
export declare const overMcp: (c: Command, full: boolean) => boolean;
|
|
75
|
+
export declare const usage: () => string;
|
|
76
|
+
export declare const helpFor: (c: Command) => string;
|
|
77
|
+
/** The reference document, generated so it cannot drift from the commands. */
|
|
78
|
+
export declare const referenceMarkdown: () => string;
|