tauri-agent-tools 0.2.1 → 0.4.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/.agents/skills/tauri-agent-tools/SKILL.md +63 -6
- package/.agents/skills/tauri-bridge-setup/SKILL.md +24 -2
- package/AGENTS.md +8 -3
- package/README.md +58 -4
- package/dist/bridge/client.d.ts +5 -2
- package/dist/bridge/client.js +26 -4
- package/dist/bridge/client.js.map +1 -1
- package/dist/bridge/tokenDiscovery.d.ts +1 -1
- package/dist/bridge/tokenDiscovery.js +3 -6
- package/dist/bridge/tokenDiscovery.js.map +1 -1
- package/dist/cli.js +10 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/consoleMonitor.js +15 -10
- package/dist/commands/consoleMonitor.js.map +1 -1
- package/dist/commands/diff.d.ts +2 -0
- package/dist/commands/diff.js +91 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/dom.d.ts +1 -0
- package/dist/commands/dom.js +81 -6
- package/dist/commands/dom.js.map +1 -1
- package/dist/commands/eval.js +6 -1
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/ipcMonitor.js +21 -9
- package/dist/commands/ipcMonitor.js.map +1 -1
- package/dist/commands/listWindows.js +5 -1
- package/dist/commands/listWindows.js.map +1 -1
- package/dist/commands/mutations.d.ts +5 -0
- package/dist/commands/mutations.js +146 -0
- package/dist/commands/mutations.js.map +1 -0
- package/dist/commands/pageState.js +2 -1
- package/dist/commands/pageState.js.map +1 -1
- package/dist/commands/rustLogs.d.ts +2 -0
- package/dist/commands/rustLogs.js +105 -0
- package/dist/commands/rustLogs.js.map +1 -0
- package/dist/commands/screenshot.js +12 -2
- package/dist/commands/screenshot.js.map +1 -1
- package/dist/commands/shared.d.ts +6 -0
- package/dist/commands/shared.js +11 -0
- package/dist/commands/shared.js.map +1 -1
- package/dist/commands/snapshot.d.ts +3 -0
- package/dist/commands/snapshot.js +138 -0
- package/dist/commands/snapshot.js.map +1 -0
- package/dist/commands/storage.js +26 -22
- package/dist/commands/storage.js.map +1 -1
- package/dist/commands/wait.js +27 -5
- package/dist/commands/wait.js.map +1 -1
- package/dist/platform/macos.d.ts +2 -1
- package/dist/platform/macos.js +3 -1
- package/dist/platform/macos.js.map +1 -1
- package/dist/platform/wayland.d.ts +2 -1
- package/dist/platform/wayland.js +4 -3
- package/dist/platform/wayland.js.map +1 -1
- package/dist/platform/x11.d.ts +2 -1
- package/dist/platform/x11.js +17 -18
- package/dist/platform/x11.js.map +1 -1
- package/dist/schemas/bridge.d.ts +120 -0
- package/dist/schemas/bridge.js +38 -0
- package/dist/schemas/bridge.js.map +1 -0
- package/dist/schemas/commands.d.ts +245 -0
- package/dist/schemas/commands.js +65 -0
- package/dist/schemas/commands.js.map +1 -0
- package/dist/schemas/dom.d.ts +22 -0
- package/dist/schemas/dom.js +22 -0
- package/dist/schemas/dom.js.map +1 -0
- package/dist/schemas/index.d.ts +11 -0
- package/dist/schemas/index.js +12 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/platform.d.ts +61 -0
- package/dist/schemas/platform.js +34 -0
- package/dist/schemas/platform.js.map +1 -0
- package/dist/types.d.ts +2 -11
- package/dist/util/exec.js +2 -4
- package/dist/util/exec.js.map +1 -1
- package/dist/util/image.d.ts +2 -1
- package/dist/util/image.js.map +1 -1
- package/examples/tauri-bridge/Cargo.toml +2 -0
- package/examples/tauri-bridge/src/dev_bridge.rs +232 -7
- package/package.json +6 -3
- package/rust-bridge/README.md +9 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom.js","sourceRoot":"","sources":["../../src/schemas/dom.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAexB,MAAM,CAAC,MAAM,aAAa,GAAuB,CAAC,CAAC,MAAM,CAAC;IACxD,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;IACf,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACzB,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IACvC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC3B,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,QAAQ,EAAE;IACpE,UAAU,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IACvD,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IACnD,IAAI,QAAQ;QACV,OAAO,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC3C,CAAC;CACF,CAAC,CAAC;AAWH,MAAM,CAAC,MAAM,cAAc,GAAwB,CAAC,CAAC,MAAM,CAAC;IAC1D,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC3B,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE;IACnD,IAAI,QAAQ;QACV,OAAO,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC5C,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Zod schemas and derived TypeScript types, organized by domain.
|
|
3
|
+
* Foundation layer of the dependency DAG — no internal imports except `zod`.
|
|
4
|
+
*
|
|
5
|
+
* Barrel re-exports from all domain files. Consumers may import directly
|
|
6
|
+
* from domain files (e.g., '../schemas/bridge.js') for explicitness.
|
|
7
|
+
*/
|
|
8
|
+
export { TokenFileSchema, type TokenFile, ElementRectSchema, type ElementRect, BridgeConfigSchema, type BridgeConfig, ViewportSizeSchema, type ViewportSize, RustLogLevelSchema, type RustLogLevel, RustLogEntrySchema, type RustLogEntry, BridgeEvalResponseSchema, BridgeLogsResponseSchema, } from './bridge.js';
|
|
9
|
+
export { DomNodeSchema, type DomNode, A11yNodeSchema, type A11yNode, } from './dom.js';
|
|
10
|
+
export { StorageEntrySchema, type StorageEntry, PageStateSchema, type PageState, ConsoleLevelSchema, type ConsoleLevel, ConsoleEntrySchema, type ConsoleEntry, MutationTypeSchema, type MutationType, MutationEntrySchema, type MutationEntry, IpcEntrySchema, type IpcEntry, SnapshotStorageResultSchema, type SnapshotStorageResult, ImageFormatSchema, type ImageFormat, StorageTypeSchema, type StorageType, DomModeSchema, type DomMode, PackageJsonSchema, type PackageJson, } from './commands.js';
|
|
11
|
+
export { WindowIdSchema, CGWindowInfoSchema, type CGWindowInfo, SwayNodeSchema, type SwayNode, } from './platform.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Zod schemas and derived TypeScript types, organized by domain.
|
|
3
|
+
* Foundation layer of the dependency DAG — no internal imports except `zod`.
|
|
4
|
+
*
|
|
5
|
+
* Barrel re-exports from all domain files. Consumers may import directly
|
|
6
|
+
* from domain files (e.g., '../schemas/bridge.js') for explicitness.
|
|
7
|
+
*/
|
|
8
|
+
export { TokenFileSchema, ElementRectSchema, BridgeConfigSchema, ViewportSizeSchema, RustLogLevelSchema, RustLogEntrySchema, BridgeEvalResponseSchema, BridgeLogsResponseSchema, } from './bridge.js';
|
|
9
|
+
export { DomNodeSchema, A11yNodeSchema, } from './dom.js';
|
|
10
|
+
export { StorageEntrySchema, PageStateSchema, ConsoleLevelSchema, ConsoleEntrySchema, MutationTypeSchema, MutationEntrySchema, IpcEntrySchema, SnapshotStorageResultSchema, ImageFormatSchema, StorageTypeSchema, DomModeSchema, PackageJsonSchema, } from './commands.js';
|
|
11
|
+
export { WindowIdSchema, CGWindowInfoSchema, SwayNodeSchema, } from './platform.js';
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/schemas/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACL,eAAe,EAEf,iBAAiB,EAEjB,kBAAkB,EAElB,kBAAkB,EAElB,kBAAkB,EAElB,kBAAkB,EAElB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,aAAa,EAEb,cAAc,GAEf,MAAM,UAAU,CAAC;AAElB,OAAO,EACL,kBAAkB,EAElB,eAAe,EAEf,kBAAkB,EAElB,kBAAkB,EAElB,kBAAkB,EAElB,mBAAmB,EAEnB,cAAc,EAEd,2BAA2B,EAE3B,iBAAiB,EAEjB,iBAAiB,EAEjB,aAAa,EAEb,iBAAiB,GAElB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,cAAc,EACd,kBAAkB,EAElB,cAAc,GAEf,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const WindowIdSchema: z.ZodString;
|
|
3
|
+
export declare const CGWindowInfoSchema: z.ZodObject<{
|
|
4
|
+
kCGWindowNumber: z.ZodNumber;
|
|
5
|
+
kCGWindowOwnerPID: z.ZodOptional<z.ZodNumber>;
|
|
6
|
+
kCGWindowName: z.ZodOptional<z.ZodString>;
|
|
7
|
+
kCGWindowOwnerName: z.ZodOptional<z.ZodString>;
|
|
8
|
+
kCGWindowBounds: z.ZodObject<{
|
|
9
|
+
X: z.ZodNumber;
|
|
10
|
+
Y: z.ZodNumber;
|
|
11
|
+
Width: z.ZodNumber;
|
|
12
|
+
Height: z.ZodNumber;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
X: number;
|
|
15
|
+
Y: number;
|
|
16
|
+
Width: number;
|
|
17
|
+
Height: number;
|
|
18
|
+
}, {
|
|
19
|
+
X: number;
|
|
20
|
+
Y: number;
|
|
21
|
+
Width: number;
|
|
22
|
+
Height: number;
|
|
23
|
+
}>;
|
|
24
|
+
}, "strip", z.ZodTypeAny, {
|
|
25
|
+
kCGWindowNumber: number;
|
|
26
|
+
kCGWindowBounds: {
|
|
27
|
+
X: number;
|
|
28
|
+
Y: number;
|
|
29
|
+
Width: number;
|
|
30
|
+
Height: number;
|
|
31
|
+
};
|
|
32
|
+
kCGWindowOwnerPID?: number | undefined;
|
|
33
|
+
kCGWindowName?: string | undefined;
|
|
34
|
+
kCGWindowOwnerName?: string | undefined;
|
|
35
|
+
}, {
|
|
36
|
+
kCGWindowNumber: number;
|
|
37
|
+
kCGWindowBounds: {
|
|
38
|
+
X: number;
|
|
39
|
+
Y: number;
|
|
40
|
+
Width: number;
|
|
41
|
+
Height: number;
|
|
42
|
+
};
|
|
43
|
+
kCGWindowOwnerPID?: number | undefined;
|
|
44
|
+
kCGWindowName?: string | undefined;
|
|
45
|
+
kCGWindowOwnerName?: string | undefined;
|
|
46
|
+
}>;
|
|
47
|
+
export type CGWindowInfo = z.infer<typeof CGWindowInfoSchema>;
|
|
48
|
+
export interface SwayNode {
|
|
49
|
+
id: number;
|
|
50
|
+
pid?: number;
|
|
51
|
+
name: string | null;
|
|
52
|
+
rect: {
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
};
|
|
58
|
+
nodes?: SwayNode[];
|
|
59
|
+
floating_nodes?: SwayNode[];
|
|
60
|
+
}
|
|
61
|
+
export declare const SwayNodeSchema: z.ZodType<SwayNode>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// === Platform: Window ID ===
|
|
3
|
+
export const WindowIdSchema = z.string().regex(/^\d+$/, 'Invalid window ID');
|
|
4
|
+
// === Platform: macOS ===
|
|
5
|
+
export const CGWindowInfoSchema = z.object({
|
|
6
|
+
kCGWindowNumber: z.number(),
|
|
7
|
+
kCGWindowOwnerPID: z.number().optional(),
|
|
8
|
+
kCGWindowName: z.string().optional(),
|
|
9
|
+
kCGWindowOwnerName: z.string().optional(),
|
|
10
|
+
kCGWindowBounds: z.object({
|
|
11
|
+
X: z.number(),
|
|
12
|
+
Y: z.number(),
|
|
13
|
+
Width: z.number(),
|
|
14
|
+
Height: z.number(),
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
export const SwayNodeSchema = z.object({
|
|
18
|
+
id: z.number(),
|
|
19
|
+
pid: z.number().optional(),
|
|
20
|
+
name: z.string().nullable(),
|
|
21
|
+
rect: z.object({
|
|
22
|
+
x: z.number(),
|
|
23
|
+
y: z.number(),
|
|
24
|
+
width: z.number(),
|
|
25
|
+
height: z.number(),
|
|
26
|
+
}),
|
|
27
|
+
get nodes() {
|
|
28
|
+
return z.array(SwayNodeSchema).optional();
|
|
29
|
+
},
|
|
30
|
+
get floating_nodes() {
|
|
31
|
+
return z.array(SwayNodeSchema).optional();
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
//# sourceMappingURL=platform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../../src/schemas/platform.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,8BAA8B;AAE9B,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC;AAE7E,0BAA0B;AAE1B,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IACzC,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE;IAC3B,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACxC,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACpC,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACzC,eAAe,EAAE,CAAC,CAAC,MAAM,CAAC;QACxB,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE;QACb,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE;QACb,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;QACjB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;KACnB,CAAC;CACH,CAAC,CAAC;AAcH,MAAM,CAAC,MAAM,cAAc,GAAwB,CAAC,CAAC,MAAM,CAAC;IAC1D,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE;IACd,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC1B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC3B,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACb,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE;QACb,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE;QACb,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;QACjB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;KACnB,CAAC;IACF,IAAI,KAAK;QACP,OAAO,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC5C,CAAC;IACD,IAAI,cAAc;QAChB,OAAO,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC5C,CAAC;CACF,CAAC,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ImageFormat } from './schemas/commands.js';
|
|
2
|
+
import type { BridgeConfig } from './schemas/bridge.js';
|
|
1
3
|
export interface WindowInfo {
|
|
2
4
|
windowId: string;
|
|
3
5
|
pid?: number;
|
|
@@ -7,18 +9,7 @@ export interface WindowInfo {
|
|
|
7
9
|
width: number;
|
|
8
10
|
height: number;
|
|
9
11
|
}
|
|
10
|
-
export interface ElementRect {
|
|
11
|
-
x: number;
|
|
12
|
-
y: number;
|
|
13
|
-
width: number;
|
|
14
|
-
height: number;
|
|
15
|
-
}
|
|
16
|
-
export interface BridgeConfig {
|
|
17
|
-
port: number;
|
|
18
|
-
token: string;
|
|
19
|
-
}
|
|
20
12
|
export type DisplayServer = 'x11' | 'wayland' | 'darwin' | 'unknown';
|
|
21
|
-
export type ImageFormat = 'png' | 'jpg';
|
|
22
13
|
export interface PlatformAdapter {
|
|
23
14
|
findWindow(title: string): Promise<string>;
|
|
24
15
|
captureWindow(windowId: string, format: ImageFormat): Promise<Buffer>;
|
package/dist/util/exec.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { execFile as cpExecFile } from 'node:child_process';
|
|
2
|
+
import { WindowIdSchema } from '../schemas/platform.js';
|
|
2
3
|
const MAX_BUFFER = 100 * 1024 * 1024; // 100MB
|
|
3
|
-
const WINDOW_ID_RE = /^\d+$/;
|
|
4
4
|
export function validateWindowId(id) {
|
|
5
|
-
|
|
6
|
-
throw new Error(`Invalid window ID: ${id}`);
|
|
7
|
-
}
|
|
5
|
+
WindowIdSchema.parse(id);
|
|
8
6
|
}
|
|
9
7
|
export function exec(cmd, args, options) {
|
|
10
8
|
return new Promise((resolve, reject) => {
|
package/dist/util/exec.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/util/exec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,UAAU,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/util/exec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,MAAM,UAAU,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,QAAQ;AAE9C,MAAM,UAAU,gBAAgB,CAAC,EAAU;IACzC,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;AAC3B,CAAC;AAOD,MAAM,UAAU,IAAI,CAClB,GAAW,EACX,IAAc,EACd,OAA8C;IAE9C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,KAAK,GAAG,UAAU,CACtB,GAAG,EACH,IAAI,EACJ;YACE,SAAS,EAAE,UAAU;YACrB,QAAQ,EAAE,QAAQ;YAClB,OAAO,EAAE,OAAO,EAAE,OAAO;SAC1B,EACD,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACxB,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;gBACrF,MAAM,CAAC,IAAI,KAAK,CAAC,GAAG,GAAG,YAAY,SAAS,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;gBAClE,OAAO;YACT,CAAC;YACD,OAAO,CAAC;gBACN,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAA2B,CAAC;gBACnF,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC;aAC3E,CAAC,CAAC;QACL,CAAC,CACF,CAAC;QAEF,IAAI,OAAO,EAAE,KAAK,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YAClC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/util/image.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { ElementRect
|
|
1
|
+
import type { ElementRect } from '../schemas/bridge.js';
|
|
2
|
+
import type { ImageFormat } from '../schemas/commands.js';
|
|
2
3
|
export declare function cropImage(buffer: Buffer, rect: ElementRect, format: ImageFormat): Promise<Buffer>;
|
|
3
4
|
export declare function resizeImage(buffer: Buffer, maxWidth: number, format: ImageFormat): Promise<Buffer>;
|
|
4
5
|
export declare function computeCropRect(elementRect: ElementRect, viewport: {
|
package/dist/util/image.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.js","sourceRoot":"","sources":["../../src/util/image.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"image.js","sourceRoot":"","sources":["../../src/util/image.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,MAAc,EACd,IAAiB,EACjB,MAAmB;IAEnB,MAAM,GAAG,GAAG,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;IAC7C,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAChH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,IAAI,CAC3B,SAAS,EACT,CAAC,GAAG,GAAG,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,GAAG,IAAI,CAAC,EAClD,EAAE,KAAK,EAAE,MAAM,EAAE,CAClB,CAAC;IACF,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAc,EACd,QAAgB,EAChB,MAAmB;IAEnB,MAAM,GAAG,GAAG,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;IAC7C,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,IAAI,CAC3B,SAAS,EACT,CAAC,GAAG,GAAG,IAAI,EAAE,SAAS,EAAE,GAAG,QAAQ,MAAM,EAAE,GAAG,GAAG,IAAI,CAAC,EACtD,EAAE,KAAK,EAAE,MAAM,EAAE,CAClB,CAAC;IACF,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,WAAwB,EACxB,QAA2C,EAC3C,cAAiD;IAEjD,MAAM,MAAM,GAAG,cAAc,CAAC,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC;IACrD,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;IACvD,OAAO;QACL,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC,CAAC;QACzB,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC,CAAC;QACzB,KAAK,EAAE,WAAW,CAAC,KAAK;QACxB,MAAM,EAAE,WAAW,CAAC,MAAM;KAC3B,CAAC;AACJ,CAAC"}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
use rand::Rng;
|
|
2
2
|
use serde::{Deserialize, Serialize};
|
|
3
|
-
use std::collections::HashMap;
|
|
3
|
+
use std::collections::{HashMap, VecDeque};
|
|
4
4
|
use std::fs;
|
|
5
|
-
use std::io::Write;
|
|
5
|
+
use std::io::{BufRead, BufReader, Write};
|
|
6
|
+
use std::process::{Command, Stdio};
|
|
6
7
|
use std::sync::{Arc, Condvar, Mutex};
|
|
7
8
|
use std::thread;
|
|
8
9
|
use tauri::{AppHandle, Manager};
|
|
9
10
|
use tiny_http::{Header, Response, Server};
|
|
11
|
+
use tracing_subscriber::layer::SubscriberExt;
|
|
12
|
+
use tracing_subscriber::util::SubscriberInitExt;
|
|
10
13
|
|
|
11
14
|
#[derive(Deserialize)]
|
|
12
15
|
struct EvalRequest {
|
|
@@ -14,11 +17,30 @@ struct EvalRequest {
|
|
|
14
17
|
token: String,
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
#[derive(Deserialize)]
|
|
21
|
+
struct LogRequest {
|
|
22
|
+
token: String,
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
#[derive(Serialize)]
|
|
18
26
|
struct EvalResponse {
|
|
19
27
|
result: serde_json::Value,
|
|
20
28
|
}
|
|
21
29
|
|
|
30
|
+
#[derive(Clone, Serialize)]
|
|
31
|
+
pub struct LogEntry {
|
|
32
|
+
pub timestamp: u64,
|
|
33
|
+
pub level: String,
|
|
34
|
+
pub target: String,
|
|
35
|
+
pub message: String,
|
|
36
|
+
pub source: String,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#[derive(Serialize)]
|
|
40
|
+
struct LogResponse {
|
|
41
|
+
entries: Vec<LogEntry>,
|
|
42
|
+
}
|
|
43
|
+
|
|
22
44
|
#[derive(Serialize)]
|
|
23
45
|
struct TokenFile {
|
|
24
46
|
port: u16,
|
|
@@ -26,6 +48,172 @@ struct TokenFile {
|
|
|
26
48
|
pid: u32,
|
|
27
49
|
}
|
|
28
50
|
|
|
51
|
+
/// Ring buffer for log entries. Thread-safe, capped at 1000 entries.
|
|
52
|
+
pub struct LogBuffer {
|
|
53
|
+
entries: Mutex<VecDeque<LogEntry>>,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
impl LogBuffer {
|
|
57
|
+
pub fn new() -> Self {
|
|
58
|
+
Self {
|
|
59
|
+
entries: Mutex::new(VecDeque::new()),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pub fn push(&self, entry: LogEntry) {
|
|
64
|
+
let mut buf = self.entries.lock().unwrap();
|
|
65
|
+
if buf.len() >= 1000 {
|
|
66
|
+
buf.pop_front();
|
|
67
|
+
}
|
|
68
|
+
buf.push_back(entry);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pub fn drain(&self) -> Vec<LogEntry> {
|
|
72
|
+
let mut buf = self.entries.lock().unwrap();
|
|
73
|
+
buf.drain(..).collect()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// A tracing layer that captures log events into a `LogBuffer`.
|
|
78
|
+
struct BridgeLogLayer {
|
|
79
|
+
buffer: Arc<LogBuffer>,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
impl<S> tracing_subscriber::Layer<S> for BridgeLogLayer
|
|
83
|
+
where
|
|
84
|
+
S: tracing::Subscriber,
|
|
85
|
+
{
|
|
86
|
+
fn on_event(
|
|
87
|
+
&self,
|
|
88
|
+
event: &tracing::Event<'_>,
|
|
89
|
+
_ctx: tracing_subscriber::layer::Context<'_, S>,
|
|
90
|
+
) {
|
|
91
|
+
let mut visitor = MessageVisitor {
|
|
92
|
+
message: String::new(),
|
|
93
|
+
};
|
|
94
|
+
event.record(&mut visitor);
|
|
95
|
+
|
|
96
|
+
let entry = LogEntry {
|
|
97
|
+
timestamp: std::time::SystemTime::now()
|
|
98
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
99
|
+
.unwrap_or_default()
|
|
100
|
+
.as_millis() as u64,
|
|
101
|
+
level: event.metadata().level().to_string().to_lowercase(),
|
|
102
|
+
target: event.metadata().target().to_string(),
|
|
103
|
+
message: visitor.message,
|
|
104
|
+
source: "rust".to_string(),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
self.buffer.push(entry);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
struct MessageVisitor {
|
|
112
|
+
message: String,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
impl tracing::field::Visit for MessageVisitor {
|
|
116
|
+
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
|
|
117
|
+
if field.name() == "message" {
|
|
118
|
+
self.message = format!("{:?}", value);
|
|
119
|
+
// Remove surrounding quotes if present
|
|
120
|
+
if self.message.starts_with('"') && self.message.ends_with('"') {
|
|
121
|
+
self.message = self.message[1..self.message.len() - 1].to_string();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
|
|
127
|
+
if field.name() == "message" {
|
|
128
|
+
self.message = value.to_string();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Create a tracing layer that captures logs into the given buffer.
|
|
134
|
+
/// Use this if you already have a tracing subscriber and want to add log capture.
|
|
135
|
+
///
|
|
136
|
+
/// ```rust
|
|
137
|
+
/// use tracing_subscriber::layer::SubscriberExt;
|
|
138
|
+
/// use tracing_subscriber::util::SubscriberInitExt;
|
|
139
|
+
///
|
|
140
|
+
/// let buffer = std::sync::Arc::new(dev_bridge::LogBuffer::new());
|
|
141
|
+
/// tracing_subscriber::registry()
|
|
142
|
+
/// .with(dev_bridge::create_log_layer(buffer.clone()))
|
|
143
|
+
/// .with(tracing_subscriber::fmt::layer())
|
|
144
|
+
/// .init();
|
|
145
|
+
/// ```
|
|
146
|
+
pub fn create_log_layer(
|
|
147
|
+
buffer: Arc<LogBuffer>,
|
|
148
|
+
) -> impl tracing_subscriber::Layer<tracing_subscriber::Registry> {
|
|
149
|
+
BridgeLogLayer { buffer }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Spawn a sidecar process with monitored stdout/stderr.
|
|
153
|
+
/// Lines from stdout are logged as "info", lines from stderr as "warn".
|
|
154
|
+
/// Returns the `std::process::Child` handle.
|
|
155
|
+
pub fn spawn_sidecar_monitored(
|
|
156
|
+
name: &str,
|
|
157
|
+
command: &str,
|
|
158
|
+
args: &[&str],
|
|
159
|
+
log_buffer: &Arc<LogBuffer>,
|
|
160
|
+
) -> Result<std::process::Child, String> {
|
|
161
|
+
let mut child = Command::new(command)
|
|
162
|
+
.args(args)
|
|
163
|
+
.stdout(Stdio::piped())
|
|
164
|
+
.stderr(Stdio::piped())
|
|
165
|
+
.spawn()
|
|
166
|
+
.map_err(|e| format!("Failed to spawn sidecar {name}: {e}"))?;
|
|
167
|
+
|
|
168
|
+
let source = format!("sidecar:{name}");
|
|
169
|
+
|
|
170
|
+
// Monitor stdout
|
|
171
|
+
if let Some(stdout) = child.stdout.take() {
|
|
172
|
+
let buffer = log_buffer.clone();
|
|
173
|
+
let source = source.clone();
|
|
174
|
+
thread::spawn(move || {
|
|
175
|
+
let reader = BufReader::new(stdout);
|
|
176
|
+
for line in reader.lines() {
|
|
177
|
+
let Ok(line) = line else { break };
|
|
178
|
+
buffer.push(LogEntry {
|
|
179
|
+
timestamp: std::time::SystemTime::now()
|
|
180
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
181
|
+
.unwrap_or_default()
|
|
182
|
+
.as_millis() as u64,
|
|
183
|
+
level: "info".to_string(),
|
|
184
|
+
target: "stdout".to_string(),
|
|
185
|
+
message: line,
|
|
186
|
+
source: source.clone(),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Monitor stderr
|
|
193
|
+
if let Some(stderr) = child.stderr.take() {
|
|
194
|
+
let buffer = log_buffer.clone();
|
|
195
|
+
let source = source.clone();
|
|
196
|
+
thread::spawn(move || {
|
|
197
|
+
let reader = BufReader::new(stderr);
|
|
198
|
+
for line in reader.lines() {
|
|
199
|
+
let Ok(line) = line else { break };
|
|
200
|
+
buffer.push(LogEntry {
|
|
201
|
+
timestamp: std::time::SystemTime::now()
|
|
202
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
203
|
+
.unwrap_or_default()
|
|
204
|
+
.as_millis() as u64,
|
|
205
|
+
level: "warn".to_string(),
|
|
206
|
+
target: "stderr".to_string(),
|
|
207
|
+
message: line,
|
|
208
|
+
source: source.clone(),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
Ok(child)
|
|
215
|
+
}
|
|
216
|
+
|
|
29
217
|
/// Shared state for pending eval results.
|
|
30
218
|
/// The HTTP handler thread waits on the Condvar; the Tauri command inserts
|
|
31
219
|
/// the result and signals.
|
|
@@ -47,8 +235,9 @@ pub fn __dev_bridge_result(
|
|
|
47
235
|
}
|
|
48
236
|
|
|
49
237
|
/// Start the development bridge HTTP server.
|
|
50
|
-
/// Returns the port number on success.
|
|
51
|
-
|
|
238
|
+
/// Returns the port number and log buffer on success.
|
|
239
|
+
/// The log buffer can be used with `spawn_sidecar_monitored()` to capture sidecar output.
|
|
240
|
+
pub fn start_bridge(app: &AppHandle) -> Result<(u16, Arc<LogBuffer>), String> {
|
|
52
241
|
let server =
|
|
53
242
|
Server::http("127.0.0.1:0").map_err(|e| format!("Failed to start bridge: {e}"))?;
|
|
54
243
|
let port = server
|
|
@@ -80,6 +269,13 @@ pub fn start_bridge(app: &AppHandle) -> Result<u16, String> {
|
|
|
80
269
|
let _ = fs::remove_file(&cleanup_path);
|
|
81
270
|
});
|
|
82
271
|
|
|
272
|
+
// Create log buffer and install tracing layer
|
|
273
|
+
let log_buffer = Arc::new(LogBuffer::new());
|
|
274
|
+
let layer = BridgeLogLayer {
|
|
275
|
+
buffer: log_buffer.clone(),
|
|
276
|
+
};
|
|
277
|
+
let _ = tracing_subscriber::registry().with(layer).try_init();
|
|
278
|
+
|
|
83
279
|
// Create shared pending-results state and register it with Tauri
|
|
84
280
|
let pending = Arc::new(PendingResults {
|
|
85
281
|
results: Mutex::new(HashMap::new()),
|
|
@@ -89,13 +285,17 @@ pub fn start_bridge(app: &AppHandle) -> Result<u16, String> {
|
|
|
89
285
|
|
|
90
286
|
let app_handle = app.clone();
|
|
91
287
|
let expected_token = token.clone();
|
|
288
|
+
let server_log_buffer = log_buffer.clone();
|
|
92
289
|
|
|
93
290
|
thread::spawn(move || {
|
|
94
291
|
// Keep _guard alive for the lifetime of the server thread
|
|
95
292
|
let _cleanup = _guard;
|
|
96
293
|
|
|
97
294
|
for request in server.incoming_requests() {
|
|
98
|
-
|
|
295
|
+
let is_post = request.method().as_str() == "POST";
|
|
296
|
+
let url = request.url().to_string();
|
|
297
|
+
|
|
298
|
+
if !is_post || (url != "/eval" && url != "/logs") {
|
|
99
299
|
let _ = request.respond(Response::from_string("Not found").with_status_code(404));
|
|
100
300
|
continue;
|
|
101
301
|
}
|
|
@@ -108,7 +308,32 @@ pub fn start_bridge(app: &AppHandle) -> Result<u16, String> {
|
|
|
108
308
|
continue;
|
|
109
309
|
}
|
|
110
310
|
|
|
111
|
-
//
|
|
311
|
+
// Handle /logs endpoint
|
|
312
|
+
if url == "/logs" {
|
|
313
|
+
let log_req: LogRequest = match serde_json::from_str(&body) {
|
|
314
|
+
Ok(r) => r,
|
|
315
|
+
Err(_) => {
|
|
316
|
+
let _ = request
|
|
317
|
+
.respond(Response::from_string("Invalid JSON").with_status_code(400));
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
if log_req.token != expected_token {
|
|
323
|
+
let _ = request
|
|
324
|
+
.respond(Response::from_string("Unauthorized").with_status_code(401));
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let entries = server_log_buffer.drain();
|
|
329
|
+
let resp = LogResponse { entries };
|
|
330
|
+
let json = serde_json::to_string(&resp).unwrap();
|
|
331
|
+
let header = Header::from_bytes("Content-Type", "application/json").unwrap();
|
|
332
|
+
let _ = request.respond(Response::from_string(json).with_header(header));
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Handle /eval endpoint
|
|
112
337
|
let eval_req: EvalRequest = match serde_json::from_str(&body) {
|
|
113
338
|
Ok(r) => r,
|
|
114
339
|
Err(_) => {
|
|
@@ -214,5 +439,5 @@ pub fn start_bridge(app: &AppHandle) -> Result<u16, String> {
|
|
|
214
439
|
eprintln!("Dev bridge started on port {port}");
|
|
215
440
|
eprintln!("Token file: {token_path}");
|
|
216
441
|
|
|
217
|
-
Ok(port)
|
|
442
|
+
Ok((port, log_buffer))
|
|
218
443
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tauri-agent-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Agent-driven inspection toolkit for Tauri desktop apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"test": "vitest run",
|
|
14
14
|
"test:watch": "vitest",
|
|
15
15
|
"dev": "tsc --watch",
|
|
16
|
-
"
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"lint": "tsc --noEmit && eslint src/",
|
|
17
18
|
"lint:fix": "eslint src/ --fix",
|
|
18
19
|
"prepublishOnly": "npm run build"
|
|
19
20
|
},
|
|
@@ -43,11 +44,13 @@
|
|
|
43
44
|
"access": "public"
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
|
-
"commander": "^14.0.0"
|
|
47
|
+
"commander": "^14.0.0",
|
|
48
|
+
"zod": "^3.25.76"
|
|
47
49
|
},
|
|
48
50
|
"devDependencies": {
|
|
49
51
|
"@eslint/js": "^9.0.0",
|
|
50
52
|
"@types/node": "^22.0.0",
|
|
53
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
51
54
|
"eslint": "^9.0.0",
|
|
52
55
|
"typescript": "^5.8.0",
|
|
53
56
|
"typescript-eslint": "^8.0.0",
|
package/rust-bridge/README.md
CHANGED
|
@@ -14,6 +14,8 @@ serde_json = "1"
|
|
|
14
14
|
scopeguard = "1"
|
|
15
15
|
rand = "0.8"
|
|
16
16
|
uuid = { version = "1", features = ["v4"] }
|
|
17
|
+
tracing = "0.1"
|
|
18
|
+
tracing-subscriber = "0.3"
|
|
17
19
|
```
|
|
18
20
|
|
|
19
21
|
### 2. Copy the bridge module
|
|
@@ -39,7 +41,7 @@ fn main() {
|
|
|
39
41
|
builder
|
|
40
42
|
.setup(|app| {
|
|
41
43
|
if cfg!(debug_assertions) {
|
|
42
|
-
if let Err(e) = dev_bridge::start_bridge(app.handle()) {
|
|
44
|
+
if let Err(e) = dev_bridge::start_bridge(app.handle()).map(|_| ()) {
|
|
43
45
|
eprintln!("Warning: Failed to start dev bridge: {e}");
|
|
44
46
|
}
|
|
45
47
|
}
|
|
@@ -80,10 +82,12 @@ tauri-agent-tools eval "document.title"
|
|
|
80
82
|
1. Bridge starts an HTTP server on a random localhost port
|
|
81
83
|
2. A token file with `{ port, token, pid }` is written to `/tmp/`
|
|
82
84
|
3. `tauri-agent-tools` discovers the token file and authenticates via the token
|
|
83
|
-
4. Requests are `POST /eval { js, token }`
|
|
84
|
-
5.
|
|
85
|
-
6. The
|
|
86
|
-
7. The
|
|
85
|
+
4. Requests are `POST /eval { js, token }` (JS evaluation) or `POST /logs { token }` (Rust log retrieval)
|
|
86
|
+
5. For `/eval`, the bridge injects JS into the webview
|
|
87
|
+
6. The injected JS evaluates the expression, then calls back into Rust via `window.__TAURI__.core.invoke("__dev_bridge_result", { id, value })` to deliver the result
|
|
88
|
+
7. The HTTP handler thread waits for the result (up to 5 seconds) and returns it as JSON
|
|
89
|
+
8. For `/logs`, the bridge drains its ring buffer of captured `tracing` events and returns them as JSON
|
|
90
|
+
9. The token file is cleaned up when the app exits
|
|
87
91
|
|
|
88
92
|
## Security
|
|
89
93
|
|