@mrc2204/opencode-bridge 0.1.1 → 0.1.3
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.en.md +73 -94
- package/README.md +73 -94
- package/dist/opencode-plugin/openclaw-bridge-callback.d.ts +5 -0
- package/dist/opencode-plugin/openclaw-bridge-callback.js +179 -0
- package/dist/src/chunk-OVQ5X54C.js +289 -0
- package/dist/src/chunk-TDVN5AFB.js +36 -0
- package/dist/{index.js → src/index.js} +605 -22
- package/dist/src/observability.d.ts +97 -0
- package/dist/{observability.js → src/observability.js} +3 -1
- package/dist/src/shared-contracts.d.ts +33 -0
- package/dist/src/shared-contracts.js +10 -0
- package/openclaw.plugin.json +2 -2
- package/opencode-plugin/README.md +25 -0
- package/opencode-plugin/openclaw-bridge-callback.ts +186 -0
- package/package.json +17 -9
- package/scripts/install-bridge.mjs +60 -0
- package/scripts/materialize-opencode-plugin.mjs +75 -0
- package/skills/opencode-orchestration/SKILL.md +108 -66
- package/src/shared-contracts.ts +58 -0
- package/dist/chunk-6NIQKNRA.js +0 -176
- package/dist/observability.d.ts +0 -52
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
type OpenCodeEventKind = "task.started" | "task.progress" | "permission.requested" | "task.stalled" | "task.failed" | "task.completed";
|
|
2
|
+
type EventScope = "session" | "global";
|
|
3
|
+
type SseFrame = {
|
|
4
|
+
event?: string;
|
|
5
|
+
id?: string;
|
|
6
|
+
retry?: number;
|
|
7
|
+
data: string;
|
|
8
|
+
raw: string;
|
|
9
|
+
};
|
|
10
|
+
type TypedEventV1 = {
|
|
11
|
+
schema: "opencode.event.v1";
|
|
12
|
+
scope: EventScope;
|
|
13
|
+
eventName?: string;
|
|
14
|
+
eventId?: string;
|
|
15
|
+
kind: OpenCodeEventKind | null;
|
|
16
|
+
summary?: string;
|
|
17
|
+
lifecycleState?: "planning" | "coding" | "verifying" | "blocked" | "running" | "awaiting_permission" | "stalled" | "failed" | "completed";
|
|
18
|
+
filesChanged?: string[];
|
|
19
|
+
verifySummary?: {
|
|
20
|
+
command?: string;
|
|
21
|
+
exit?: number | null;
|
|
22
|
+
output_preview?: string | null;
|
|
23
|
+
} | null;
|
|
24
|
+
blockers?: string[];
|
|
25
|
+
completionSummary?: string | null;
|
|
26
|
+
runId?: string;
|
|
27
|
+
taskId?: string;
|
|
28
|
+
sessionId?: string;
|
|
29
|
+
timestamp: string;
|
|
30
|
+
wrappers: string[];
|
|
31
|
+
payload: any;
|
|
32
|
+
};
|
|
33
|
+
declare function parseSseFramesFromBuffer(input: string): {
|
|
34
|
+
frames: SseFrame[];
|
|
35
|
+
remainder: string;
|
|
36
|
+
};
|
|
37
|
+
declare function parseSseData(data: string): any;
|
|
38
|
+
declare function unwrapGlobalPayload(raw: any): {
|
|
39
|
+
payload: any;
|
|
40
|
+
wrappers: string[];
|
|
41
|
+
};
|
|
42
|
+
declare function normalizeOpenCodeEvent(raw: any): {
|
|
43
|
+
kind: OpenCodeEventKind | null;
|
|
44
|
+
summary?: string;
|
|
45
|
+
raw: any;
|
|
46
|
+
lifecycleState?: TypedEventV1["lifecycleState"];
|
|
47
|
+
filesChanged?: string[];
|
|
48
|
+
verifySummary?: {
|
|
49
|
+
command?: string;
|
|
50
|
+
exit?: number | null;
|
|
51
|
+
output_preview?: string | null;
|
|
52
|
+
} | null;
|
|
53
|
+
blockers?: string[];
|
|
54
|
+
completionSummary?: string | null;
|
|
55
|
+
};
|
|
56
|
+
declare function normalizeTypedEventV1(frame: SseFrame, scope: EventScope): TypedEventV1;
|
|
57
|
+
declare function summarizeLifecycle(events?: Array<{
|
|
58
|
+
kind?: OpenCodeEventKind | null;
|
|
59
|
+
summary?: string;
|
|
60
|
+
lifecycleState?: TypedEventV1["lifecycleState"];
|
|
61
|
+
filesChanged?: string[];
|
|
62
|
+
verifySummary?: {
|
|
63
|
+
command?: string;
|
|
64
|
+
exit?: number | null;
|
|
65
|
+
output_preview?: string | null;
|
|
66
|
+
} | null;
|
|
67
|
+
blockers?: string[];
|
|
68
|
+
completionSummary?: string | null;
|
|
69
|
+
timestamp?: string;
|
|
70
|
+
}>): {
|
|
71
|
+
currentState: "planning" | "coding" | "verifying" | "blocked" | "running" | "awaiting_permission" | "stalled" | "failed" | "completed" | null;
|
|
72
|
+
current_state: "planning" | "coding" | "verifying" | "blocked" | "running" | "awaiting_permission" | "stalled" | "failed" | "completed" | null;
|
|
73
|
+
last_event_kind: OpenCodeEventKind | null;
|
|
74
|
+
last_event_at: string | null;
|
|
75
|
+
files_changed: string[];
|
|
76
|
+
verify_summary: {
|
|
77
|
+
command?: string;
|
|
78
|
+
exit?: number | null;
|
|
79
|
+
output_preview?: string | null;
|
|
80
|
+
}[];
|
|
81
|
+
blockers: string[];
|
|
82
|
+
completion_summary: string | null;
|
|
83
|
+
};
|
|
84
|
+
declare function resolveSessionId(input: {
|
|
85
|
+
explicitSessionId?: string;
|
|
86
|
+
runId?: string;
|
|
87
|
+
taskId?: string;
|
|
88
|
+
sessionKey?: string;
|
|
89
|
+
artifactSessionId?: string;
|
|
90
|
+
sessionList?: any[];
|
|
91
|
+
}): {
|
|
92
|
+
sessionId?: string;
|
|
93
|
+
strategy: "explicit" | "artifact" | "scored_fallback" | "latest" | "none";
|
|
94
|
+
score?: number;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export { type EventScope, type OpenCodeEventKind, type SseFrame, type TypedEventV1, normalizeOpenCodeEvent, normalizeTypedEventV1, parseSseData, parseSseFramesFromBuffer, resolveSessionId, summarizeLifecycle, unwrapGlobalPayload };
|
|
@@ -4,13 +4,15 @@ import {
|
|
|
4
4
|
parseSseData,
|
|
5
5
|
parseSseFramesFromBuffer,
|
|
6
6
|
resolveSessionId,
|
|
7
|
+
summarizeLifecycle,
|
|
7
8
|
unwrapGlobalPayload
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-OVQ5X54C.js";
|
|
9
10
|
export {
|
|
10
11
|
normalizeOpenCodeEvent,
|
|
11
12
|
normalizeTypedEventV1,
|
|
12
13
|
parseSseData,
|
|
13
14
|
parseSseFramesFromBuffer,
|
|
14
15
|
resolveSessionId,
|
|
16
|
+
summarizeLifecycle,
|
|
15
17
|
unwrapGlobalPayload
|
|
16
18
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
type BridgeSessionTagFields = {
|
|
2
|
+
runId: string;
|
|
3
|
+
taskId: string;
|
|
4
|
+
requested: string;
|
|
5
|
+
resolved: string;
|
|
6
|
+
callbackSession: string;
|
|
7
|
+
callbackSessionId?: string;
|
|
8
|
+
projectId?: string;
|
|
9
|
+
repoRoot?: string;
|
|
10
|
+
};
|
|
11
|
+
type OpenCodePluginCallbackAuditRecord = {
|
|
12
|
+
phase?: string;
|
|
13
|
+
event_type?: string;
|
|
14
|
+
session_id?: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
tags?: Record<string, string> | null;
|
|
17
|
+
dedupeKey?: string;
|
|
18
|
+
ok?: boolean;
|
|
19
|
+
status?: number;
|
|
20
|
+
reason?: string;
|
|
21
|
+
body?: string;
|
|
22
|
+
payload?: any;
|
|
23
|
+
raw?: any;
|
|
24
|
+
created_at: string;
|
|
25
|
+
};
|
|
26
|
+
declare function buildTaggedSessionTitle(fields: BridgeSessionTagFields): string;
|
|
27
|
+
declare function parseTaggedSessionTitle(title?: string): Record<string, string> | null;
|
|
28
|
+
declare function buildPluginCallbackDedupeKey(input: {
|
|
29
|
+
sessionId?: string;
|
|
30
|
+
runId?: string;
|
|
31
|
+
}): string;
|
|
32
|
+
|
|
33
|
+
export { type BridgeSessionTagFields, type OpenCodePluginCallbackAuditRecord, buildPluginCallbackDedupeKey, buildTaggedSessionTitle, parseTaggedSessionTitle };
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "opencode-bridge",
|
|
3
3
|
"name": "OpenCode Bridge",
|
|
4
|
-
"description": "Bridge plugin
|
|
5
|
-
"version": "0.1.
|
|
4
|
+
"description": "Bridge plugin for OpenClaw ↔ OpenCode orchestration, execution routing, and callback observability.",
|
|
5
|
+
"version": "0.1.3",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# OpenCode-side plugin boundary
|
|
2
|
+
|
|
3
|
+
This directory is the intended home for the canonical OpenCode-side callback plugin source.
|
|
4
|
+
|
|
5
|
+
## Current transitional state
|
|
6
|
+
The active runtime-tested plugin file currently lives at:
|
|
7
|
+
- `.opencode/plugins/openclaw-bridge-callback.ts`
|
|
8
|
+
|
|
9
|
+
That location is convenient for project-local loading in OpenCode.
|
|
10
|
+
|
|
11
|
+
## Current source-of-truth
|
|
12
|
+
Canonical OpenCode-side plugin source now lives at:
|
|
13
|
+
- `opencode-plugin/openclaw-bridge-callback.ts`
|
|
14
|
+
|
|
15
|
+
The runtime-loaded project-local file remains:
|
|
16
|
+
- `.opencode/plugins/openclaw-bridge-callback.ts`
|
|
17
|
+
|
|
18
|
+
but it should be treated as a thin re-export shim for local OpenCode loading during development.
|
|
19
|
+
|
|
20
|
+
## Intended next step
|
|
21
|
+
Promote this boundary further so the repository can ship:
|
|
22
|
+
- OpenClaw-side bridge artifact
|
|
23
|
+
- OpenCode-side callback plugin artifact
|
|
24
|
+
|
|
25
|
+
from one source-of-truth without mixing runtime test placement with canonical source layout.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { buildPluginCallbackDedupeKey, parseTaggedSessionTitle } from "../src/shared-contracts";
|
|
4
|
+
|
|
5
|
+
function asString(value: unknown): string | undefined {
|
|
6
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function ensureAuditDir(directory: string) {
|
|
10
|
+
mkdirSync(directory, { recursive: true });
|
|
11
|
+
return directory;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getAuditPath(directory: string) {
|
|
15
|
+
const auditDir = asString(process.env.OPENCLAW_BRIDGE_AUDIT_DIR) || join(directory, ".opencode");
|
|
16
|
+
ensureAuditDir(auditDir);
|
|
17
|
+
return join(auditDir, "bridge-callback-audit.jsonl");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function appendAudit(directory: string, record: any) {
|
|
21
|
+
const path = getAuditPath(directory);
|
|
22
|
+
appendFileSync(path, JSON.stringify({ ...record, created_at: new Date().toISOString() }) + "\n", "utf8");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getOpenClawAuditPath() {
|
|
26
|
+
const explicit = asString(process.env.OPENCLAW_BRIDGE_OPENCLAW_AUDIT_PATH);
|
|
27
|
+
if (explicit) {
|
|
28
|
+
ensureAuditDir(join(explicit, ".."));
|
|
29
|
+
return explicit;
|
|
30
|
+
}
|
|
31
|
+
const home = asString(process.env.HOME);
|
|
32
|
+
if (!home) return null;
|
|
33
|
+
const auditDir = join(home, ".openclaw", "opencode-bridge", "audit");
|
|
34
|
+
ensureAuditDir(auditDir);
|
|
35
|
+
return join(auditDir, "callbacks.jsonl");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function appendOpenClawAudit(record: any) {
|
|
39
|
+
const path = getOpenClawAuditPath();
|
|
40
|
+
if (!path) return;
|
|
41
|
+
appendFileSync(path, JSON.stringify({ ...record, createdAt: new Date().toISOString() }) + "\n", "utf8");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildCallbackPayload(tags: Record<string, string>, eventType: string) {
|
|
45
|
+
const agentId = tags.requested || tags.requested_agent_id;
|
|
46
|
+
const sessionKey = tags.callbackSession || tags.callback_target_session_key;
|
|
47
|
+
const sessionId = tags.callbackSessionId || tags.callback_target_session_id;
|
|
48
|
+
if (!agentId || !sessionKey) return null;
|
|
49
|
+
return {
|
|
50
|
+
message: `OpenCode plugin event=${eventType} run=${tags.runId || tags.run_id || "unknown"} task=${tags.taskId || tags.task_id || "unknown"}`,
|
|
51
|
+
name: "OpenCode",
|
|
52
|
+
agentId,
|
|
53
|
+
sessionKey,
|
|
54
|
+
...(sessionId ? { sessionId } : {}),
|
|
55
|
+
wakeMode: "now",
|
|
56
|
+
deliver: false,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function postCallback(directory: string, payload: any, meta?: { eventType?: string; sessionId?: string; runId?: string; taskId?: string; requestedAgentId?: string; resolvedAgentId?: string; callbackTargetSessionKey?: string; callbackTargetSessionId?: string }) {
|
|
61
|
+
const hookBaseUrl = asString(process.env.OPENCLAW_HOOK_BASE_URL);
|
|
62
|
+
const hookToken = asString(process.env.OPENCLAW_HOOK_TOKEN);
|
|
63
|
+
if (!hookBaseUrl || !hookToken) {
|
|
64
|
+
appendAudit(directory, { ok: false, status: 0, reason: "missing_hook_env", payload, meta });
|
|
65
|
+
appendOpenClawAudit({
|
|
66
|
+
taskId: meta?.taskId,
|
|
67
|
+
runId: meta?.runId,
|
|
68
|
+
agentId: payload?.agentId,
|
|
69
|
+
requestedAgentId: meta?.requestedAgentId,
|
|
70
|
+
resolvedAgentId: meta?.resolvedAgentId,
|
|
71
|
+
sessionKey: undefined,
|
|
72
|
+
callbackTargetSessionKey: meta?.callbackTargetSessionKey,
|
|
73
|
+
callbackTargetSessionId: meta?.callbackTargetSessionId,
|
|
74
|
+
event: meta?.eventType,
|
|
75
|
+
callbackStatus: 0,
|
|
76
|
+
callbackOk: false,
|
|
77
|
+
callbackBody: "missing_hook_env",
|
|
78
|
+
});
|
|
79
|
+
return { ok: false, status: 0, reason: "missing_hook_env" };
|
|
80
|
+
}
|
|
81
|
+
const response = await fetch(`${hookBaseUrl.replace(/\/$/, "")}/hooks/agent`, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: {
|
|
84
|
+
Authorization: `Bearer ${hookToken}`,
|
|
85
|
+
"Content-Type": "application/json",
|
|
86
|
+
},
|
|
87
|
+
body: JSON.stringify(payload),
|
|
88
|
+
});
|
|
89
|
+
const text = await response.text();
|
|
90
|
+
appendAudit(directory, { ok: response.ok, status: response.status, body: text, payload, meta });
|
|
91
|
+
const openClawAuditRecord = {
|
|
92
|
+
taskId: meta?.taskId,
|
|
93
|
+
runId: meta?.runId,
|
|
94
|
+
agentId: payload?.agentId,
|
|
95
|
+
requestedAgentId: meta?.requestedAgentId,
|
|
96
|
+
resolvedAgentId: meta?.resolvedAgentId,
|
|
97
|
+
sessionKey: undefined,
|
|
98
|
+
callbackTargetSessionKey: meta?.callbackTargetSessionKey,
|
|
99
|
+
callbackTargetSessionId: meta?.callbackTargetSessionId,
|
|
100
|
+
event: meta?.eventType,
|
|
101
|
+
callbackStatus: response.status,
|
|
102
|
+
callbackOk: response.ok,
|
|
103
|
+
callbackBody: text,
|
|
104
|
+
};
|
|
105
|
+
appendAudit(directory, { phase: "openclaw_audit_mirror", record: openClawAuditRecord });
|
|
106
|
+
appendOpenClawAudit(openClawAuditRecord);
|
|
107
|
+
return { ok: response.ok, status: response.status, body: text };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const callbackDedupe = new Set<string>();
|
|
111
|
+
const sessionTagCache = new Map<string, Record<string, string>>();
|
|
112
|
+
|
|
113
|
+
function readSessionId(event: any): string | undefined {
|
|
114
|
+
return (
|
|
115
|
+
asString(event?.session?.id) ||
|
|
116
|
+
asString(event?.session?.sessionID) ||
|
|
117
|
+
asString(event?.data?.session?.id) ||
|
|
118
|
+
asString(event?.data?.sessionID) ||
|
|
119
|
+
asString(event?.properties?.sessionID) ||
|
|
120
|
+
asString(event?.properties?.info?.sessionID) ||
|
|
121
|
+
asString(event?.properties?.info?.id) ||
|
|
122
|
+
asString(event?.payload?.sessionID)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isTerminalEvent(_event: any, type: string): boolean {
|
|
127
|
+
return type === "session.idle" || type === "session.error";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const OpenClawBridgeCallbackPlugin = async ({ client, directory }: any) => {
|
|
131
|
+
await client.app.log({
|
|
132
|
+
body: {
|
|
133
|
+
service: "openclaw-bridge-callback",
|
|
134
|
+
level: "info",
|
|
135
|
+
message: "OpenClaw bridge callback plugin initialized",
|
|
136
|
+
extra: { directory },
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
event: async ({ event }: any) => {
|
|
142
|
+
const type = asString(event?.type) || "unknown";
|
|
143
|
+
const sessionId = readSessionId(event);
|
|
144
|
+
const title =
|
|
145
|
+
asString(event?.session?.title) ||
|
|
146
|
+
asString(event?.data?.session?.title) ||
|
|
147
|
+
asString(event?.data?.title) ||
|
|
148
|
+
asString(event?.properties?.info?.title) ||
|
|
149
|
+
asString(event?.properties?.title) ||
|
|
150
|
+
asString(event?.payload?.session?.title) ||
|
|
151
|
+
asString(event?.payload?.title);
|
|
152
|
+
const parsedTags = parseTaggedSessionTitle(title);
|
|
153
|
+
if (sessionId && parsedTags && (parsedTags.callbackSession || parsedTags.callback_target_session_key)) {
|
|
154
|
+
sessionTagCache.set(sessionId, parsedTags);
|
|
155
|
+
}
|
|
156
|
+
const tags = parsedTags || (sessionId ? sessionTagCache.get(sessionId) || null : null);
|
|
157
|
+
appendAudit(directory, { phase: "event_seen", event_type: type, session_id: sessionId, title, tags, raw: event });
|
|
158
|
+
if (!tags || !(tags.callbackSession || tags.callback_target_session_key)) return;
|
|
159
|
+
if (!isTerminalEvent(event, type)) return;
|
|
160
|
+
const dedupeKey = buildPluginCallbackDedupeKey({ sessionId, runId: tags.runId || tags.run_id });
|
|
161
|
+
if (callbackDedupe.has(dedupeKey)) {
|
|
162
|
+
appendAudit(directory, { phase: "deduped", event_type: type, session_id: sessionId, dedupeKey, tags });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
callbackDedupe.add(dedupeKey);
|
|
166
|
+
const payload = buildCallbackPayload(tags, type);
|
|
167
|
+
if (!payload) {
|
|
168
|
+
appendAudit(directory, { phase: "skipped_no_payload", event_type: type, session_id: sessionId, tags });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
await postCallback(directory, payload, {
|
|
172
|
+
eventType: type,
|
|
173
|
+
sessionId,
|
|
174
|
+
runId: tags.runId || tags.run_id,
|
|
175
|
+
taskId: tags.taskId || tags.task_id,
|
|
176
|
+
requestedAgentId: tags.requested || tags.requested_agent_id,
|
|
177
|
+
resolvedAgentId: tags.resolved || tags.resolved_agent_id,
|
|
178
|
+
callbackTargetSessionKey: tags.callbackSession || tags.callback_target_session_key,
|
|
179
|
+
callbackTargetSessionId: tags.callbackSessionId || tags.callback_target_session_id,
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export default OpenClawBridgeCallbackPlugin;
|
|
186
|
+
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrc2204/opencode-bridge",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "OpenClaw ↔ OpenCode bridge plugin for routing, callback
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "OpenClaw ↔ OpenCode bridge plugin for hybrid routing, callback orchestration, and multi-project-safe runtime control.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./dist/index.js",
|
|
7
|
-
"types": "./dist/index.d.ts",
|
|
6
|
+
"main": "./dist/src/index.js",
|
|
7
|
+
"types": "./dist/src/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js",
|
|
12
|
-
"default": "./dist/index.js"
|
|
10
|
+
"types": "./dist/src/index.d.ts",
|
|
11
|
+
"import": "./dist/src/index.js",
|
|
12
|
+
"default": "./dist/src/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"private": false,
|
|
@@ -23,9 +23,13 @@
|
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"clean": "rm -rf dist",
|
|
26
|
-
"build": "tsup
|
|
26
|
+
"build": "tsup --config tsup.config.ts",
|
|
27
27
|
"typecheck": "tsc --noEmit",
|
|
28
28
|
"test": "tsx test/run-tests.ts",
|
|
29
|
+
"materialize:opencode-plugin:project": "node ./scripts/materialize-opencode-plugin.mjs --mode project",
|
|
30
|
+
"materialize:opencode-plugin:global": "node ./scripts/materialize-opencode-plugin.mjs --mode global",
|
|
31
|
+
"install:bridge:project": "node ./scripts/install-bridge.mjs --mode project",
|
|
32
|
+
"install:bridge:global": "node ./scripts/install-bridge.mjs --mode global",
|
|
29
33
|
"prepublishOnly": "npm run build && npm run test"
|
|
30
34
|
},
|
|
31
35
|
"devDependencies": {
|
|
@@ -36,12 +40,16 @@
|
|
|
36
40
|
},
|
|
37
41
|
"openclaw": {
|
|
38
42
|
"extensions": [
|
|
39
|
-
"./dist/index.js"
|
|
43
|
+
"./dist/src/index.js"
|
|
40
44
|
]
|
|
41
45
|
},
|
|
42
46
|
"files": [
|
|
43
47
|
"dist/",
|
|
44
48
|
"skills/",
|
|
49
|
+
"opencode-plugin/",
|
|
50
|
+
"scripts/materialize-opencode-plugin.mjs",
|
|
51
|
+
"scripts/install-bridge.mjs",
|
|
52
|
+
"src/shared-contracts.ts",
|
|
45
53
|
"README.md",
|
|
46
54
|
"README.en.md",
|
|
47
55
|
"openclaw.plugin.json"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve, dirname } from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
const args = { mode: "project", target: undefined, skipOpenClaw: false, skipOpencode: false };
|
|
8
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
9
|
+
const arg = argv[i];
|
|
10
|
+
if (arg === "--mode") args.mode = argv[++i] || args.mode;
|
|
11
|
+
else if (arg === "--target") args.target = argv[++i];
|
|
12
|
+
else if (arg === "--skip-openclaw") args.skipOpenClaw = true;
|
|
13
|
+
else if (arg === "--skip-opencode") args.skipOpencode = true;
|
|
14
|
+
}
|
|
15
|
+
return args;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function run(command, args, options = {}) {
|
|
19
|
+
const result = spawnSync(command, args, {
|
|
20
|
+
stdio: "inherit",
|
|
21
|
+
shell: false,
|
|
22
|
+
...options,
|
|
23
|
+
});
|
|
24
|
+
if (result.status !== 0) {
|
|
25
|
+
process.exit(result.status || 1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const args = parseArgs(process.argv.slice(2));
|
|
30
|
+
const repoRoot = resolve(dirname(new URL(import.meta.url).pathname), "..");
|
|
31
|
+
const target = args.target ? resolve(args.target) : process.cwd();
|
|
32
|
+
|
|
33
|
+
if (!args.skipOpenClaw) {
|
|
34
|
+
run("openclaw", ["plugins", "install", "-l", repoRoot]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!args.skipOpencode) {
|
|
38
|
+
const materializeArgs = [resolve(repoRoot, "scripts", "materialize-opencode-plugin.mjs"), "--mode", args.mode];
|
|
39
|
+
if (args.mode === "project") {
|
|
40
|
+
materializeArgs.push("--target", target);
|
|
41
|
+
} else if (args.target) {
|
|
42
|
+
materializeArgs.push("--target", target);
|
|
43
|
+
}
|
|
44
|
+
run("node", materializeArgs, { cwd: repoRoot });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const summary = {
|
|
48
|
+
ok: true,
|
|
49
|
+
mode: args.mode,
|
|
50
|
+
repoRoot,
|
|
51
|
+
target,
|
|
52
|
+
steps: {
|
|
53
|
+
openclawInstalled: !args.skipOpenClaw,
|
|
54
|
+
opencodeMaterialized: !args.skipOpencode,
|
|
55
|
+
},
|
|
56
|
+
note: args.mode === "global"
|
|
57
|
+
? "Global mode installed OpenClaw plugin locally and materialized OpenCode plugin into global config dir."
|
|
58
|
+
: "Project mode installed OpenClaw plugin locally and materialized OpenCode plugin into the target project's .opencode directory.",
|
|
59
|
+
};
|
|
60
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdirSync, copyFileSync, writeFileSync, existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
const args = { mode: "project", target: undefined, force: false };
|
|
8
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
9
|
+
const arg = argv[i];
|
|
10
|
+
if (arg === "--mode") args.mode = argv[++i] || args.mode;
|
|
11
|
+
else if (arg === "--target") args.target = argv[++i];
|
|
12
|
+
else if (arg === "--force") args.force = true;
|
|
13
|
+
}
|
|
14
|
+
return args;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ensurePluginEntry(configPath, pluginRef) {
|
|
18
|
+
if (!existsSync(configPath)) return { updated: false, reason: "missing_config" };
|
|
19
|
+
const raw = readFileSync(configPath, "utf8");
|
|
20
|
+
const data = JSON.parse(raw);
|
|
21
|
+
const plugins = Array.isArray(data.plugin) ? data.plugin : [];
|
|
22
|
+
if (!plugins.includes(pluginRef)) {
|
|
23
|
+
data.plugin = [...plugins, pluginRef];
|
|
24
|
+
writeFileSync(configPath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
25
|
+
return { updated: true, reason: "added_plugin_ref" };
|
|
26
|
+
}
|
|
27
|
+
return { updated: false, reason: "already_present" };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const args = parseArgs(process.argv.slice(2));
|
|
31
|
+
const repoRoot = resolve(dirname(new URL(import.meta.url).pathname), "..");
|
|
32
|
+
const pluginArtifact = join(repoRoot, "dist", "opencode-plugin", "openclaw-bridge-callback.js");
|
|
33
|
+
const sharedChunkArtifact = join(repoRoot, "dist", "chunk-TDVN5AFB.js");
|
|
34
|
+
if (!existsSync(pluginArtifact)) {
|
|
35
|
+
console.error(`Missing built plugin artifact: ${pluginArtifact}. Run npm run build first.`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
if (!existsSync(sharedChunkArtifact)) {
|
|
39
|
+
console.error(`Missing shared chunk artifact: ${sharedChunkArtifact}. Run npm run build first.`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const targetBase = args.target
|
|
44
|
+
? resolve(args.target)
|
|
45
|
+
: args.mode === "global"
|
|
46
|
+
? resolve(process.env.HOME || "~", ".config", "opencode")
|
|
47
|
+
: repoRoot;
|
|
48
|
+
|
|
49
|
+
const pluginDir = args.mode === "global"
|
|
50
|
+
? join(targetBase, "plugins")
|
|
51
|
+
: join(targetBase, ".opencode", "plugins");
|
|
52
|
+
const targetFile = join(pluginDir, "openclaw-bridge-callback.js");
|
|
53
|
+
|
|
54
|
+
mkdirSync(pluginDir, { recursive: true });
|
|
55
|
+
copyFileSync(pluginArtifact, targetFile);
|
|
56
|
+
|
|
57
|
+
let configUpdate = { updated: false, reason: "not_requested" };
|
|
58
|
+
if (args.mode === "global") {
|
|
59
|
+
const configPath = join(targetBase, "opencode.json");
|
|
60
|
+
configUpdate = ensurePluginEntry(configPath, "./plugins/openclaw-bridge-callback.js");
|
|
61
|
+
} else {
|
|
62
|
+
const configPath = join(targetBase, ".opencode", "opencode.json");
|
|
63
|
+
configUpdate = ensurePluginEntry(configPath, "./plugins/openclaw-bridge-callback.js");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(JSON.stringify({
|
|
67
|
+
ok: true,
|
|
68
|
+
mode: args.mode,
|
|
69
|
+
sourceFile: pluginArtifact,
|
|
70
|
+
targetFile,
|
|
71
|
+
configUpdate,
|
|
72
|
+
note: args.mode === "global"
|
|
73
|
+
? "Copy complete. Global OpenCode config was auto-patched when present."
|
|
74
|
+
: "Copy complete. Project-local .opencode/opencode.json was updated when present.",
|
|
75
|
+
}, null, 2));
|