@azatakmyradov/opencode-save-md-plugin 0.0.0 → 0.1.1

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/dist/core.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ import type { SessionMessage } from "@opencode-ai/schema/session-message";
2
+ import { Effect, Schema } from "effect";
3
+ declare const NoAssistantResponseError_base: Schema.Class<NoAssistantResponseError, Schema.TaggedStruct<"NoAssistantResponseError", {
4
+ readonly message: Schema.String;
5
+ }>, import("effect/Cause").YieldableError>;
6
+ export declare class NoAssistantResponseError extends NoAssistantResponseError_base {
7
+ }
8
+ declare const NoMarkdownTextError_base: Schema.Class<NoMarkdownTextError, Schema.TaggedStruct<"NoMarkdownTextError", {
9
+ readonly message: Schema.String;
10
+ }>, import("effect/Cause").YieldableError>;
11
+ export declare class NoMarkdownTextError extends NoMarkdownTextError_base {
12
+ }
13
+ declare const InvalidPathError_base: Schema.Class<InvalidPathError, Schema.TaggedStruct<"InvalidPathError", {
14
+ readonly name: Schema.String;
15
+ readonly message: Schema.String;
16
+ }>, import("effect/Cause").YieldableError>;
17
+ export declare class InvalidPathError extends InvalidPathError_base {
18
+ }
19
+ declare const DestinationExistsError_base: Schema.Class<DestinationExistsError, Schema.TaggedStruct<"DestinationExistsError", {
20
+ readonly path: Schema.String;
21
+ readonly message: Schema.String;
22
+ }>, import("effect/Cause").YieldableError>;
23
+ export declare class DestinationExistsError extends DestinationExistsError_base {
24
+ }
25
+ declare const FileSystemWriteError_base: Schema.Class<FileSystemWriteError, Schema.TaggedStruct<"FileSystemWriteError", {
26
+ readonly path: Schema.String;
27
+ readonly message: Schema.String;
28
+ readonly cause: Schema.Defect;
29
+ }>, import("effect/Cause").YieldableError>;
30
+ export declare class FileSystemWriteError extends FileSystemWriteError_base {
31
+ }
32
+ export type SaveMarkdownError = NoAssistantResponseError | NoMarkdownTextError | InvalidPathError | DestinationExistsError | FileSystemWriteError;
33
+ export declare const selectLatestAssistant: (messages: readonly SessionMessage.Info[]) => Effect.Effect<SessionMessage.Assistant, NoAssistantResponseError, never>;
34
+ export declare const extractMarkdown: (message: SessionMessage.Assistant) => Effect.Effect<string, NoMarkdownTextError, never>;
35
+ export declare const extractLatestMarkdown: (messages: readonly SessionMessage.Info[]) => Effect.Effect<string, NoAssistantResponseError | NoMarkdownTextError, never>;
36
+ export declare const resolveMarkdownPath: (directory: string, name: string) => Effect.Effect<string, InvalidPathError, never>;
37
+ export declare const saveMarkdown: (directory: string, name: string, markdown: string) => Effect.Effect<string, InvalidPathError | DestinationExistsError | FileSystemWriteError, never>;
38
+ export declare const saveLatestAssistant: (directory: string, name: string, messages: readonly SessionMessage.Info[]) => Effect.Effect<string, SaveMarkdownError, never>;
39
+ export {};
@@ -0,0 +1,3 @@
1
+ import { Plugin } from "@opencode-ai/plugin/effect";
2
+ declare const _default: Plugin.Plugin<import("effect/Scope").Scope>;
3
+ export default _default;
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { z } from "zod";
5
5
  var NoDetails = z.object({});
6
6
  var InvalidPathDetails = z.object({ name: z.string() });
7
7
  var DestinationDetails = z.object({ path: z.string() });
