@azatakmyradov/opencode-save-md-plugin 0.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 +54 -0
- package/dist/index.js +188 -0
- package/dist/rpc.js +46 -0
- package/dist/tui.js +163 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# OpenCode save Markdown plugin
|
|
2
|
+
|
|
3
|
+
Save the latest assistant response from an OpenCode V2 session as a Markdown file in the server workspace.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
opencode2 plugin add @azatakmyradov/opencode-save-md-plugin
|
|
9
|
+
opencode2 service restart
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The package exposes server, TUI, and portable RPC entrypoints. OpenCode V2 loads the TUI entrypoint automatically when the package is installed.
|
|
13
|
+
|
|
14
|
+
## Use
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
/save-md design
|
|
18
|
+
/save-md notes/design.md
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The first command writes `design.md`; the second keeps the supplied `.md` suffix. The command waits for an active response to finish, then saves only the latest assistant message from the active context. Text parts are preserved and joined with a blank line; reasoning and tool parts are excluded.
|
|
22
|
+
|
|
23
|
+
Writes happen in the server process, so a remote TUI saves into the server workspace. Absolute paths and relative paths that escape the current location are rejected. Existing files are never overwritten.
|
|
24
|
+
|
|
25
|
+
## Local Development
|
|
26
|
+
|
|
27
|
+
Install the workspace and verify the package:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bun install
|
|
31
|
+
bun run --filter @azatakmyradov/opencode-save-md-plugin check
|
|
32
|
+
bun run --filter @azatakmyradov/opencode-save-md-plugin test
|
|
33
|
+
bun run --filter @azatakmyradov/opencode-save-md-plugin build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Load the package directory by absolute path in a project `opencode.jsonc`:
|
|
37
|
+
|
|
38
|
+
```jsonc
|
|
39
|
+
{
|
|
40
|
+
"$schema": "https://opencode.ai/config.json",
|
|
41
|
+
"plugins": ["/absolute/path/to/opencode-plugins/packages/save-md"],
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The package's root development entrypoints load `src/index.ts` and `src/tui.tsx`, so the server and matching TUI plugin are both discovered. Run the package build first only when testing the npm `dist` exports.
|
|
46
|
+
|
|
47
|
+
## Publish
|
|
48
|
+
|
|
49
|
+
Create a changeset for normal releases. To bootstrap npm trusted publishing for this package:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm publish --workspace @azatakmyradov/opencode-save-md-plugin
|
|
53
|
+
npm trust github @azatakmyradov/opencode-save-md-plugin --file release.yml --repo azatakmyradov/opencode-plugins --allow-publish
|
|
54
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/rpc.ts
|
|
3
|
+
import { Rpc } from "@opencode-ai/plugin/rpc";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
var NoDetails = z.object({});
|
|
6
|
+
var InvalidPathDetails = z.object({ name: z.string() });
|
|
7
|
+
var DestinationDetails = z.object({ path: z.string() });
|
|
8
|
+
var SaveMdRpc = Rpc.define({
|
|
9
|
+
id: "save-md",
|
|
10
|
+
methods: {
|
|
11
|
+
save: {
|
|
12
|
+
input: z.object({
|
|
13
|
+
sessionID: z.string().min(1),
|
|
14
|
+
name: z.string().min(1)
|
|
15
|
+
}),
|
|
16
|
+
output: z.object({ path: z.string() }),
|
|
17
|
+
errors: {
|
|
18
|
+
no_assistant_response: NoDetails,
|
|
19
|
+
no_markdown_text: NoDetails,
|
|
20
|
+
invalid_path: InvalidPathDetails,
|
|
21
|
+
destination_exists: DestinationDetails,
|
|
22
|
+
filesystem_write_failed: DestinationDetails
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
events: {}
|
|
27
|
+
});
|
|
28
|
+
var SaveMdRpcFailure = z.discriminatedUnion("type", [
|
|
29
|
+
z.object({ type: z.literal("no_assistant_response"), message: z.string(), data: NoDetails }),
|
|
30
|
+
z.object({ type: z.literal("no_markdown_text"), message: z.string(), data: NoDetails }),
|
|
31
|
+
z.object({ type: z.literal("invalid_path"), message: z.string(), data: InvalidPathDetails }),
|
|
32
|
+
z.object({
|
|
33
|
+
type: z.literal("destination_exists"),
|
|
34
|
+
message: z.string(),
|
|
35
|
+
data: DestinationDetails
|
|
36
|
+
}),
|
|
37
|
+
z.object({
|
|
38
|
+
type: z.literal("filesystem_write_failed"),
|
|
39
|
+
message: z.string(),
|
|
40
|
+
data: DestinationDetails
|
|
41
|
+
})
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
// src/index.ts
|
|
45
|
+
import { Effect as Effect2 } from "effect";
|
|
46
|
+
import { Plugin } from "@opencode-ai/plugin/effect";
|
|
47
|
+
|
|
48
|
+
// src/core.ts
|
|
49
|
+
import { writeFile } from "fs/promises";
|
|
50
|
+
import { isAbsolute, relative, resolve, sep, win32 } from "path";
|
|
51
|
+
import { DateTime, Effect, Schema } from "effect";
|
|
52
|
+
|
|
53
|
+
class NoAssistantResponseError extends Schema.TaggedError()("NoAssistantResponseError", { message: Schema.String }) {
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class NoMarkdownTextError extends Schema.TaggedError()("NoMarkdownTextError", { message: Schema.String }) {
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class InvalidPathError extends Schema.TaggedError()("InvalidPathError", {
|
|
60
|
+
name: Schema.String,
|
|
61
|
+
message: Schema.String
|
|
62
|
+
}) {
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class DestinationExistsError extends Schema.TaggedError()("DestinationExistsError", {
|
|
66
|
+
path: Schema.String,
|
|
67
|
+
message: Schema.String
|
|
68
|
+
}) {
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class FileSystemWriteError extends Schema.TaggedError()("FileSystemWriteError", {
|
|
72
|
+
path: Schema.String,
|
|
73
|
+
message: Schema.String,
|
|
74
|
+
cause: Schema.Defect()
|
|
75
|
+
}) {
|
|
76
|
+
}
|
|
77
|
+
var ExistingFileSystemError = Schema.Struct({ code: Schema.Literal("EEXIST") });
|
|
78
|
+
var isExistingFileSystemError = Schema.is(ExistingFileSystemError);
|
|
79
|
+
var selectLatestAssistant = Effect.fn("selectLatestAssistant")(function* (messages) {
|
|
80
|
+
let latest;
|
|
81
|
+
for (const message of messages) {
|
|
82
|
+
if (message.type !== "assistant")
|
|
83
|
+
continue;
|
|
84
|
+
if (!latest || DateTime.toEpochMillis(message.time.created) >= DateTime.toEpochMillis(latest.time.created)) {
|
|
85
|
+
latest = message;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!latest) {
|
|
89
|
+
return yield* new NoAssistantResponseError({
|
|
90
|
+
message: "No assistant response is available in the active session context."
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return latest;
|
|
94
|
+
});
|
|
95
|
+
var extractMarkdown = Effect.fn("extractMarkdown")(function* (message) {
|
|
96
|
+
const markdown = message.content.flatMap((part) => part.type === "text" ? [part.text] : []).join(`
|
|
97
|
+
|
|
98
|
+
`);
|
|
99
|
+
if (!markdown.trim()) {
|
|
100
|
+
return yield* new NoMarkdownTextError({
|
|
101
|
+
message: "The latest assistant response contains no Markdown text."
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return markdown;
|
|
105
|
+
});
|
|
106
|
+
var extractLatestMarkdown = Effect.fn("extractLatestMarkdown")(function* (messages) {
|
|
107
|
+
const assistant = yield* selectLatestAssistant(messages);
|
|
108
|
+
return yield* extractMarkdown(assistant);
|
|
109
|
+
});
|
|
110
|
+
var resolveMarkdownPath = Effect.fn("resolveMarkdownPath")(function* (directory, name) {
|
|
111
|
+
const requested = name.trim();
|
|
112
|
+
const invalid = requested.length === 0 || requested.includes("\x00") || isAbsolute(requested) || win32.isAbsolute(requested);
|
|
113
|
+
if (invalid) {
|
|
114
|
+
return yield* new InvalidPathError({
|
|
115
|
+
name,
|
|
116
|
+
message: "The destination must be a non-empty relative path inside the current location."
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const root = resolve(directory);
|
|
120
|
+
const filename = requested.endsWith(".md") ? requested : `${requested}.md`;
|
|
121
|
+
const destination = resolve(root, filename);
|
|
122
|
+
const contained = relative(root, destination);
|
|
123
|
+
if (contained.length === 0 || contained === ".." || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {
|
|
124
|
+
return yield* new InvalidPathError({
|
|
125
|
+
name,
|
|
126
|
+
message: "The destination path escapes the current location."
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return destination;
|
|
130
|
+
});
|
|
131
|
+
var saveMarkdown = Effect.fn("saveMarkdown")(function* (directory, name, markdown) {
|
|
132
|
+
const destination = yield* resolveMarkdownPath(directory, name);
|
|
133
|
+
const content = markdown.endsWith(`
|
|
134
|
+
`) ? markdown : `${markdown}
|
|
135
|
+
`;
|
|
136
|
+
yield* Effect.tryPromise({
|
|
137
|
+
try: (signal) => writeFile(destination, content, {
|
|
138
|
+
encoding: "utf8",
|
|
139
|
+
flag: "wx",
|
|
140
|
+
signal
|
|
141
|
+
}),
|
|
142
|
+
catch: (cause) => {
|
|
143
|
+
if (isExistingFileSystemError(cause)) {
|
|
144
|
+
return new DestinationExistsError({
|
|
145
|
+
path: destination,
|
|
146
|
+
message: `Destination already exists: ${destination}`
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return new FileSystemWriteError({
|
|
150
|
+
path: destination,
|
|
151
|
+
cause,
|
|
152
|
+
message: `Could not write Markdown to ${destination}: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
return destination;
|
|
157
|
+
});
|
|
158
|
+
var saveLatestAssistant = Effect.fn("saveLatestAssistant")(function* (directory, name, messages) {
|
|
159
|
+
const markdown = yield* extractLatestMarkdown(messages);
|
|
160
|
+
return yield* saveMarkdown(directory, name, markdown);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// src/index.ts
|
|
164
|
+
var src_default = Plugin.define({
|
|
165
|
+
id: "save-md",
|
|
166
|
+
effect: (ctx) => Effect2.gen(function* () {
|
|
167
|
+
yield* ctx.rpc.register(SaveMdRpc, {
|
|
168
|
+
save: ({ sessionID, name }, rpc) => {
|
|
169
|
+
const id = sessionID;
|
|
170
|
+
return Effect2.gen(function* () {
|
|
171
|
+
yield* ctx.session.wait({ sessionID: id }).pipe(Effect2.orDie);
|
|
172
|
+
const messages = yield* ctx.session.context({ sessionID: id }).pipe(Effect2.orDie);
|
|
173
|
+
const path = yield* saveLatestAssistant(ctx.location.directory, name, messages);
|
|
174
|
+
return { path };
|
|
175
|
+
}).pipe(Effect2.catchTags({
|
|
176
|
+
NoAssistantResponseError: (error) => Effect2.fail(rpc.error("no_assistant_response", error.message, {})),
|
|
177
|
+
NoMarkdownTextError: (error) => Effect2.fail(rpc.error("no_markdown_text", error.message, {})),
|
|
178
|
+
InvalidPathError: (error) => Effect2.fail(rpc.error("invalid_path", error.message, { name: error.name })),
|
|
179
|
+
DestinationExistsError: (error) => Effect2.fail(rpc.error("destination_exists", error.message, { path: error.path })),
|
|
180
|
+
FileSystemWriteError: (error) => Effect2.fail(rpc.error("filesystem_write_failed", error.message, { path: error.path }))
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
}).pipe(Effect2.orDie);
|
|
184
|
+
})
|
|
185
|
+
});
|
|
186
|
+
export {
|
|
187
|
+
src_default as default
|
|
188
|
+
};
|
package/dist/rpc.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/rpc.ts
|
|
3
|
+
import { Rpc } from "@opencode-ai/plugin/rpc";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
var NoDetails = z.object({});
|
|
6
|
+
var InvalidPathDetails = z.object({ name: z.string() });
|
|
7
|
+
var DestinationDetails = z.object({ path: z.string() });
|
|
8
|
+
var SaveMdRpc = Rpc.define({
|
|
9
|
+
id: "save-md",
|
|
10
|
+
methods: {
|
|
11
|
+
save: {
|
|
12
|
+
input: z.object({
|
|
13
|
+
sessionID: z.string().min(1),
|
|
14
|
+
name: z.string().min(1)
|
|
15
|
+
}),
|
|
16
|
+
output: z.object({ path: z.string() }),
|
|
17
|
+
errors: {
|
|
18
|
+
no_assistant_response: NoDetails,
|
|
19
|
+
no_markdown_text: NoDetails,
|
|
20
|
+
invalid_path: InvalidPathDetails,
|
|
21
|
+
destination_exists: DestinationDetails,
|
|
22
|
+
filesystem_write_failed: DestinationDetails
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
events: {}
|
|
27
|
+
});
|
|
28
|
+
var SaveMdRpcFailure = z.discriminatedUnion("type", [
|
|
29
|
+
z.object({ type: z.literal("no_assistant_response"), message: z.string(), data: NoDetails }),
|
|
30
|
+
z.object({ type: z.literal("no_markdown_text"), message: z.string(), data: NoDetails }),
|
|
31
|
+
z.object({ type: z.literal("invalid_path"), message: z.string(), data: InvalidPathDetails }),
|
|
32
|
+
z.object({
|
|
33
|
+
type: z.literal("destination_exists"),
|
|
34
|
+
message: z.string(),
|
|
35
|
+
data: DestinationDetails
|
|
36
|
+
}),
|
|
37
|
+
z.object({
|
|
38
|
+
type: z.literal("filesystem_write_failed"),
|
|
39
|
+
message: z.string(),
|
|
40
|
+
data: DestinationDetails
|
|
41
|
+
})
|
|
42
|
+
]);
|
|
43
|
+
export {
|
|
44
|
+
SaveMdRpc,
|
|
45
|
+
SaveMdRpcFailure
|
|
46
|
+
};
|
package/dist/tui.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/rpc.ts
|
|
3
|
+
import { Rpc } from "@opencode-ai/plugin/rpc";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
var NoDetails = z.object({});
|
|
6
|
+
var InvalidPathDetails = z.object({ name: z.string() });
|
|
7
|
+
var DestinationDetails = z.object({ path: z.string() });
|
|
8
|
+
var SaveMdRpc = Rpc.define({
|
|
9
|
+
id: "save-md",
|
|
10
|
+
methods: {
|
|
11
|
+
save: {
|
|
12
|
+
input: z.object({
|
|
13
|
+
sessionID: z.string().min(1),
|
|
14
|
+
name: z.string().min(1)
|
|
15
|
+
}),
|
|
16
|
+
output: z.object({ path: z.string() }),
|
|
17
|
+
errors: {
|
|
18
|
+
no_assistant_response: NoDetails,
|
|
19
|
+
no_markdown_text: NoDetails,
|
|
20
|
+
invalid_path: InvalidPathDetails,
|
|
21
|
+
destination_exists: DestinationDetails,
|
|
22
|
+
filesystem_write_failed: DestinationDetails
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
events: {}
|
|
27
|
+
});
|
|
28
|
+
var SaveMdRpcFailure = z.discriminatedUnion("type", [
|
|
29
|
+
z.object({ type: z.literal("no_assistant_response"), message: z.string(), data: NoDetails }),
|
|
30
|
+
z.object({ type: z.literal("no_markdown_text"), message: z.string(), data: NoDetails }),
|
|
31
|
+
z.object({ type: z.literal("invalid_path"), message: z.string(), data: InvalidPathDetails }),
|
|
32
|
+
z.object({
|
|
33
|
+
type: z.literal("destination_exists"),
|
|
34
|
+
message: z.string(),
|
|
35
|
+
data: DestinationDetails
|
|
36
|
+
}),
|
|
37
|
+
z.object({
|
|
38
|
+
type: z.literal("filesystem_write_failed"),
|
|
39
|
+
message: z.string(),
|
|
40
|
+
data: DestinationDetails
|
|
41
|
+
})
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
// src/tui.tsx
|
|
45
|
+
import { Plugin } from "@opencode-ai/plugin/tui";
|
|
46
|
+
import { Cause, Effect, Option, Semaphore } from "effect";
|
|
47
|
+
var TITLE = "Save Markdown";
|
|
48
|
+
var tui_default = Plugin.define({
|
|
49
|
+
id: "save-md",
|
|
50
|
+
setup(context) {
|
|
51
|
+
const rpc = context.client.rpc(SaveMdRpc);
|
|
52
|
+
const workflows = Semaphore.makeUnsafe(1);
|
|
53
|
+
function showFailure(error) {
|
|
54
|
+
const parsed = SaveMdRpcFailure.safeParse(error);
|
|
55
|
+
if (!parsed.success) {
|
|
56
|
+
context.ui.toast.show({
|
|
57
|
+
title: TITLE,
|
|
58
|
+
message: error instanceof Error ? error.message : String(error),
|
|
59
|
+
variant: "error"
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const failure = parsed.data;
|
|
64
|
+
const warning = failure.type === "no_assistant_response" || failure.type === "no_markdown_text" || failure.type === "invalid_path" || failure.type === "destination_exists";
|
|
65
|
+
context.ui.toast.show({
|
|
66
|
+
title: TITLE,
|
|
67
|
+
message: failure.message,
|
|
68
|
+
variant: warning ? "warning" : "error"
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const runWorkflow = Effect.fn("SaveMd.runWorkflow")(function* (input) {
|
|
72
|
+
const route = yield* Effect.sync(() => context.ui.router.current());
|
|
73
|
+
if (route.type !== "session") {
|
|
74
|
+
yield* Effect.sync(() => context.ui.toast.show({
|
|
75
|
+
title: TITLE,
|
|
76
|
+
message: "Open a session before saving an assistant response.",
|
|
77
|
+
variant: "warning"
|
|
78
|
+
}));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const name = input?.trim();
|
|
82
|
+
if (!name) {
|
|
83
|
+
yield* Effect.sync(() => context.ui.toast.show({
|
|
84
|
+
title: TITLE,
|
|
85
|
+
message: "Provide a destination name, for example: /save-md design",
|
|
86
|
+
variant: "warning"
|
|
87
|
+
}));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const session = yield* Effect.sync(() => context.data.session.get(route.sessionID));
|
|
91
|
+
if (!session) {
|
|
92
|
+
yield* Effect.sync(() => context.ui.toast.show({
|
|
93
|
+
title: TITLE,
|
|
94
|
+
message: "The active session is not available yet.",
|
|
95
|
+
variant: "warning"
|
|
96
|
+
}));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const location = {
|
|
100
|
+
directory: session.location.directory,
|
|
101
|
+
workspace: session.location.workspaceID
|
|
102
|
+
};
|
|
103
|
+
const result = yield* Effect.tryPromise({
|
|
104
|
+
try: (signal) => rpc.save({
|
|
105
|
+
sessionID: route.sessionID,
|
|
106
|
+
name
|
|
107
|
+
}, {
|
|
108
|
+
location,
|
|
109
|
+
signal
|
|
110
|
+
}),
|
|
111
|
+
catch: (error) => error
|
|
112
|
+
});
|
|
113
|
+
yield* Effect.sync(() => context.ui.toast.show({
|
|
114
|
+
title: TITLE,
|
|
115
|
+
message: `Saved ${context.ui.format.path(result.path)}`,
|
|
116
|
+
variant: "success"
|
|
117
|
+
}));
|
|
118
|
+
});
|
|
119
|
+
function run(input) {
|
|
120
|
+
return Effect.runPromise(workflows.withPermitsIfAvailable(1)(runWorkflow(input)).pipe(Effect.flatMap((result) => {
|
|
121
|
+
if (Option.isSome(result))
|
|
122
|
+
return Effect.void;
|
|
123
|
+
return Effect.sync(() => context.ui.toast.show({
|
|
124
|
+
title: TITLE,
|
|
125
|
+
message: "A save workflow is already running.",
|
|
126
|
+
variant: "warning"
|
|
127
|
+
}));
|
|
128
|
+
}), Effect.catchCause((cause) => {
|
|
129
|
+
if (Cause.hasInterruptsOnly(cause))
|
|
130
|
+
return Effect.void;
|
|
131
|
+
return Effect.sync(() => showFailure(Cause.squash(cause)));
|
|
132
|
+
})));
|
|
133
|
+
}
|
|
134
|
+
function AppExtensions() {
|
|
135
|
+
context.keymap.layer(() => ({
|
|
136
|
+
mode: "global",
|
|
137
|
+
priority: 10,
|
|
138
|
+
commands: [{
|
|
139
|
+
id: "save-md.save",
|
|
140
|
+
title: "Save assistant response as Markdown",
|
|
141
|
+
group: "Markdown",
|
|
142
|
+
description: "Save the latest assistant response in the server workspace.",
|
|
143
|
+
palette: true,
|
|
144
|
+
suggested: true,
|
|
145
|
+
bind: false,
|
|
146
|
+
slash: {
|
|
147
|
+
name: "save-md",
|
|
148
|
+
arguments: true
|
|
149
|
+
},
|
|
150
|
+
run
|
|
151
|
+
}]
|
|
152
|
+
}));
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
return context.ui.slot({
|
|
156
|
+
append: "app",
|
|
157
|
+
render: AppExtensions
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
export {
|
|
162
|
+
tui_default as default
|
|
163
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@azatakmyradov/opencode-save-md-plugin",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Save the latest OpenCode assistant response as Markdown",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"markdown",
|
|
7
|
+
"opencode",
|
|
8
|
+
"opencode-plugin",
|
|
9
|
+
"tui"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/azatakmyradov/opencode-plugins/tree/main/packages/save-md",
|
|
12
|
+
"bugs": "https://github.com/azatakmyradov/opencode-plugins/issues",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/azatakmyradov/opencode-plugins.git",
|
|
17
|
+
"directory": "packages/save-md"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./dist/index.js",
|
|
26
|
+
"./tui": "./dist/tui.js",
|
|
27
|
+
"./rpc": "./dist/rpc.js"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "bun ../../scripts/build.ts .",
|
|
34
|
+
"check": "tsc --noEmit",
|
|
35
|
+
"prepack": "bun run build",
|
|
36
|
+
"test": "vp test run"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@opencode-ai/client": "0.0.0-beta-18721",
|
|
40
|
+
"@opencode-ai/plugin": "0.0.0-beta-18721",
|
|
41
|
+
"@opencode-ai/schema": "0.0.0-beta-18721",
|
|
42
|
+
"effect": "4.0.0-rc.112",
|
|
43
|
+
"zod": "4.1.8"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@opentui/core": ">=0.5.9",
|
|
47
|
+
"@opentui/solid": ">=0.5.9",
|
|
48
|
+
"solid-js": ">=1.9.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"@opentui/core": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"@opentui/solid": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"solid-js": {
|
|
58
|
+
"optional": true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|