talon-agent 1.53.0 → 2.0.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/README.md +1 -1
- package/package.json +3 -2
- package/src/app.ts +11 -0
- package/src/cli/index.ts +0 -15
- package/src/core/doctor.ts +20 -0
- package/src/core/engine/gateway-actions/index.ts +0 -3
- package/src/core/engine/gateway-actions/native.ts +108 -100
- package/src/core/engine/gateway.ts +0 -28
- package/src/core/tools/index.ts +0 -2
- package/src/core/tools/native.ts +16 -8
- package/src/core/tools/types.ts +1 -2
- package/src/core/vfs/fusefs.ts +269 -0
- package/src/core/vfs/index.ts +21 -2
- package/src/core/vfs/nsdir.ts +85 -0
- package/src/core/vfs/rewrite.ts +170 -0
- package/src/core/vfs/vfs.ts +34 -19
- package/src/native/fusefs.ts +97 -0
- package/src/util/config.ts +8 -0
- package/src/util/log.ts +1 -0
- package/src/util/paths.ts +7 -0
- package/src/cli/fs.ts +0 -138
- package/src/core/engine/gateway-actions/vfs.ts +0 -69
- package/src/core/tools/vfs.ts +0 -61
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* talon-fusefs — TypeScript boundary over the FUSE addon
|
|
3
|
+
* (native/talon-fusefs, `bin/talon-fusefs.node`).
|
|
4
|
+
*
|
|
5
|
+
* The addon mounts the talon:// namespace at ~/.talon/ns while the
|
|
6
|
+
* daemon runs: file-backed mounts are served as symlinks (the kernel
|
|
7
|
+
* follows them — heavy file I/O never crosses the FUSE boundary) and
|
|
8
|
+
* synthetic mounts (proc/, plugins/) are served live through a
|
|
9
|
+
* threadsafe callback bridge into the JS Vfs.
|
|
10
|
+
*
|
|
11
|
+
* Per-arch artifact like blake3-napi: present on binary-channel and
|
|
12
|
+
* source installs, absent otherwise — absence simply means the FUSE
|
|
13
|
+
* layer is off and the namespace falls back to the symlink farm.
|
|
14
|
+
* `TALON_FUSEFS_NODE` overrides the location, `TALON_NO_FUSEFS=1`
|
|
15
|
+
* disables it. Linux-only: fuser's pure-Rust mount path (no libfuse
|
|
16
|
+
* link) needs only /dev/fuse + fusermount3 at runtime.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createRequire } from "node:module";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
|
|
22
|
+
/** One root symlink entry the mount serves (a file-backed mount). */
|
|
23
|
+
export interface FuseSymlinkSpec {
|
|
24
|
+
name: string;
|
|
25
|
+
target: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A bridge request from the FUSE thread. Answer by calling `reply(id,
|
|
30
|
+
* json)` — every request MUST be answered or the kernel caller waits
|
|
31
|
+
* out the addon's internal timeout and gets EIO. `path` is
|
|
32
|
+
* namespace-relative ("proc/tasks/7"). The reply JSON shapes are
|
|
33
|
+
* defined by `serveNamespaceRequest` in core/vfs/fusefs.ts.
|
|
34
|
+
*/
|
|
35
|
+
export type FuseBridgeHandler = (id: number, op: string, path: string) => void;
|
|
36
|
+
|
|
37
|
+
/** The N-API surface exported by native/talon-fusefs. */
|
|
38
|
+
export interface NativeFuseFs {
|
|
39
|
+
version(): string;
|
|
40
|
+
/**
|
|
41
|
+
* Mount at `mountpoint` and return once the kernel accepted the
|
|
42
|
+
* mount. Throws when mounting fails (no /dev/fuse, no fusermount,
|
|
43
|
+
* mountpoint busy). The FUSE session runs on its own thread until
|
|
44
|
+
* `unmount()`.
|
|
45
|
+
*/
|
|
46
|
+
mount(
|
|
47
|
+
mountpoint: string,
|
|
48
|
+
symlinks: FuseSymlinkSpec[],
|
|
49
|
+
synthetic: string[],
|
|
50
|
+
onRequest: FuseBridgeHandler,
|
|
51
|
+
): void;
|
|
52
|
+
/** Answer a bridge request. Unknown/expired ids are ignored. */
|
|
53
|
+
reply(id: number, json: string): void;
|
|
54
|
+
/** Tear the mount down. Idempotent. */
|
|
55
|
+
unmount(): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let addon: NativeFuseFs | null | undefined;
|
|
59
|
+
|
|
60
|
+
/** Resolve the addon, or null when the FUSE layer must stay off. */
|
|
61
|
+
export function nativeFuseFs(): NativeFuseFs | null {
|
|
62
|
+
if (addon === undefined) addon = loadNativeFuseFs();
|
|
63
|
+
return addon;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function loadNativeFuseFs(): NativeFuseFs | null {
|
|
67
|
+
if (process.env.TALON_NO_FUSEFS === "1") return null;
|
|
68
|
+
|
|
69
|
+
let candidate = process.env.TALON_FUSEFS_NODE;
|
|
70
|
+
if (!candidate) {
|
|
71
|
+
try {
|
|
72
|
+
// Beside bin/talon.js — where build:fusefs and packaging put it.
|
|
73
|
+
// Throws under bun single-binary builds (no real fs URL): those
|
|
74
|
+
// ship through channels that set TALON_FUSEFS_NODE, or fall back.
|
|
75
|
+
candidate = fileURLToPath(
|
|
76
|
+
new URL("../../bin/talon-fusefs.node", import.meta.url),
|
|
77
|
+
);
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const requireAddon = createRequire(import.meta.url);
|
|
84
|
+
const loaded = requireAddon(candidate) as NativeFuseFs;
|
|
85
|
+
// Trust nothing that can't state its version — a truncated or
|
|
86
|
+
// wrong-arch artifact fails here and the FUSE layer stays off.
|
|
87
|
+
if (typeof loaded.version() !== "string") return null;
|
|
88
|
+
return loaded;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Tests swap addons via TALON_FUSEFS_NODE and need the memo dropped. */
|
|
95
|
+
export function _resetNativeFuseFsForTesting(): void {
|
|
96
|
+
addon = undefined;
|
|
97
|
+
}
|
package/src/util/config.ts
CHANGED
|
@@ -479,6 +479,14 @@ const configSchema = z.object({
|
|
|
479
479
|
// and restart), with an instant rollback by flipping back to false.
|
|
480
480
|
nativeTools: z.boolean().default(false),
|
|
481
481
|
|
|
482
|
+
// FUSE layer for the talon:// namespace (~/.talon/ns). "auto" mounts the
|
|
483
|
+
// live views (proc/, plugins/) when the host can (Linux, /dev/fuse, the
|
|
484
|
+
// talon-fusefs addon present) and degrades to the plain symlink farm with a
|
|
485
|
+
// logged reason when it can't — fuseless hosts get the identical namespace
|
|
486
|
+
// minus live views. "off" never mounts. There is no "force": a mount that
|
|
487
|
+
// can't come up healthy is always rolled back rather than half-served.
|
|
488
|
+
fuse: z.enum(["auto", "off"]).default("auto"),
|
|
489
|
+
|
|
482
490
|
// Display name shown in terminal UI (defaults to "Talon")
|
|
483
491
|
botDisplayName: z.string().default("Talon"),
|
|
484
492
|
|
package/src/util/log.ts
CHANGED
package/src/util/paths.ts
CHANGED
|
@@ -72,6 +72,13 @@ export const dirs = {
|
|
|
72
72
|
skills: resolve(TALON_ROOT, "workspace", "skills"),
|
|
73
73
|
/** Key material (bridge TLS identity, release keys): ~/.talon/keys/ */
|
|
74
74
|
keys: resolve(TALON_ROOT, "keys"),
|
|
75
|
+
/**
|
|
76
|
+
* The talon:// namespace on disk: ~/.talon/ns/. Holds one symlink per
|
|
77
|
+
* file-backed mount (home → workspace/, skills/, …); while the daemon
|
|
78
|
+
* runs with FUSE the synthetic mounts (proc/, plugins/) appear here too.
|
|
79
|
+
* `talon://x` and `~/.talon/ns/x` are the same address in two spellings.
|
|
80
|
+
*/
|
|
81
|
+
ns: resolve(TALON_ROOT, "ns"),
|
|
75
82
|
/** CLI-installed plugins (`talon plugin install`): ~/.talon/plugins/ */
|
|
76
83
|
plugins: resolve(TALON_ROOT, "plugins"),
|
|
77
84
|
} as const;
|
package/src/cli/fs.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `talon ls` / `talon cat` — the talon:// namespace from the outside.
|
|
3
|
-
*
|
|
4
|
-
* A running daemon answers through the gateway (`GET /vfs/list|read`) so
|
|
5
|
-
* the synthetic mounts (proc, plugins) show live state. With no daemon,
|
|
6
|
-
* the same namespace is served in-process — file mounts work identically,
|
|
7
|
-
* the synthetic mounts are simply empty — so browsing the workspace never
|
|
8
|
-
* requires Talon to be up.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import pc from "picocolors";
|
|
12
|
-
import { findRunningInstance } from "../core/daemon/discovery.js";
|
|
13
|
-
import type { VfsErrorCode, VfsResult, VfsStat } from "../core/vfs/index.js";
|
|
14
|
-
import { fetchGateway } from "./daemon-api.js";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Ask the daemon first (live proc/plugins state), fall back to serving the
|
|
18
|
-
* namespace in-process. `live` records which one answered.
|
|
19
|
-
*/
|
|
20
|
-
async function query(
|
|
21
|
-
op: "list" | "read",
|
|
22
|
-
path: string,
|
|
23
|
-
): Promise<{ result: VfsResult<VfsStat[] | string>; live: boolean }> {
|
|
24
|
-
const instance = await findRunningInstance();
|
|
25
|
-
if (instance?.port) {
|
|
26
|
-
const body = (await fetchGateway(
|
|
27
|
-
instance.port,
|
|
28
|
-
`/vfs/${op}?path=${encodeURIComponent(path)}`,
|
|
29
|
-
)) as {
|
|
30
|
-
ok: boolean;
|
|
31
|
-
entries?: VfsStat[];
|
|
32
|
-
content?: string;
|
|
33
|
-
error?: string;
|
|
34
|
-
detail?: string;
|
|
35
|
-
};
|
|
36
|
-
const result: VfsResult<VfsStat[] | string> = body.ok
|
|
37
|
-
? { ok: true, value: op === "list" ? body.entries! : body.content! }
|
|
38
|
-
: {
|
|
39
|
-
ok: false,
|
|
40
|
-
error: (body.error ?? "io-error") as VfsErrorCode,
|
|
41
|
-
...(body.detail !== undefined ? { detail: body.detail } : {}),
|
|
42
|
-
};
|
|
43
|
-
return { result, live: true };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const { getVfs } = await import("../core/vfs/index.js");
|
|
47
|
-
const vfs = getVfs();
|
|
48
|
-
return {
|
|
49
|
-
result: op === "list" ? vfs.list(path) : vfs.read(path),
|
|
50
|
-
live: false,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function printError(
|
|
55
|
-
path: string,
|
|
56
|
-
result: { error: string; detail?: string },
|
|
57
|
-
): void {
|
|
58
|
-
console.error(
|
|
59
|
-
` ${pc.red("✖")} talon://${path}: ${result.error}${result.detail ? ` — ${result.detail}` : ""}`,
|
|
60
|
-
);
|
|
61
|
-
process.exitCode = 1;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function formatModified(entry: VfsStat): string {
|
|
65
|
-
if (entry.modifiedAt === undefined) return "-";
|
|
66
|
-
return new Date(entry.modifiedAt)
|
|
67
|
-
.toISOString()
|
|
68
|
-
.slice(0, 16)
|
|
69
|
-
.replace("T", " ");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export async function showLs(rawPath: string | undefined): Promise<void> {
|
|
73
|
-
const path = (rawPath ?? "").trim();
|
|
74
|
-
console.log();
|
|
75
|
-
const { result, live } = await query("list", path);
|
|
76
|
-
if (!result.ok) {
|
|
77
|
-
printError(path, result);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
const entries = result.value as VfsStat[];
|
|
81
|
-
|
|
82
|
-
if (!live) {
|
|
83
|
-
console.log(
|
|
84
|
-
` ${pc.dim("Talon is not running — file mounts only; proc/ and plugins/ are empty.")}`,
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
if (entries.length === 0) {
|
|
88
|
-
console.log(` ${pc.dim("(empty)")}\n`);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const header = ["KIND", "SIZE", "MODIFIED", "NAME"];
|
|
93
|
-
const cells = entries.map((entry) => [
|
|
94
|
-
entry.kind === "dir" ? "dir" : "file",
|
|
95
|
-
entry.kind === "dir" ? "-" : String(entry.size ?? "?"),
|
|
96
|
-
formatModified(entry),
|
|
97
|
-
entry.kind === "dir" ? `${entry.name}/` : entry.name,
|
|
98
|
-
]);
|
|
99
|
-
const widths = header.map((h, col) =>
|
|
100
|
-
Math.max(h.length, ...cells.map((row) => row[col]!.length)),
|
|
101
|
-
);
|
|
102
|
-
console.log(
|
|
103
|
-
` ${pc.dim(header.map((h, col) => h.padEnd(widths[col]!)).join(" "))}`,
|
|
104
|
-
);
|
|
105
|
-
cells.forEach((row, i) => {
|
|
106
|
-
const padded = row.map((cell, col) => cell.padEnd(widths[col]!));
|
|
107
|
-
const name = entries[i]!.kind === "dir" ? pc.cyan(padded[3]!) : padded[3]!;
|
|
108
|
-
// Root entries carry the mount's disk location — show the mapping, it's
|
|
109
|
-
// what shell commands (which can't read talon://) need.
|
|
110
|
-
const entry = entries[i]!;
|
|
111
|
-
const mapping =
|
|
112
|
-
entry.osPath !== undefined && !entry.path.includes("/")
|
|
113
|
-
? pc.dim(` → ${entry.osPath}`)
|
|
114
|
-
: "";
|
|
115
|
-
console.log(
|
|
116
|
-
` ${padded[0]} ${padded[1]} ${padded[2]} ${name}${mapping}`,
|
|
117
|
-
);
|
|
118
|
-
});
|
|
119
|
-
console.log();
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export async function showCat(rawPath: string | undefined): Promise<void> {
|
|
123
|
-
if (!rawPath || !rawPath.trim()) {
|
|
124
|
-
console.error(
|
|
125
|
-
` ${pc.red("✖")} Usage: ${pc.cyan("talon cat <path>")} — e.g. ${pc.cyan("talon cat proc/events")}`,
|
|
126
|
-
);
|
|
127
|
-
process.exitCode = 1;
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
const path = rawPath.trim();
|
|
131
|
-
const { result } = await query("read", path);
|
|
132
|
-
if (!result.ok) {
|
|
133
|
-
printError(path, result);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
// Raw content, no decoration — `talon cat x | jq` must work.
|
|
137
|
-
process.stdout.write(result.value as string);
|
|
138
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* VFS — the `talon://` namespace over the gateway: list / read / write.
|
|
3
|
-
* Formatting only; the namespace itself lives in core/vfs.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { getVfs, type VfsResult, type VfsStat } from "../../vfs/index.js";
|
|
7
|
-
import { log } from "../../../util/log.js";
|
|
8
|
-
import type { SharedActionHandlers } from "./types.js";
|
|
9
|
-
|
|
10
|
-
function describeError(
|
|
11
|
-
path: string,
|
|
12
|
-
result: { error: string; detail?: string },
|
|
13
|
-
): string {
|
|
14
|
-
return `talon://${path}: ${result.error}${result.detail ? ` — ${result.detail}` : ""}`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function formatSize(entry: VfsStat): string {
|
|
18
|
-
return entry.kind === "dir" ? "-" : String(entry.size ?? "?");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function formatEntry(entry: VfsStat): string {
|
|
22
|
-
const kind = entry.kind === "dir" ? "d" : "-";
|
|
23
|
-
const write = entry.writable ? "w" : "-";
|
|
24
|
-
const suffix = entry.kind === "dir" ? "/" : "";
|
|
25
|
-
// Root entries (single-segment paths) show where the mount lives on
|
|
26
|
-
// disk — the bridge for OS-level tools, which can't read talon://.
|
|
27
|
-
const mapping =
|
|
28
|
-
entry.osPath !== undefined && !entry.path.includes("/")
|
|
29
|
-
? ` → ${entry.osPath}`
|
|
30
|
-
: "";
|
|
31
|
-
return `${kind}${write} ${formatSize(entry).padStart(9)} ${entry.name}${suffix}${mapping}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function normalize(body: Record<string, unknown>): string {
|
|
35
|
-
return String(body.path ?? "").trim();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export const vfsHandlers: SharedActionHandlers = {
|
|
39
|
-
vfs_list: (body) => {
|
|
40
|
-
const path = normalize(body);
|
|
41
|
-
const result = getVfs().list(path);
|
|
42
|
-
if (!result.ok) return { ok: false, error: describeError(path, result) };
|
|
43
|
-
const header = `talon://${path.replace(/\/+$/, "")}${path ? "/" : ""} (${result.value.length} entries)`;
|
|
44
|
-
const lines = result.value.map(formatEntry);
|
|
45
|
-
return {
|
|
46
|
-
ok: true,
|
|
47
|
-
text: [header, ...(lines.length > 0 ? lines : ["(empty)"])].join("\n"),
|
|
48
|
-
};
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
vfs_read: (body) => {
|
|
52
|
-
const path = normalize(body);
|
|
53
|
-
const result = getVfs().read(path);
|
|
54
|
-
if (!result.ok) return { ok: false, error: describeError(path, result) };
|
|
55
|
-
return { ok: true, text: result.value };
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
vfs_write: (body) => {
|
|
59
|
-
const path = normalize(body);
|
|
60
|
-
const content = String(body.content ?? "");
|
|
61
|
-
const result: VfsResult<VfsStat> = getVfs().write(path, content);
|
|
62
|
-
if (!result.ok) return { ok: false, error: describeError(path, result) };
|
|
63
|
-
log("gateway", `vfs_write: talon://${result.value.path}`);
|
|
64
|
-
return {
|
|
65
|
-
ok: true,
|
|
66
|
-
text: `Wrote ${result.value.size ?? content.length} bytes to talon://${result.value.path}`,
|
|
67
|
-
};
|
|
68
|
-
},
|
|
69
|
-
};
|
package/src/core/tools/vfs.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* VFS tools — the unified `talon://` namespace.
|
|
3
|
-
*
|
|
4
|
-
* One browsable tree over everything the daemon owns: real stores (the
|
|
5
|
-
* workspace, skills, scripts, logs) and live synthetic state (`proc/` for
|
|
6
|
-
* the task table and event bus, `plugins/` for the registry) answer the
|
|
7
|
-
* same three tools. The synthetic mounts are the point — this is the only
|
|
8
|
-
* way to read live daemon state as plain files.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { z } from "zod";
|
|
12
|
-
import type { ToolDefinition } from "./types.js";
|
|
13
|
-
|
|
14
|
-
const NAMESPACE = `Namespace roots: home/ (workspace), skills/, scripts/, logs/, proc/ (live task table under proc/tasks/<id> and the event ring at proc/events), plugins/ (registry view). List "" to see the root and where each mount lives on disk. Absolute OS paths inside a mounted directory are accepted and translated.`;
|
|
15
|
-
|
|
16
|
-
export const vfsTools: ToolDefinition[] = [
|
|
17
|
-
{
|
|
18
|
-
name: "vfs_list",
|
|
19
|
-
description: `List a directory in the talon:// namespace — the unified view over Talon's own state. ${NAMESPACE} Entries show kind (d/-), writability, size, and name.`,
|
|
20
|
-
schema: {
|
|
21
|
-
path: z
|
|
22
|
-
.string()
|
|
23
|
-
.describe(
|
|
24
|
-
'Namespace path, e.g. "" (root), "proc/tasks", "skills/review-pr". Forward slashes on every platform; "talon://" prefix optional.',
|
|
25
|
-
),
|
|
26
|
-
},
|
|
27
|
-
execute: (params, bridge) => bridge("vfs_list", params),
|
|
28
|
-
tag: "vfs",
|
|
29
|
-
},
|
|
30
|
-
|
|
31
|
-
{
|
|
32
|
-
name: "vfs_read",
|
|
33
|
-
description: `Read a file from the talon:// namespace (UTF-8 text, 256KB cap). ${NAMESPACE} Reading proc/tasks/<id> returns that task as JSON; proc/events returns the event ring as JSON Lines.`,
|
|
34
|
-
schema: {
|
|
35
|
-
path: z
|
|
36
|
-
.string()
|
|
37
|
-
.describe(
|
|
38
|
-
'Namespace file path, e.g. "proc/events" or "logs/2026-07-16.md"',
|
|
39
|
-
),
|
|
40
|
-
},
|
|
41
|
-
execute: (params, bridge) => bridge("vfs_read", params),
|
|
42
|
-
tag: "vfs",
|
|
43
|
-
},
|
|
44
|
-
|
|
45
|
-
{
|
|
46
|
-
name: "vfs_write",
|
|
47
|
-
description: `Write a UTF-8 file into a writable part of the talon:// namespace (home/, skills/, scripts/). Parent directories are created; synthetic mounts (proc/, plugins/) and logs/ are read-only views.`,
|
|
48
|
-
schema: {
|
|
49
|
-
path: z
|
|
50
|
-
.string()
|
|
51
|
-
.describe(
|
|
52
|
-
'Namespace file path under a writable mount, e.g. "home/notes/ideas.md"',
|
|
53
|
-
),
|
|
54
|
-
content: z
|
|
55
|
-
.string()
|
|
56
|
-
.describe("Full file content (replaces any existing content)"),
|
|
57
|
-
},
|
|
58
|
-
execute: (params, bridge) => bridge("vfs_write", params),
|
|
59
|
-
tag: "vfs",
|
|
60
|
-
},
|
|
61
|
-
];
|