infinicode 2.8.27 → 2.8.29

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.
@@ -1,124 +1,124 @@
1
- import { createSignal, onCleanup } from "solid-js";
2
- import { ASCII_VIDEO_FPS, ASCII_VIDEO_FRAMES } from "../../dist/ascii-video-animation.js";
3
-
4
- // On the home screen the logo slot shares the column with (above it) a flexible
5
- // spacer + a 4-row gap, and (below it) the prompt box and footer. Those lower
6
- // elements do not shrink, so we must keep the logo within `terminalHeight minus
7
- // this reserve` or its top/bottom gets clipped off the screen.
8
- const VERTICAL_CHROME_ROWS = 9;
9
- // Fallbacks used only before the renderer reports a real size.
10
- const FALLBACK_COLUMNS = 120;
11
- const FALLBACK_ROWS = 40;
12
-
13
- type BoundingBox = {
14
- lines: string[];
15
- left: number;
16
- width: number;
17
- };
18
-
19
- function boundingBox(frame: string): BoundingBox | null {
20
- const lines = frame.split("\n");
21
- const nonEmptyRows = lines
22
- .map((line, index) => (line.trim() ? index : -1))
23
- .filter(index => index >= 0);
24
-
25
- if (nonEmptyRows.length === 0) return null;
26
-
27
- const top = nonEmptyRows[0];
28
- const bottom = nonEmptyRows[nonEmptyRows.length - 1];
29
- const visibleLines = lines.slice(top, bottom + 1).map(line => line.replace(/\s+$/, ""));
30
-
31
- const left = Math.min(
32
- ...visibleLines.map(line => (line.length ? line.length - line.trimStart().length : Number.POSITIVE_INFINITY))
33
- );
34
- const right = Math.max(...visibleLines.map(line => line.length), 0);
35
-
36
- return { lines: visibleLines, left: Number.isFinite(left) ? left : 0, width: Math.max(0, right - left) };
37
- }
38
-
39
- // Sample `count` evenly spaced indices across a span of `length` items.
40
- function sampleIndices(length: number, count: number): number[] {
41
- if (count >= length) return Array.from({ length }, (_, index) => index);
42
- const indices: number[] = [];
43
- for (let i = 0; i < count; i++) {
44
- indices.push(Math.min(length - 1, Math.round((i * (length - 1)) / (count - 1 || 1))));
45
- }
46
- return indices;
47
- }
48
-
49
- function fitFrame(frame: string, terminalColumns: number, terminalRows: number): string {
50
- const box = boundingBox(frame);
51
- if (!box) return "";
52
-
53
- const { lines, left, width } = box;
54
- const height = lines.length;
55
-
56
- const availableColumns = Math.max(20, terminalColumns - 2);
57
- const availableRows = Math.max(6, terminalRows - VERTICAL_CHROME_ROWS);
58
-
59
- // Single uniform scale so the whole logo fits without distorting its aspect.
60
- const scale = Math.min(1, availableColumns / Math.max(1, width), availableRows / height);
61
- const targetHeight = Math.max(1, Math.round(height * scale));
62
- const targetWidth = Math.max(1, Math.round(width * scale));
63
-
64
- const rowIndices = sampleIndices(height, targetHeight);
65
- const columnIndices = sampleIndices(width, targetWidth);
66
-
67
- return rowIndices
68
- .map(rowIndex => {
69
- const line = lines[rowIndex];
70
- return columnIndices
71
- .map(columnIndex => line[left + columnIndex] ?? " ")
72
- .join("")
73
- .replace(/\s+$/, "");
74
- })
75
- .join("\n");
76
- }
77
-
78
- function AnimatedLogo(props: { renderer?: any }) {
79
- const [frameIndex, setFrameIndex] = createSignal(0);
80
-
81
- // Track the real terminal size from the TUI renderer (process.stdout.rows is
82
- // unreliable inside the plugin runtime). Re-fit whenever the terminal resizes.
83
- const renderer = props.renderer;
84
- const readSize = () => ({
85
- columns: Math.max(40, renderer?.width ?? process.stdout.columns ?? FALLBACK_COLUMNS),
86
- rows: Math.max(10, renderer?.height ?? process.stdout.rows ?? FALLBACK_ROWS),
87
- });
88
- const [size, setSize] = createSignal(readSize());
89
-
90
- if (typeof renderer?.on === "function") {
91
- const onResize = () => setSize(readSize());
92
- renderer.on("resize", onResize);
93
- onCleanup(() => {
94
- if (typeof renderer.off === "function") renderer.off("resize", onResize);
95
- else if (typeof renderer.removeListener === "function") renderer.removeListener("resize", onResize);
96
- });
97
- }
98
-
99
- const frameDelayMs = Math.max(16, Math.round(1000 / ASCII_VIDEO_FPS));
100
- const timer = setInterval(() => {
101
- setFrameIndex(index => (index + 1) % ASCII_VIDEO_FRAMES.length);
102
- }, frameDelayMs);
103
- onCleanup(() => clearInterval(timer));
104
-
105
- return (
106
- <text fg="#fbbf24">
107
- {fitFrame(ASCII_VIDEO_FRAMES[frameIndex()] ?? "", size().columns, size().rows)}
108
- </text>
109
- );
110
- }
111
-
112
- const tui = async (api: any) => {
113
- api.slots.register({
114
- slots: {
115
- home_logo() {
116
- return <AnimatedLogo renderer={api.renderer} />;
117
- },
118
- },
119
- });
120
- };
121
-
122
- // The TUI plugin loader reads `mod.default` (strict mode) and requires an object
123
- // with a tui() function — a bare `export const tui` is silently skipped.
124
- export default { id: "infinicode-home-logo", tui };
1
+ import { createSignal, onCleanup } from "solid-js";
2
+ import { ASCII_VIDEO_FPS, ASCII_VIDEO_FRAMES } from "../../dist/ascii-video-animation.js";
3
+
4
+ // On the home screen the logo slot shares the column with (above it) a flexible
5
+ // spacer + a 4-row gap, and (below it) the prompt box and footer. Those lower
6
+ // elements do not shrink, so we must keep the logo within `terminalHeight minus
7
+ // this reserve` or its top/bottom gets clipped off the screen.
8
+ const VERTICAL_CHROME_ROWS = 9;
9
+ // Fallbacks used only before the renderer reports a real size.
10
+ const FALLBACK_COLUMNS = 120;
11
+ const FALLBACK_ROWS = 40;
12
+
13
+ type BoundingBox = {
14
+ lines: string[];
15
+ left: number;
16
+ width: number;
17
+ };
18
+
19
+ function boundingBox(frame: string): BoundingBox | null {
20
+ const lines = frame.split("\n");
21
+ const nonEmptyRows = lines
22
+ .map((line, index) => (line.trim() ? index : -1))
23
+ .filter(index => index >= 0);
24
+
25
+ if (nonEmptyRows.length === 0) return null;
26
+
27
+ const top = nonEmptyRows[0];
28
+ const bottom = nonEmptyRows[nonEmptyRows.length - 1];
29
+ const visibleLines = lines.slice(top, bottom + 1).map(line => line.replace(/\s+$/, ""));
30
+
31
+ const left = Math.min(
32
+ ...visibleLines.map(line => (line.length ? line.length - line.trimStart().length : Number.POSITIVE_INFINITY))
33
+ );
34
+ const right = Math.max(...visibleLines.map(line => line.length), 0);
35
+
36
+ return { lines: visibleLines, left: Number.isFinite(left) ? left : 0, width: Math.max(0, right - left) };
37
+ }
38
+
39
+ // Sample `count` evenly spaced indices across a span of `length` items.
40
+ function sampleIndices(length: number, count: number): number[] {
41
+ if (count >= length) return Array.from({ length }, (_, index) => index);
42
+ const indices: number[] = [];
43
+ for (let i = 0; i < count; i++) {
44
+ indices.push(Math.min(length - 1, Math.round((i * (length - 1)) / (count - 1 || 1))));
45
+ }
46
+ return indices;
47
+ }
48
+
49
+ function fitFrame(frame: string, terminalColumns: number, terminalRows: number): string {
50
+ const box = boundingBox(frame);
51
+ if (!box) return "";
52
+
53
+ const { lines, left, width } = box;
54
+ const height = lines.length;
55
+
56
+ const availableColumns = Math.max(20, terminalColumns - 2);
57
+ const availableRows = Math.max(6, terminalRows - VERTICAL_CHROME_ROWS);
58
+
59
+ // Single uniform scale so the whole logo fits without distorting its aspect.
60
+ const scale = Math.min(1, availableColumns / Math.max(1, width), availableRows / height);
61
+ const targetHeight = Math.max(1, Math.round(height * scale));
62
+ const targetWidth = Math.max(1, Math.round(width * scale));
63
+
64
+ const rowIndices = sampleIndices(height, targetHeight);
65
+ const columnIndices = sampleIndices(width, targetWidth);
66
+
67
+ return rowIndices
68
+ .map(rowIndex => {
69
+ const line = lines[rowIndex];
70
+ return columnIndices
71
+ .map(columnIndex => line[left + columnIndex] ?? " ")
72
+ .join("")
73
+ .replace(/\s+$/, "");
74
+ })
75
+ .join("\n");
76
+ }
77
+
78
+ function AnimatedLogo(props: { renderer?: any }) {
79
+ const [frameIndex, setFrameIndex] = createSignal(0);
80
+
81
+ // Track the real terminal size from the TUI renderer (process.stdout.rows is
82
+ // unreliable inside the plugin runtime). Re-fit whenever the terminal resizes.
83
+ const renderer = props.renderer;
84
+ const readSize = () => ({
85
+ columns: Math.max(40, renderer?.width ?? process.stdout.columns ?? FALLBACK_COLUMNS),
86
+ rows: Math.max(10, renderer?.height ?? process.stdout.rows ?? FALLBACK_ROWS),
87
+ });
88
+ const [size, setSize] = createSignal(readSize());
89
+
90
+ if (typeof renderer?.on === "function") {
91
+ const onResize = () => setSize(readSize());
92
+ renderer.on("resize", onResize);
93
+ onCleanup(() => {
94
+ if (typeof renderer.off === "function") renderer.off("resize", onResize);
95
+ else if (typeof renderer.removeListener === "function") renderer.removeListener("resize", onResize);
96
+ });
97
+ }
98
+
99
+ const frameDelayMs = Math.max(16, Math.round(1000 / ASCII_VIDEO_FPS));
100
+ const timer = setInterval(() => {
101
+ setFrameIndex(index => (index + 1) % ASCII_VIDEO_FRAMES.length);
102
+ }, frameDelayMs);
103
+ onCleanup(() => clearInterval(timer));
104
+
105
+ return (
106
+ <text fg="#fbbf24">
107
+ {fitFrame(ASCII_VIDEO_FRAMES[frameIndex()] ?? "", size().columns, size().rows)}
108
+ </text>
109
+ );
110
+ }
111
+
112
+ const tui = async (api: any) => {
113
+ api.slots.register({
114
+ slots: {
115
+ home_logo() {
116
+ return <AnimatedLogo renderer={api.renderer} />;
117
+ },
118
+ },
119
+ });
120
+ };
121
+
122
+ // The TUI plugin loader reads `mod.default` (strict mode) and requires an object
123
+ // with a tui() function — a bare `export const tui` is silently skipped.
124
+ export default { id: "infinicode-home-logo", tui };
@@ -1,140 +1,140 @@
1
- /** @jsxImportSource @opentui/solid */
2
- /**
3
- * infinicode — mesh command palette
4
- *
5
- * Adds the OpenKernel mesh/kernel slash-commands natively to the TUI's `/`
6
- * command palette. Each command is dispatched to a LOCAL mesh node over HTTP
7
- * (POST /fed/command → kernel.command()), so the same command surface exposed by
8
- * `infinicode serve`'s console and the CLI is available right inside the TUI.
9
- *
10
- * The local node is started (or reused) by `infinicode run`, which injects:
11
- * INFINICODE_MESH_URL e.g. http://127.0.0.1:47913
12
- * INFINICODE_MESH_TOKEN optional bearer token
13
- *
14
- * Argless commands (/nodes, /running, /activity, /whoami, /mesh-help) run
15
- * immediately and show output in an alert. Arg-taking commands (/node, /build,
16
- * /follow, /dispatch, /goal, /mission, /role) open a prompt first.
17
- */
18
- import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
19
-
20
- type CommandResult = { ok?: boolean; text?: string; data?: unknown }
21
-
22
- const meshUrl = () => (process.env.INFINICODE_MESH_URL ?? "http://127.0.0.1:47913").replace(/\/+$/, "")
23
- const meshToken = () => process.env.INFINICODE_MESH_TOKEN
24
-
25
- /** POST a slash-command to the local mesh node and return its CommandResult. */
26
- async function send(input: string): Promise<CommandResult> {
27
- const token = meshToken()
28
- try {
29
- const res = await fetch(`${meshUrl()}/fed/command`, {
30
- method: "POST",
31
- headers: {
32
- "content-type": "application/json",
33
- ...(token ? { authorization: `Bearer ${token}` } : {}),
34
- },
35
- body: JSON.stringify({ input }),
36
- signal: AbortSignal.timeout(30_000),
37
- })
38
- if (!res.ok) return { ok: false, text: `mesh error ${res.status} — is a node running? (infinicode serve)` }
39
- return (await res.json()) as CommandResult
40
- } catch (e) {
41
- const msg = e instanceof Error ? e.message : String(e)
42
- return { ok: false, text: `no local mesh node reachable at ${meshUrl()} (${msg})` }
43
- }
44
- }
45
-
46
- /** Render a CommandResult in a scrollable alert dialog. */
47
- function show(api: TuiPluginApi, title: string, res: CommandResult): void {
48
- const DialogAlert = api.ui.DialogAlert
49
- const message = (res.text && res.text.trim()) || (res.ok ? "(no output)" : "command failed")
50
- api.ui.dialog.setSize("large")
51
- api.ui.dialog.replace(() => (
52
- <DialogAlert title={title} message={message} onConfirm={() => api.ui.dialog.clear()} />
53
- ))
54
- }
55
-
56
- /** Run a fixed slash-command now and show the result. */
57
- function runNow(api: TuiPluginApi, title: string, input: string): void {
58
- api.ui.toast({ variant: "info", title: "mesh", message: `${input} …`, duration: 1200 })
59
- void send(input).then((res) => show(api, title, res))
60
- }
61
-
62
- /** Open a prompt, then dispatch `<slash> <entered args>`. */
63
- function runWithArgs(
64
- api: TuiPluginApi,
65
- title: string,
66
- slash: string,
67
- placeholder: string,
68
- ): void {
69
- const DialogPrompt = api.ui.DialogPrompt
70
- api.ui.dialog.setSize("medium")
71
- api.ui.dialog.replace(() => (
72
- <DialogPrompt
73
- title={title}
74
- value=""
75
- placeholder={placeholder}
76
- onConfirm={(args: string) => {
77
- api.ui.dialog.clear()
78
- const input = args && args.trim() ? `${slash} ${args.trim()}` : slash
79
- api.ui.toast({ variant: "info", title: "mesh", message: `${input} …`, duration: 1200 })
80
- void send(input).then((res) => show(api, title, res))
81
- }}
82
- onCancel={() => api.ui.dialog.clear()}
83
- />
84
- ))
85
- }
86
-
87
- interface Spec {
88
- name: string
89
- slashName: string
90
- title: string
91
- /** if set, prompt for args first with this placeholder; else run immediately */
92
- arg?: string
93
- /** the underlying kernel slash-command (defaults to `/${slashName}`) */
94
- cmd?: string
95
- }
96
-
97
- const SPECS: Spec[] = [
98
- { name: "mesh_nodes", slashName: "nodes", title: "Mesh nodes" },
99
- { name: "mesh_running", slashName: "running", title: "Running tasks" },
100
- { name: "mesh_activity", slashName: "activity", title: "Mesh activity" },
101
- { name: "mesh_whoami", slashName: "whoami", title: "This node" },
102
- { name: "mesh_help", slashName: "mesh-help", title: "Mesh commands", cmd: "/help" },
103
- { name: "mesh_node", slashName: "node", title: "Node control", arg: "<node> <message>", cmd: "/node" },
104
- { name: "mesh_build", slashName: "build", title: "Phased build", arg: "<node> <goal>", cmd: "/build" },
105
- { name: "mesh_dispatch", slashName: "dispatch", title: "Dispatch task", arg: "<node> <task>", cmd: "/dispatch" },
106
- { name: "mesh_follow", slashName: "follow", title: "Follow run", arg: "<run-id>", cmd: "/follow" },
107
- { name: "mesh_goal", slashName: "goal", title: "New goal", arg: "<goal description>", cmd: "/goal" },
108
- { name: "mesh_mission", slashName: "mission", title: "Mission", arg: "<mission description>", cmd: "/mission" },
109
- { name: "mesh_role", slashName: "role", title: "Set role", arg: "<role>", cmd: "/role" },
110
- ]
111
-
112
- const tui: TuiPlugin = async (api, options) => {
113
- if (options?.enabled === false) return
114
-
115
- // No keybindings — these are palette-only slash commands. Empty bindings avoids
116
- // any dependency on the keymap-extras runtime module (which may not resolve for
117
- // an externally-loaded plugin) so registration can never fail to load.
118
- api.keymap.registerLayer({
119
- commands: SPECS.map((s) => ({
120
- name: s.name,
121
- title: s.title,
122
- category: "Mesh",
123
- namespace: "palette" as const,
124
- slashName: s.slashName,
125
- run() {
126
- const cmd = s.cmd ?? `/${s.slashName}`
127
- if (s.arg) runWithArgs(api, s.title, cmd, s.arg)
128
- else runNow(api, s.title, cmd)
129
- },
130
- })),
131
- bindings: [],
132
- })
133
- }
134
-
135
- const plugin: TuiPluginModule & { id: string } = {
136
- id: "infinicode-mesh-commands",
137
- tui,
138
- }
139
-
140
- export default plugin
1
+ /** @jsxImportSource @opentui/solid */
2
+ /**
3
+ * infinicode — mesh command palette
4
+ *
5
+ * Adds the OpenKernel mesh/kernel slash-commands natively to the TUI's `/`
6
+ * command palette. Each command is dispatched to a LOCAL mesh node over HTTP
7
+ * (POST /fed/command → kernel.command()), so the same command surface exposed by
8
+ * `infinicode serve`'s console and the CLI is available right inside the TUI.
9
+ *
10
+ * The local node is started (or reused) by `infinicode run`, which injects:
11
+ * INFINICODE_MESH_URL e.g. http://127.0.0.1:47913
12
+ * INFINICODE_MESH_TOKEN optional bearer token
13
+ *
14
+ * Argless commands (/nodes, /running, /activity, /whoami, /mesh-help) run
15
+ * immediately and show output in an alert. Arg-taking commands (/node, /build,
16
+ * /follow, /dispatch, /goal, /mission, /role) open a prompt first.
17
+ */
18
+ import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
19
+
20
+ type CommandResult = { ok?: boolean; text?: string; data?: unknown }
21
+
22
+ const meshUrl = () => (process.env.INFINICODE_MESH_URL ?? "http://127.0.0.1:47913").replace(/\/+$/, "")
23
+ const meshToken = () => process.env.INFINICODE_MESH_TOKEN
24
+
25
+ /** POST a slash-command to the local mesh node and return its CommandResult. */
26
+ async function send(input: string): Promise<CommandResult> {
27
+ const token = meshToken()
28
+ try {
29
+ const res = await fetch(`${meshUrl()}/fed/command`, {
30
+ method: "POST",
31
+ headers: {
32
+ "content-type": "application/json",
33
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
34
+ },
35
+ body: JSON.stringify({ input }),
36
+ signal: AbortSignal.timeout(30_000),
37
+ })
38
+ if (!res.ok) return { ok: false, text: `mesh error ${res.status} — is a node running? (infinicode serve)` }
39
+ return (await res.json()) as CommandResult
40
+ } catch (e) {
41
+ const msg = e instanceof Error ? e.message : String(e)
42
+ return { ok: false, text: `no local mesh node reachable at ${meshUrl()} (${msg})` }
43
+ }
44
+ }
45
+
46
+ /** Render a CommandResult in a scrollable alert dialog. */
47
+ function show(api: TuiPluginApi, title: string, res: CommandResult): void {
48
+ const DialogAlert = api.ui.DialogAlert
49
+ const message = (res.text && res.text.trim()) || (res.ok ? "(no output)" : "command failed")
50
+ api.ui.dialog.setSize("large")
51
+ api.ui.dialog.replace(() => (
52
+ <DialogAlert title={title} message={message} onConfirm={() => api.ui.dialog.clear()} />
53
+ ))
54
+ }
55
+
56
+ /** Run a fixed slash-command now and show the result. */
57
+ function runNow(api: TuiPluginApi, title: string, input: string): void {
58
+ api.ui.toast({ variant: "info", title: "mesh", message: `${input} …`, duration: 1200 })
59
+ void send(input).then((res) => show(api, title, res))
60
+ }
61
+
62
+ /** Open a prompt, then dispatch `<slash> <entered args>`. */
63
+ function runWithArgs(
64
+ api: TuiPluginApi,
65
+ title: string,
66
+ slash: string,
67
+ placeholder: string,
68
+ ): void {
69
+ const DialogPrompt = api.ui.DialogPrompt
70
+ api.ui.dialog.setSize("medium")
71
+ api.ui.dialog.replace(() => (
72
+ <DialogPrompt
73
+ title={title}
74
+ value=""
75
+ placeholder={placeholder}
76
+ onConfirm={(args: string) => {
77
+ api.ui.dialog.clear()
78
+ const input = args && args.trim() ? `${slash} ${args.trim()}` : slash
79
+ api.ui.toast({ variant: "info", title: "mesh", message: `${input} …`, duration: 1200 })
80
+ void send(input).then((res) => show(api, title, res))
81
+ }}
82
+ onCancel={() => api.ui.dialog.clear()}
83
+ />
84
+ ))
85
+ }
86
+
87
+ interface Spec {
88
+ name: string
89
+ slashName: string
90
+ title: string
91
+ /** if set, prompt for args first with this placeholder; else run immediately */
92
+ arg?: string
93
+ /** the underlying kernel slash-command (defaults to `/${slashName}`) */
94
+ cmd?: string
95
+ }
96
+
97
+ const SPECS: Spec[] = [
98
+ { name: "mesh_nodes", slashName: "nodes", title: "Mesh nodes" },
99
+ { name: "mesh_running", slashName: "running", title: "Running tasks" },
100
+ { name: "mesh_activity", slashName: "activity", title: "Mesh activity" },
101
+ { name: "mesh_whoami", slashName: "whoami", title: "This node" },
102
+ { name: "mesh_help", slashName: "mesh-help", title: "Mesh commands", cmd: "/help" },
103
+ { name: "mesh_node", slashName: "node", title: "Node control", arg: "<node> <message>", cmd: "/node" },
104
+ { name: "mesh_build", slashName: "build", title: "Phased build", arg: "<node> <goal>", cmd: "/build" },
105
+ { name: "mesh_dispatch", slashName: "dispatch", title: "Dispatch task", arg: "<node> <task>", cmd: "/dispatch" },
106
+ { name: "mesh_follow", slashName: "follow", title: "Follow run", arg: "<run-id>", cmd: "/follow" },
107
+ { name: "mesh_goal", slashName: "goal", title: "New goal", arg: "<goal description>", cmd: "/goal" },
108
+ { name: "mesh_mission", slashName: "mission", title: "Mission", arg: "<mission description>", cmd: "/mission" },
109
+ { name: "mesh_role", slashName: "role", title: "Set role", arg: "<role>", cmd: "/role" },
110
+ ]
111
+
112
+ const tui: TuiPlugin = async (api, options) => {
113
+ if (options?.enabled === false) return
114
+
115
+ // No keybindings — these are palette-only slash commands. Empty bindings avoids
116
+ // any dependency on the keymap-extras runtime module (which may not resolve for
117
+ // an externally-loaded plugin) so registration can never fail to load.
118
+ api.keymap.registerLayer({
119
+ commands: SPECS.map((s) => ({
120
+ name: s.name,
121
+ title: s.title,
122
+ category: "Mesh",
123
+ namespace: "palette" as const,
124
+ slashName: s.slashName,
125
+ run() {
126
+ const cmd = s.cmd ?? `/${s.slashName}`
127
+ if (s.arg) runWithArgs(api, s.title, cmd, s.arg)
128
+ else runNow(api, s.title, cmd)
129
+ },
130
+ })),
131
+ bindings: [],
132
+ })
133
+ }
134
+
135
+ const plugin: TuiPluginModule & { id: string } = {
136
+ id: "infinicode-mesh-commands",
137
+ tui,
138
+ }
139
+
140
+ export default plugin