8
+ var SessionFailureDetails = z.object({ operation: z.enum(["wait", "context"]) });
8
9
  var SaveMdRpc = Rpc.define({
9
10
  id: "save-md",
10
11
  methods: {
@@ -19,7 +20,8 @@ var SaveMdRpc = Rpc.define({
19
20
  no_markdown_text: NoDetails,
20
21
  invalid_path: InvalidPathDetails,
21
22
  destination_exists: DestinationDetails,
22
- filesystem_write_failed: DestinationDetails
23
+ filesystem_write_failed: DestinationDetails,
24
+ session_failed: SessionFailureDetails
23
25
  }
24
26
  }
25
27
  },
@@ -38,16 +40,22 @@ var SaveMdRpcFailure = z.discriminatedUnion("type", [
38
40
  type: z.literal("filesystem_write_failed"),
39
41
  message: z.string(),
40
42
  data: DestinationDetails
43
+ }),
44
+ z.object({
45
+ type: z.literal("session_failed"),
46
+ message: z.string(),
47
+ data: SessionFailureDetails
41
48
  })
42
49
  ]);
50
+ var RpcFailure = z.object({ type: z.string(), message: z.string() });
43
51
 
44
52
  // src/index.ts
45
- import { Effect as Effect2 } from "effect";
53
+ import { Cause, Effect as Effect2, Schema as Schema2 } from "effect";
46
54
  import { Plugin } from "@opencode-ai/plugin/effect";
47
55
 
48
56
  // src/core.ts
49
- import { writeFile } from "fs/promises";
50
- import { isAbsolute, relative, resolve, sep, win32 } from "path";
57
+ import { realpath, writeFile } from "fs/promises";
58
+ import { dirname, isAbsolute, relative, resolve, sep, win32 } from "path";
51
59
  import { DateTime, Effect, Schema } from "effect";
52
60
 
