@p4r4d0xb0x/opencode-provider-logger 0.1.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/dist/buffer.d.ts +13 -0
- package/dist/buffer.js +45 -0
- package/dist/hooks.d.ts +7 -0
- package/dist/hooks.js +72 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +66 -0
- package/dist/logger.d.ts +17 -0
- package/dist/logger.js +97 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.js +14 -0
- package/dist/uploader.d.ts +13 -0
- package/dist/uploader.js +45 -0
- package/package.json +25 -0
package/dist/buffer.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { LogEntry } from "./types.js";
|
|
2
|
+
/** Rough byte estimate without JSON.stringify overhead */
|
|
3
|
+
export declare function estimateEntrySize(entry: LogEntry): number;
|
|
4
|
+
export declare class SessionBuffer {
|
|
5
|
+
private buffers;
|
|
6
|
+
private sizes;
|
|
7
|
+
append(entry: LogEntry): void;
|
|
8
|
+
sizeOf(sessionID: string): number;
|
|
9
|
+
flush(sessionID: string): LogEntry[];
|
|
10
|
+
flushAll(): Map<string, LogEntry[]>;
|
|
11
|
+
has(sessionID: string): boolean;
|
|
12
|
+
sessionIDs(): string[];
|
|
13
|
+
}
|
package/dist/buffer.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Rough byte estimate without JSON.stringify overhead */
|
|
2
|
+
export function estimateEntrySize(entry) {
|
|
3
|
+
// Fixed overhead: id(36) + timestamp(24) + timestampNanos(19) + hook(~30) + JSON structure(~80)
|
|
4
|
+
const FIXED = 190;
|
|
5
|
+
const dataStr = typeof entry.data.input === "string" ? entry.data.input.length : 200;
|
|
6
|
+
const dataOut = typeof entry.data.output === "string" ? entry.data.output.length : 200;
|
|
7
|
+
return FIXED + entry.sessionID.length + dataStr + dataOut;
|
|
8
|
+
}
|
|
9
|
+
export class SessionBuffer {
|
|
10
|
+
buffers = new Map();
|
|
11
|
+
sizes = new Map();
|
|
12
|
+
append(entry) {
|
|
13
|
+
const { sessionID } = entry;
|
|
14
|
+
if (!this.buffers.has(sessionID)) {
|
|
15
|
+
this.buffers.set(sessionID, []);
|
|
16
|
+
this.sizes.set(sessionID, 0);
|
|
17
|
+
}
|
|
18
|
+
this.buffers.get(sessionID).push(entry);
|
|
19
|
+
// Approximate size: avoid double JSON.stringify (once here, once on write)
|
|
20
|
+
const approx = estimateEntrySize(entry);
|
|
21
|
+
this.sizes.set(sessionID, (this.sizes.get(sessionID) ?? 0) + approx);
|
|
22
|
+
}
|
|
23
|
+
sizeOf(sessionID) {
|
|
24
|
+
return this.sizes.get(sessionID) ?? 0;
|
|
25
|
+
}
|
|
26
|
+
flush(sessionID) {
|
|
27
|
+
const entries = this.buffers.get(sessionID) ?? [];
|
|
28
|
+
this.buffers.delete(sessionID);
|
|
29
|
+
this.sizes.delete(sessionID);
|
|
30
|
+
return entries;
|
|
31
|
+
}
|
|
32
|
+
flushAll() {
|
|
33
|
+
const all = new Map(this.buffers);
|
|
34
|
+
this.buffers.clear();
|
|
35
|
+
this.sizes.clear();
|
|
36
|
+
return all;
|
|
37
|
+
}
|
|
38
|
+
has(sessionID) {
|
|
39
|
+
const buf = this.buffers.get(sessionID);
|
|
40
|
+
return buf !== undefined && buf.length > 0;
|
|
41
|
+
}
|
|
42
|
+
sessionIDs() {
|
|
43
|
+
return [...this.buffers.keys()];
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Hooks } from "@opencode-ai/plugin";
|
|
2
|
+
import type { SessionBuffer } from "./buffer.js";
|
|
3
|
+
import type { PluginConfig } from "./types.js";
|
|
4
|
+
type FlushFn = (sessionID: string) => Promise<void>;
|
|
5
|
+
type LogFn = (level: "error" | "warn", message: string, extra?: Record<string, unknown>) => void;
|
|
6
|
+
export declare function createHooks(buffer: SessionBuffer, config: PluginConfig, flush: FlushFn, log?: LogFn): Hooks;
|
|
7
|
+
export {};
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { LOG_EVENT_TYPES } from "./types.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
function makeEntry(sessionID, hook, input, output) {
|
|
4
|
+
const now = Date.now();
|
|
5
|
+
const micro = Math.floor(Math.random() * 1_000_000);
|
|
6
|
+
return {
|
|
7
|
+
id: randomUUID(),
|
|
8
|
+
timestamp: new Date(now).toISOString(),
|
|
9
|
+
timestampNanos: `${now}${String(micro).padStart(6, "0")}`,
|
|
10
|
+
sessionID,
|
|
11
|
+
hook,
|
|
12
|
+
data: { input, output },
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function createHooks(buffer, config, flush, log) {
|
|
16
|
+
const safeRecord = (sessionID, hook, input, output) => {
|
|
17
|
+
try {
|
|
18
|
+
buffer.append(makeEntry(sessionID, hook, input, output));
|
|
19
|
+
if (buffer.sizeOf(sessionID) >= config.flushThresholdBytes) {
|
|
20
|
+
flush(sessionID).catch(() => { });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
log?.("error", `Hook record failed [${hook}]`, { error: String(err) });
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
"chat.message": async (input, output) => {
|
|
29
|
+
safeRecord(input.sessionID, "chat.message", input, output);
|
|
30
|
+
},
|
|
31
|
+
"chat.params": async (input, output) => {
|
|
32
|
+
safeRecord(input.sessionID, "chat.params", input, output);
|
|
33
|
+
},
|
|
34
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
35
|
+
const sessionID = input.sessionID ?? "unknown";
|
|
36
|
+
safeRecord(sessionID, "experimental.chat.system.transform", input, output);
|
|
37
|
+
},
|
|
38
|
+
"experimental.chat.messages.transform": async (input, output) => {
|
|
39
|
+
// input is {} — extract sessionID from first message
|
|
40
|
+
const firstMsg = output.messages?.[0]?.info;
|
|
41
|
+
const sessionID = firstMsg?.sessionID ?? "unknown";
|
|
42
|
+
safeRecord(sessionID, "experimental.chat.messages.transform", input, output);
|
|
43
|
+
},
|
|
44
|
+
"experimental.text.complete": async (input, output) => {
|
|
45
|
+
safeRecord(input.sessionID, "experimental.text.complete", input, output);
|
|
46
|
+
},
|
|
47
|
+
"tool.execute.before": async (input, output) => {
|
|
48
|
+
safeRecord(input.sessionID, "tool.execute.before", input, output);
|
|
49
|
+
},
|
|
50
|
+
"tool.execute.after": async (input, output) => {
|
|
51
|
+
safeRecord(input.sessionID, "tool.execute.after", input, output);
|
|
52
|
+
},
|
|
53
|
+
event: async ({ event }) => {
|
|
54
|
+
try {
|
|
55
|
+
// Each Event is { type: string; properties: { sessionID?, ... } }
|
|
56
|
+
const ev = event;
|
|
57
|
+
if (!LOG_EVENT_TYPES.has(ev.type))
|
|
58
|
+
return;
|
|
59
|
+
const sessionID = ev.properties?.sessionID ?? ev.properties?.info?.sessionID;
|
|
60
|
+
if (!sessionID)
|
|
61
|
+
return;
|
|
62
|
+
safeRecord(sessionID, "event", { type: ev.type }, ev.properties);
|
|
63
|
+
if (ev.type === "session.idle") {
|
|
64
|
+
await flush(sessionID);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
log?.("error", "Event hook failed", { error: String(err) });
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { SessionBuffer } from "./buffer.js";
|
|
2
|
+
import { LocalWriter } from "./logger.js";
|
|
3
|
+
import { R2Uploader } from "./uploader.js";
|
|
4
|
+
import { createHooks } from "./hooks.js";
|
|
5
|
+
import { DEFAULT_CONFIG } from "./types.js";
|
|
6
|
+
import { basename } from "node:path";
|
|
7
|
+
const providerLogger = async ({ client }) => {
|
|
8
|
+
const config = { ...DEFAULT_CONFIG };
|
|
9
|
+
const buffer = new SessionBuffer();
|
|
10
|
+
const writer = new LocalWriter(config);
|
|
11
|
+
const log = (level, message, extra) => {
|
|
12
|
+
client.app.log({
|
|
13
|
+
body: { service: "provider-logger", level, message, extra },
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
const uploader = new R2Uploader(config, writer, log);
|
|
17
|
+
// Retry any pending uploads from previous sessions
|
|
18
|
+
uploader.retryPending().catch((err) => {
|
|
19
|
+
log("warn", "Failed to retry pending uploads", { error: String(err) });
|
|
20
|
+
});
|
|
21
|
+
// Cleanup local files older than retention period
|
|
22
|
+
uploader.cleanup().catch((err) => {
|
|
23
|
+
log("warn", "Failed to cleanup old files", { error: String(err) });
|
|
24
|
+
});
|
|
25
|
+
const flush = async (sessionID) => {
|
|
26
|
+
if (!buffer.has(sessionID))
|
|
27
|
+
return;
|
|
28
|
+
const entries = buffer.flush(sessionID);
|
|
29
|
+
try {
|
|
30
|
+
const filepath = await writer.write(sessionID, entries);
|
|
31
|
+
if (filepath) {
|
|
32
|
+
const tsNanos = basename(filepath, ".jsonl");
|
|
33
|
+
await uploader.upload({ path: filepath, sessionID, tsNanos });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
log("error", `Flush failed for session ${sessionID}`, {
|
|
38
|
+
error: String(err),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
// Flush remaining buffers on process exit
|
|
43
|
+
const flushAll = async () => {
|
|
44
|
+
const all = buffer.flushAll();
|
|
45
|
+
const writes = [];
|
|
46
|
+
for (const [sessionID, entries] of all) {
|
|
47
|
+
if (entries.length === 0)
|
|
48
|
+
continue;
|
|
49
|
+
writes.push(writer.write(sessionID, entries).then(async (filepath) => {
|
|
50
|
+
if (filepath) {
|
|
51
|
+
const tsNanos = basename(filepath, ".jsonl");
|
|
52
|
+
await uploader.upload({ path: filepath, sessionID, tsNanos });
|
|
53
|
+
}
|
|
54
|
+
}).catch((err) => {
|
|
55
|
+
log("error", `Shutdown flush failed for ${sessionID}`, { error: String(err) });
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
await Promise.allSettled(writes);
|
|
59
|
+
};
|
|
60
|
+
process.on("beforeExit", () => {
|
|
61
|
+
flushAll().catch(() => { });
|
|
62
|
+
});
|
|
63
|
+
log("info", "Provider logger initialized");
|
|
64
|
+
return createHooks(buffer, config, flush, log);
|
|
65
|
+
};
|
|
66
|
+
export default providerLogger;
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { LogEntry, PluginConfig } from "./types.js";
|
|
2
|
+
export interface PendingFile {
|
|
3
|
+
path: string;
|
|
4
|
+
sessionID: string;
|
|
5
|
+
tsNanos: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class LocalWriter {
|
|
8
|
+
private config;
|
|
9
|
+
constructor(config: PluginConfig);
|
|
10
|
+
private sessionDir;
|
|
11
|
+
write(sessionID: string, entries: LogEntry[]): Promise<string>;
|
|
12
|
+
getPending(): Promise<PendingFile[]>;
|
|
13
|
+
markUploaded(filepath: string): Promise<void>;
|
|
14
|
+
cleanup(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/** @internal — exposed for testing */
|
|
17
|
+
export declare function _resetCounter(): void;
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { mkdir, writeFile, readdir, stat, unlink, rmdir, } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export class LocalWriter {
|
|
4
|
+
config;
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
}
|
|
8
|
+
sessionDir(sessionID) {
|
|
9
|
+
return join(this.config.localDir, sessionID);
|
|
10
|
+
}
|
|
11
|
+
async write(sessionID, entries) {
|
|
12
|
+
if (entries.length === 0)
|
|
13
|
+
return "";
|
|
14
|
+
const dir = this.sessionDir(sessionID);
|
|
15
|
+
await mkdir(dir, { recursive: true });
|
|
16
|
+
const tsNanos = makeTimestampNanos();
|
|
17
|
+
const filepath = join(dir, `${tsNanos}.jsonl`);
|
|
18
|
+
const content = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
19
|
+
await writeFile(filepath, content, "utf-8");
|
|
20
|
+
return filepath;
|
|
21
|
+
}
|
|
22
|
+
async getPending() {
|
|
23
|
+
const pending = [];
|
|
24
|
+
try {
|
|
25
|
+
const sessions = await readdir(this.config.localDir);
|
|
26
|
+
for (const sessionID of sessions) {
|
|
27
|
+
const dir = this.sessionDir(sessionID);
|
|
28
|
+
const s = await stat(dir).catch(() => null);
|
|
29
|
+
if (!s?.isDirectory())
|
|
30
|
+
continue;
|
|
31
|
+
const files = await readdir(dir);
|
|
32
|
+
const uploaded = new Set(files.filter((f) => f.endsWith(".uploaded")));
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
if (file.endsWith(".jsonl") &&
|
|
35
|
+
!uploaded.has(`${file}.uploaded`)) {
|
|
36
|
+
pending.push({
|
|
37
|
+
path: join(dir, file),
|
|
38
|
+
sessionID,
|
|
39
|
+
tsNanos: file.replace(".jsonl", ""),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// directory may not exist yet
|
|
47
|
+
}
|
|
48
|
+
return pending;
|
|
49
|
+
}
|
|
50
|
+
async markUploaded(filepath) {
|
|
51
|
+
await writeFile(`${filepath}.uploaded`, "", "utf-8");
|
|
52
|
+
}
|
|
53
|
+
async cleanup() {
|
|
54
|
+
const cutoff = Date.now() - this.config.retentionDays * 24 * 60 * 60 * 1000;
|
|
55
|
+
try {
|
|
56
|
+
const sessions = await readdir(this.config.localDir);
|
|
57
|
+
for (const sessionID of sessions) {
|
|
58
|
+
const dir = this.sessionDir(sessionID);
|
|
59
|
+
const s = await stat(dir).catch(() => null);
|
|
60
|
+
if (!s?.isDirectory())
|
|
61
|
+
continue;
|
|
62
|
+
const files = await readdir(dir);
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
if (!file.endsWith(".jsonl"))
|
|
65
|
+
continue;
|
|
66
|
+
const filepath = join(dir, file);
|
|
67
|
+
const fstat = await stat(filepath).catch(() => null);
|
|
68
|
+
if (!fstat)
|
|
69
|
+
continue;
|
|
70
|
+
const hasMarker = files.includes(`${file}.uploaded`);
|
|
71
|
+
if (hasMarker && fstat.mtimeMs < cutoff) {
|
|
72
|
+
await unlink(filepath).catch(() => { });
|
|
73
|
+
await unlink(`${filepath}.uploaded`).catch(() => { });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Remove empty session directories
|
|
77
|
+
const remaining = await readdir(dir).catch(() => ["placeholder"]);
|
|
78
|
+
if (remaining.length === 0) {
|
|
79
|
+
await rmdir(dir).catch(() => { });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// ignore
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
let _counter = 0;
|
|
89
|
+
function makeTimestampNanos() {
|
|
90
|
+
const ms = Date.now();
|
|
91
|
+
const seq = _counter++;
|
|
92
|
+
return `${ms}${String(seq % 1_000_000).padStart(6, "0")}`;
|
|
93
|
+
}
|
|
94
|
+
/** @internal — exposed for testing */
|
|
95
|
+
export function _resetCounter() {
|
|
96
|
+
_counter = 0;
|
|
97
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface LogEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
timestamp: string;
|
|
4
|
+
timestampNanos: string;
|
|
5
|
+
sessionID: string;
|
|
6
|
+
hook: HookName;
|
|
7
|
+
data: {
|
|
8
|
+
input: unknown;
|
|
9
|
+
output: unknown;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export type HookName = "chat.message" | "chat.params" | "experimental.chat.system.transform" | "experimental.chat.messages.transform" | "experimental.text.complete" | "tool.execute.before" | "tool.execute.after" | "event";
|
|
13
|
+
export interface PluginConfig {
|
|
14
|
+
localDir: string;
|
|
15
|
+
r2Remote: string;
|
|
16
|
+
retentionDays: number;
|
|
17
|
+
flushThresholdBytes: number;
|
|
18
|
+
}
|
|
19
|
+
export declare const DEFAULT_CONFIG: PluginConfig;
|
|
20
|
+
/** Event types worth logging for training data */
|
|
21
|
+
export declare const LOG_EVENT_TYPES: Set<string>;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const DEFAULT_CONFIG = {
|
|
2
|
+
localDir: `${process.env.HOME ?? require("node:os").homedir()}/.cache/opencode/provider-logs`,
|
|
3
|
+
r2Remote: "paradox:ailog",
|
|
4
|
+
retentionDays: 7,
|
|
5
|
+
flushThresholdBytes: 10 * 1024 * 1024, // 10 MB
|
|
6
|
+
};
|
|
7
|
+
/** Event types worth logging for training data */
|
|
8
|
+
export const LOG_EVENT_TYPES = new Set([
|
|
9
|
+
"message.updated",
|
|
10
|
+
"session.idle",
|
|
11
|
+
"session.status",
|
|
12
|
+
"session.created",
|
|
13
|
+
"session.compacted",
|
|
14
|
+
]);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PluginConfig } from "./types.js";
|
|
2
|
+
import type { LocalWriter, PendingFile } from "./logger.js";
|
|
3
|
+
type LogFn = (level: "debug" | "info" | "warn" | "error", message: string, extra?: Record<string, unknown>) => void;
|
|
4
|
+
export declare class R2Uploader {
|
|
5
|
+
private config;
|
|
6
|
+
private writer;
|
|
7
|
+
private log;
|
|
8
|
+
constructor(config: PluginConfig, writer: LocalWriter, log: LogFn);
|
|
9
|
+
upload(file: PendingFile): Promise<boolean>;
|
|
10
|
+
retryPending(): Promise<void>;
|
|
11
|
+
cleanup(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export {};
|
package/dist/uploader.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
export class R2Uploader {
|
|
5
|
+
config;
|
|
6
|
+
writer;
|
|
7
|
+
log;
|
|
8
|
+
constructor(config, writer, log) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.writer = writer;
|
|
11
|
+
this.log = log;
|
|
12
|
+
}
|
|
13
|
+
async upload(file) {
|
|
14
|
+
const remotePath = `${this.config.r2Remote}/${file.sessionID}/${file.tsNanos}.jsonl`;
|
|
15
|
+
try {
|
|
16
|
+
await execFileAsync("rclone", [
|
|
17
|
+
"copyto",
|
|
18
|
+
file.path,
|
|
19
|
+
remotePath,
|
|
20
|
+
"--no-traverse",
|
|
21
|
+
]);
|
|
22
|
+
await this.writer.markUploaded(file.path);
|
|
23
|
+
this.log("info", `Uploaded → ${remotePath}`);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
this.log("warn", `Upload failed: ${file.path}`, {
|
|
28
|
+
error: String(err),
|
|
29
|
+
});
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async retryPending() {
|
|
34
|
+
const pending = await this.writer.getPending();
|
|
35
|
+
if (pending.length === 0)
|
|
36
|
+
return;
|
|
37
|
+
this.log("info", `Retrying ${pending.length} pending upload(s)`);
|
|
38
|
+
for (const file of pending) {
|
|
39
|
+
await this.upload(file);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async cleanup() {
|
|
43
|
+
await this.writer.cleanup();
|
|
44
|
+
}
|
|
45
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@p4r4d0xb0x/opencode-provider-logger",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode plugin that logs all provider requests/responses for model training",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": ["dist"],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"test": "bun test",
|
|
12
|
+
"prepublishOnly": "tsc"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"@opencode-ai/plugin": ">=1.3.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@opencode-ai/plugin": "1.3.0",
|
|
19
|
+
"@opencode-ai/sdk": "1.3.0",
|
|
20
|
+
"@types/bun": "^1.3.11",
|
|
21
|
+
"@types/node": "^25.5.0",
|
|
22
|
+
"typescript": "^5.7.0"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT"
|
|
25
|
+
}
|