53
61
  class NoAssistantResponseError extends Schema.TaggedError()("NoAssistantResponseError", { message: Schema.String }) {
@@ -76,6 +84,26 @@ class FileSystemWriteError extends Schema.TaggedError()("FileSystemWriteError",
76
84
  }
77
85
  var ExistingFileSystemError = Schema.Struct({ code: Schema.Literal("EEXIST") });
78
86
  var isExistingFileSystemError = Schema.is(ExistingFileSystemError);
87
+ function writeFailure(path, cause) {
88
+ return new FileSystemWriteError({
89
+ path,
90
+ cause,
91
+ message: `Could not write Markdown to ${path}: ${cause instanceof Error ? cause.message : String(cause)}`
92
+ });
93
+ }
94
+ var validatePhysicalContainment = Effect.fn("validatePhysicalContainment")(function* (directory, destination, name) {
95
+ const [root, parent] = yield* Effect.tryPromise({
96
+ try: () => Promise.all([realpath(directory), realpath(dirname(destination))]),
97
+ catch: (cause) => writeFailure(destination, cause)
98
+ });
99
+ const contained = relative(root, parent);
100
+ if (contained === ".." || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {
101
+ return yield* new InvalidPathError({
102
+ name,
103
+ message: "The destination path escapes the current location through a symbolic link."
104
+ });
105
+ }
106
+ });
79
107
  var selectLatestAssistant = Effect.fn("selectLatestAssistant")(function* (messages) {
80
108
  let latest;
81
109
  for (const message of messages) {
@@ -130,6 +158,7 @@ var resolveMarkdownPath = Effect.fn("resolveMarkdownPath")(function* (directory,
130
158
  });
131
159
  var saveMarkdown = Effect.fn("saveMarkdown")(function* (directory, name, markdown) {
132
160
  const destination = yield* resolveMarkdownPath(directory, name);
161
+ yield* validatePhysicalContainment(resolve(directory), destination, name);
133
162
  const content = markdown.endsWith(`
134
163
  `) ? markdown : `${markdown}
135
164
  `;
@@ -146,11 +175,7 @@ var saveMarkdown = Effect.fn("saveMarkdown")(function* (directory, name, markdow
146
175
  message: `Destination already exists: ${destination}`
147
176
  });
148
177
  }
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
- });
178
+ return writeFailure(destination, cause);
154
179
  }
155
180
  });
156
181
  return destination;
@@ -161,6 +186,24 @@ var saveLatestAssistant = Effect.fn("saveLatestAssistant")(function* (directory,
161
186
  });
162
187
 
163
188
  // src/index.ts
189
+ class SessionAccessError extends Schema2.TaggedError()("SessionAccessError", {
190
+ operation: Schema2.Literals(["wait", "context"]),
191
+ message: Schema2.String,
192
+ cause: Schema2.Defect()
193
+ }) {
194
+ }
195
+ function sessionOperation(operation, effect) {
196
+ return Effect2.catchCause(effect, (cause) => {
197
+ if (Cause.hasInterruptsOnly(cause))
198
+ return Effect2.interrupt;
199
+ const error = Cause.squash(cause);
200
+ return Effect2.fail(new SessionAccessError({
201
+ operation,
202
+ cause: error,
203
+ message: `Session ${operation} failed: ${error instanceof Error ? error.message : String(error)}`
204
+ }));
205
+ });
206
+ }
164
207
  var src_default = Plugin.define({
165
208
  id: "save-md",
166
209
  effect: (ctx) => Effect2.gen(function* () {
@@ -168,8 +211,8 @@ var src_default = Plugin.define({
168
211
  save: ({ sessionID, name }, rpc) => {
169
212
  const id = sessionID;
170
213
  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);
214
+ yield* sessionOperation("wait", ctx.session.wait({ sessionID: id }));
215
+ const messages = yield* sessionOperation("context", ctx.session.context({ sessionID: id }));
173
216
  const path = yield* saveLatestAssistant(ctx.location.directory, name, messages);
174
217
  return { path };
175
218
  }).pipe(Effect2.catchTags({
@@ -177,7 +220,8 @@ var src_default = Plugin.define({
177
220
  NoMarkdownTextError: (error) => Effect2.fail(rpc.error("no_markdown_text", error.message, {})),
178
221
  InvalidPathError: (error) => Effect2.fail(rpc.error("invalid_path", error.message, { name: error.name })),
179
222
  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 }))
223
+ FileSystemWriteError: (error) => Effect2.fail(rpc.error("filesystem_write_failed", error.message, { path: error.path })),
224
+ SessionAccessError: (error) => Effect2.fail(rpc.error("session_failed", error.message, { operation: error.operation }))
181
225
  }));
182
226
  }
183
227
  }).pipe(Effect2.orDie);
package/dist/rpc.d.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { z } from "zod";
2
+ export declare const SaveMdRpc: {
3
+ readonly id: "save-md";
4
+ readonly methods: {
5
+ readonly save: {
6
+ readonly input: z.ZodObject<{
7
+ sessionID: z.ZodString;
8
+ name: z.ZodString;
9
+ }, z.core.$strip>;
10
+ readonly output: z.ZodObject<{
11
+ path: z.ZodString;
12
+ }, z.core.$strip>;
13
+ readonly errors: {
14
+ readonly no_assistant_response: z.ZodObject<{}, z.core.$strip>;
15
+ readonly no_markdown_text: z.ZodObject<{}, z.core.$strip>;
16
+ readonly invalid_path: z.ZodObject<{
17
+ name: z.ZodString;
18
+ }, z.core.$strip>;
19
+ readonly destination_exists: z.ZodObject<{
20
+ path: z.ZodString;
21
+ }, z.core.$strip>;
22
+ readonly filesystem_write_failed: z.ZodObject<{
23
+ path: z.ZodString;
24
+ }, z.core.$strip>;
25
+ readonly session_failed: z.ZodObject<{
26
+ operation: z.ZodEnum<{
27
+ wait: "wait";
28
+ context: "context";
29
+ }>;
30
+ }, z.core.$strip>;
31
+ };
32
+ };
33
+ };
34
+ readonly events: {};
35
+ };
36
+ export declare const SaveMdRpcFailure: z.ZodDiscriminatedUnion<[z.ZodObject<{
37
+ type: z.ZodLiteral<"no_assistant_response">;
38
+ message: z.ZodString;
39
+ data: z.ZodObject<{}, z.core.$strip>;
40
+ }, z.core.$strip>, z.ZodObject<{
41
+ type: z.ZodLiteral<"no_markdown_text">;
42
+ message: z.ZodString;
43
+ data: z.ZodObject<{}, z.core.$strip>;
44
+ }, z.core.$strip>, z.ZodObject<{
45
+ type: z.ZodLiteral<"invalid_path">;
46
+ message: z.ZodString;
47
+ data: z.ZodObject<{
48
+ name: z.ZodString;
49
+ }, z.core.$strip>;
50
+ }, z.core.$strip>, z.ZodObject<{
51
+ type: z.ZodLiteral<"destination_exists">;
52
+ message: z.ZodString;
53
+ data: z.ZodObject<{
54
+ path: z.ZodString;
55
+ }, z.core.$strip>;
56
+ }, z.core.$strip>, z.ZodObject<{
57
+ type: z.ZodLiteral<"filesystem_write_failed">;
58
+ message: z.ZodString;
59
+ data: z.ZodObject<{
60
+ path: z.ZodString;
61
+ }, z.core.$strip>;
62
+ }, z.core.$strip>, z.ZodObject<{
63
+ type: z.ZodLiteral<"session_failed">;
64
+ message: z.ZodString;
65
+ data: z.ZodObject<{
66
+ operation: z.ZodEnum<{
67
+ wait: "wait";
68
+ context: "context";
69
+ }>;
70
+ }, z.core.$strip>;
71
+ }, z.core.$strip>], "type">;
72
+ export declare const RpcFailure: z.ZodObject<{
73
+ type: z.ZodString;
74
+ message: z.ZodString;
75
+ }, z.core.$strip>;
package/dist/rpc.js CHANGED
@@ -5,6 +5,7 @@ import { z } from "zod";
5
5
  var NoDetails = z.object({});
6
6
  var InvalidPathDetails = z.object({ name: z.string() });
7
7
  var DestinationDetails = z.object({ path: z.string() });
8
+ var SessionFailureDetails = z.object({ operation: z.enum(["wait", "context"]) });
8
9
  var SaveMdRpc = Rpc.define({
9
10
  id: "save-md",
10
11
  methods: {
@@ -19,7 +20,8 @@ var SaveMdRpc = Rpc.define({
19
20
  no_markdown_text: NoDetails,
20
21
  invalid_path: InvalidPathDetails,
21
22
  destination_exists: DestinationDetails,
22
- filesystem_write_failed: DestinationDetails
23
+ filesystem_write_failed: DestinationDetails,
24
+ session_failed: SessionFailureDetails
23
25
  }
24
26
  }
25
27
  },
@@ -38,9 +40,16 @@ var SaveMdRpcFailure = z.discriminatedUnion("type", [
38
40
  type: z.literal("filesystem_write_failed"),
39
41
  message: z.string(),
40
42
  data: DestinationDetails
43
+ }),
44
+ z.object({
45
+ type: z.literal("session_failed"),
46
+ message: z.string(),
47
+ data: SessionFailureDetails
41
48
  })
42
49
  ]);
50
+ var RpcFailure = z.object({ type: z.string(), message: z.string() });
43
51
  export {
52
+ RpcFailure,
44
53
  SaveMdRpc,
45
54
  SaveMdRpcFailure
46
55
  };
package/dist/tui.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { Plugin } from "@opencode-ai/plugin/tui";
2
+ declare const _default: Plugin.Definition;
3
+ export default _default;
package/dist/tui.js CHANGED
@@ -5,6 +5,7 @@ import { z } from "zod";
5
5
  var NoDetails = z.object({});
6
6
  var InvalidPathDetails = z.object({ name: z.string() });
7
7
  var DestinationDetails = z.object({ path: z.string() });
8
+ var SessionFailureDetails = z.object({ operation: z.enum(["wait", "context"]) });
8
9
  var SaveMdRpc = Rpc.define({
9
10
  id: "save-md",
10
11
  methods: {
@@ -19,7 +20,8 @@ var SaveMdRpc = Rpc.define({
19
20
  no_markdown_text: NoDetails,
20
21
  invalid_path: InvalidPathDetails,
21
22
  destination_exists: DestinationDetails,
22
- filesystem_write_failed: DestinationDetails
23
+ filesystem_write_failed: DestinationDetails,
24
+ session_failed: SessionFailureDetails
23
25
  }
24
26
  }
25
27
  },
@@ -38,8 +40,14 @@ var SaveMdRpcFailure = z.discriminatedUnion("type", [
38
40
  type: z.literal("filesystem_write_failed"),
39
41
  message: z.string(),
40
42
  data: DestinationDetails
43
+ }),
44
+ z.object({
45
+ type: z.literal("session_failed"),
46
+ message: z.string(),
47
+ data: SessionFailureDetails
41
48
  })
42
49
  ]);
50
+ var RpcFailure = z.object({ type: z.string(), message: z.string() });
43
51
 
44
52
  // src/tui.tsx
45
53
  import { Plugin } from "@opencode-ai/plugin/tui";
@@ -50,12 +58,14 @@ var tui_default = Plugin.define({
50
58
  setup(context) {
51
59
  const rpc = context.client.rpc(SaveMdRpc);
52
60
  const workflows = Semaphore.makeUnsafe(1);
61
+ const lifecycle = new AbortController;
53
62
  function showFailure(error) {
54
63
  const parsed = SaveMdRpcFailure.safeParse(error);
55
64
  if (!parsed.success) {
65
+ const fallback = RpcFailure.safeParse(error);
56
66
  context.ui.toast.show({
57
67
  title: TITLE,
58
- message: error instanceof Error ? error.message : String(error),
68
+ message: fallback.success ? fallback.data.message : error instanceof Error ? error.message : String(error),
59
69
  variant: "error"
60
70
  });
61
71
  return;
@@ -106,7 +116,7 @@ var tui_default = Plugin.define({
106
116
  name
107
117
  }, {
108
118
  location,
109
- signal
119
+ signal: AbortSignal.any([signal, lifecycle.signal])
110
120
  }),
111
121
  catch: (error) => error
112
122
  });
@@ -126,7 +136,7 @@ var tui_default = Plugin.define({
126
136
  variant: "warning"
127
137
  }));
128
138
  }), Effect.catchCause((cause) => {
129
- if (Cause.hasInterruptsOnly(cause))
139
+ if (lifecycle.signal.aborted || Cause.hasInterruptsOnly(cause))
130
140
  return Effect.void;
131
141
  return Effect.sync(() => showFailure(Cause.squash(cause)));
132
142
  })));
@@ -152,10 +162,14 @@ var tui_default = Plugin.define({
152
162
  }));
153
163
  return [];
154
164
  }
155
- return context.ui.slot({
165
+ const removeSlot = context.ui.slot({
156
166
  append: "app",
157
167
  render: AppExtensions
158
168
  });
169
+ return () => {
170
+ lifecycle.abort();
171
+ removeSlot();
172
+ };
159
173
  }
160
174
  });
161
175
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azatakmyradov/opencode-save-md-plugin",
3
- "version": "0.0.0",
3
+ "version": "0.1.1",
4
4
  "description": "Save the latest OpenCode assistant response as Markdown",
5
5
  "keywords": [
6
6
  "markdown",
@@ -22,15 +22,24 @@
22
22
  ],
23
23
  "type": "module",
24
24
  "exports": {
25
- ".": "./dist/index.js",
26
- "./tui": "./dist/tui.js",
27
- "./rpc": "./dist/rpc.js"
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js"
28
+ },
29
+ "./tui": {
30
+ "types": "./dist/tui.d.ts",
31
+ "import": "./dist/tui.js"
32
+ },
33
+ "./rpc": {
34
+ "types": "./dist/rpc.d.ts",
35
+ "import": "./dist/rpc.js"
36
+ }
28
37
  },
29
38
  "publishConfig": {
30
39
  "access": "public"
31
40
  },
32
41
  "scripts": {
33
- "build": "bun ../../scripts/build.ts .",
42
+ "build": "bun ../../scripts/build.ts . && tsc -p tsconfig.build.json",
34
43
  "check": "tsc --noEmit",
35
44
  "prepack": "bun run build",
36
45
  "test": "vp test run"