opencode-collaboration 0.4.0 → 0.5.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 +1 -0
- package/README.zh-CN.md +1 -0
- package/dist/delivery.d.ts +12 -0
- package/dist/delivery.js +57 -22
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -1
- package/dist/session-runtime.d.ts +1 -0
- package/dist/session-runtime.js +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ Run several opencode terminals in parallel (different repos, worktrees, or tasks
|
|
|
27
27
|
- **Explicit TUI controls**: palette actions use host dialogs for selection and confirmation; slash wrappers remain available for automation and compatibility
|
|
28
28
|
- Local only: everything stays on your machine (Unix-domain sockets on macOS/Linux, loopback TCP on Windows, plus a compatibility loopback listener for v1 peers)
|
|
29
29
|
- The peer name is appended to this process's root session titles, so every opencode window shows who it is at a glance
|
|
30
|
+
- Peer-triggered turns run under the receiving session's selected agent/mode (e.g. a custom `teacher`) instead of opencode's default `build`, so that agent's model, tools and permission rules apply. The mode is taken from the session's last human input (message or command) and its server-persisted agent; if none is known yet, the default agent is used
|
|
30
31
|
|
|
31
32
|
## Install
|
|
32
33
|
|
package/README.zh-CN.md
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
- **明确的 TUI 控制**:面板操作使用宿主对话框进行选择确认;斜杠命令封装仍可用于自动化和兼容性场景
|
|
28
28
|
- 纯本地运行:一切都在你的机器上(macOS/Linux 使用 Unix 域套接字,Windows 使用回环 TCP,另有一个兼容 v1 对端的回环监听器)
|
|
29
29
|
- 本进程的 peer 名字会追加到根会话标题后面,一眼就能看出每个 opencode 窗口是谁
|
|
30
|
+
- peer 触发的回合会沿用接收方会话当前选中的 agent/模式(如自定义的 `teacher`),而不是 opencode 默认的 `build`,因此该 agent 的模型、工具与权限规则都会生效。模式取自该会话最近一条人类输入(消息或命令)及其服务端持久化的 agent;若尚无所知则使用默认 agent
|
|
30
31
|
|
|
31
32
|
## 安装
|
|
32
33
|
|
package/dist/delivery.d.ts
CHANGED
|
@@ -18,6 +18,18 @@ export interface DeliveryOptions {
|
|
|
18
18
|
logger: Logger;
|
|
19
19
|
/** Protocol-v2 delivery targets this session immediately, even while busy. */
|
|
20
20
|
immediate?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* The agent/mode the receiving session currently has selected (e.g. a custom
|
|
23
|
+
* "teacher"). When set, injected peer turns run under it instead of opencode's
|
|
24
|
+
* default agent. Returns undefined when unknown.
|
|
25
|
+
*/
|
|
26
|
+
agent?: () => string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Called when an injection failed because the recorded agent no longer
|
|
29
|
+
* exists, so the caller can drop it. The message is then retried once under
|
|
30
|
+
* opencode's default agent instead of requeue-looping on the same error.
|
|
31
|
+
*/
|
|
32
|
+
onAgentRejected?: () => void;
|
|
21
33
|
/**
|
|
22
34
|
* Max time one prompt injection may take before it is treated as a delivery
|
|
23
35
|
* failure and the message is requeued. Without this bound a hung
|
package/dist/delivery.js
CHANGED
|
@@ -62,28 +62,15 @@ export function Delivery(opts) {
|
|
|
62
62
|
const status = sdkResult?.response?.status;
|
|
63
63
|
const statusText = sdkResult?.response?.statusText;
|
|
64
64
|
const detail = status ? ` (${status}${statusText ? ` ${statusText}` : ""})` : "";
|
|
65
|
-
|
|
65
|
+
const rawError = sdkResult?.error;
|
|
66
|
+
const errorText = rawError == null
|
|
67
|
+
? "request failed"
|
|
68
|
+
: typeof rawError === "string"
|
|
69
|
+
? rawError
|
|
70
|
+
: JSON.stringify(rawError);
|
|
71
|
+
throw new Error(`OpenCode prompt injection failed${detail}: ${errorText}`);
|
|
66
72
|
}
|
|
67
|
-
async function
|
|
68
|
-
const part = {
|
|
69
|
-
type: "text",
|
|
70
|
-
text,
|
|
71
|
-
synthetic: true,
|
|
72
|
-
metadata: {
|
|
73
|
-
peerMessage: message ? {
|
|
74
|
-
version: 2,
|
|
75
|
-
messageId: message.id,
|
|
76
|
-
fromEndpointId: message.from.instanceId,
|
|
77
|
-
toSessionId: sessionId,
|
|
78
|
-
} : true,
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
const messageID = message ? deterministicPeerMessageId(sessionId, message) : undefined;
|
|
82
|
-
const body = {
|
|
83
|
-
...(messageID ? { messageID } : {}),
|
|
84
|
-
...(message ? { system: REPLY_DIRECTIVE } : {}),
|
|
85
|
-
parts: [part],
|
|
86
|
-
};
|
|
73
|
+
async function sendPrompt(sessionId, body) {
|
|
87
74
|
const session = opts.client.session;
|
|
88
75
|
if (typeof session.promptAsync === "function") {
|
|
89
76
|
const result = await withTimeout(session.promptAsync({
|
|
@@ -97,11 +84,59 @@ export function Delivery(opts) {
|
|
|
97
84
|
}
|
|
98
85
|
const result = await withTimeout(opts.client.session.prompt({
|
|
99
86
|
path: { id: sessionId },
|
|
100
|
-
body,
|
|
87
|
+
body: body,
|
|
101
88
|
query: { directory: opts.directory },
|
|
102
89
|
}));
|
|
103
90
|
assertPromptSucceeded(result);
|
|
104
91
|
}
|
|
92
|
+
function isAgentNotFoundError(err) {
|
|
93
|
+
const source = err;
|
|
94
|
+
const text = [
|
|
95
|
+
err instanceof Error ? err.message : String(err),
|
|
96
|
+
source?.data?.message,
|
|
97
|
+
source?.responseBody,
|
|
98
|
+
source?.body,
|
|
99
|
+
]
|
|
100
|
+
.filter((value) => typeof value === "string" && value.length > 0)
|
|
101
|
+
.join(" ")
|
|
102
|
+
.toLowerCase();
|
|
103
|
+
return text.includes("agent not found");
|
|
104
|
+
}
|
|
105
|
+
async function inject(sessionId, text, message) {
|
|
106
|
+
const messageID = message ? deterministicPeerMessageId(sessionId, message) : undefined;
|
|
107
|
+
const buildBody = (agent) => ({
|
|
108
|
+
...(messageID ? { messageID } : {}),
|
|
109
|
+
...(agent ? { agent } : {}),
|
|
110
|
+
...(message ? { system: REPLY_DIRECTIVE } : {}),
|
|
111
|
+
parts: [
|
|
112
|
+
{
|
|
113
|
+
type: "text",
|
|
114
|
+
text,
|
|
115
|
+
synthetic: true,
|
|
116
|
+
metadata: {
|
|
117
|
+
peerMessage: message ? {
|
|
118
|
+
version: 2,
|
|
119
|
+
messageId: message.id,
|
|
120
|
+
fromEndpointId: message.from.instanceId,
|
|
121
|
+
toSessionId: sessionId,
|
|
122
|
+
} : true,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
const agent = opts.agent?.();
|
|
128
|
+
try {
|
|
129
|
+
await sendPrompt(sessionId, buildBody(agent));
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
// The recorded agent no longer exists (e.g. removed from config). Drop it
|
|
133
|
+
// and retry once under opencode's default agent instead of requeue-looping.
|
|
134
|
+
if (!agent || !isAgentNotFoundError(err))
|
|
135
|
+
throw err;
|
|
136
|
+
opts.onAgentRejected?.();
|
|
137
|
+
await sendPrompt(sessionId, buildBody(undefined));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
105
140
|
async function flushOnce() {
|
|
106
141
|
if (flushing)
|
|
107
142
|
return false;
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import type { Plugin, PluginModule } from "@opencode-ai/plugin";
|
|
23
23
|
import type { QueueInstance } from "./queue.js";
|
|
24
24
|
import type { DeliveryInstance } from "./delivery.js";
|
|
25
|
+
export declare const PLUGIN_VERSION: string;
|
|
25
26
|
export declare function runReliabilitySweep(queue: QueueInstance, delivery: Pick<DeliveryInstance, "flush">): Promise<void>;
|
|
26
27
|
export declare const PeersPlugin: Plugin;
|
|
27
28
|
export declare const plugin: PluginModule;
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* listener; the receiving instance injects into its own session via the
|
|
20
20
|
* opencode SDK immediately, including while the target session is busy.
|
|
21
21
|
*/
|
|
22
|
+
import { readFileSync } from "node:fs";
|
|
22
23
|
import { resolveConfig, defaultPeerName } from "./config.js";
|
|
23
24
|
import { Registry, newInboxToken, newInstanceId } from "./registry.js";
|
|
24
25
|
import { InboxListener } from "./listener.js";
|
|
@@ -31,7 +32,18 @@ import { PeerPermissions } from "./permissions.js";
|
|
|
31
32
|
import { buildPeerTools } from "./tools/peers-tools.js";
|
|
32
33
|
import { handlePeersCommand } from "./commands.js";
|
|
33
34
|
import { consumeCommand, createLogger, errorMessage } from "./feedback.js";
|
|
34
|
-
|
|
35
|
+
// Read the real package version at runtime so registry entries never report a
|
|
36
|
+
// stale hardcoded number. Falls back to "0.0.0" if package.json is unavailable.
|
|
37
|
+
function readPluginVersion() {
|
|
38
|
+
try {
|
|
39
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
40
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return "0.0.0";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export const PLUGIN_VERSION = readPluginVersion();
|
|
35
47
|
const COMMAND_NAMES = new Set(["peers", "list-agents", "peers-name", "peers-inbox", "peers-outbox"]);
|
|
36
48
|
/**
|
|
37
49
|
* Command definitions injected through the `config` hook. opencode 1.18 does
|
|
@@ -279,6 +291,8 @@ export const PeersPlugin = async (ctx, pluginOptions) => {
|
|
|
279
291
|
if (disposing)
|
|
280
292
|
return;
|
|
281
293
|
await runtime.noteActivity(input.sessionID);
|
|
294
|
+
if (input.agent)
|
|
295
|
+
await runtime.noteAgent(input.sessionID, input.agent);
|
|
282
296
|
if (!disposing)
|
|
283
297
|
await registry.heartbeat();
|
|
284
298
|
},
|
|
@@ -28,6 +28,7 @@ export interface SessionRuntimeInstance {
|
|
|
28
28
|
properties?: Record<string, unknown>;
|
|
29
29
|
}) => Promise<boolean>;
|
|
30
30
|
noteActivity: (sessionId: string) => Promise<void>;
|
|
31
|
+
noteAgent: (sessionId: string, agent: string) => Promise<void>;
|
|
31
32
|
queueForSession: (sessionId: string) => QueueInstance | null;
|
|
32
33
|
deliveryForSession: (sessionId: string) => DeliveryInstance | null;
|
|
33
34
|
sweep: () => Promise<void>;
|
package/dist/session-runtime.js
CHANGED
|
@@ -38,6 +38,8 @@ export function SessionRuntime(opts) {
|
|
|
38
38
|
current.updatedAt = Math.max(current.updatedAt, session.time.updated);
|
|
39
39
|
if (status)
|
|
40
40
|
setStatus(current, status);
|
|
41
|
+
if (session.agent)
|
|
42
|
+
current.agent = session.agent;
|
|
41
43
|
return current;
|
|
42
44
|
}
|
|
43
45
|
const queue = createSessionMessageQueue({ config: opts.config, sessionId: session.id, logger: opts.logger });
|
|
@@ -51,6 +53,7 @@ export function SessionRuntime(opts) {
|
|
|
51
53
|
updatedAt: session.time.updated,
|
|
52
54
|
queue,
|
|
53
55
|
tracker,
|
|
56
|
+
agent: session.agent,
|
|
54
57
|
delivery: undefined,
|
|
55
58
|
};
|
|
56
59
|
if (endpoint.status !== "idle")
|
|
@@ -62,6 +65,10 @@ export function SessionRuntime(opts) {
|
|
|
62
65
|
directory: session.directory || opts.directory,
|
|
63
66
|
logger: opts.logger,
|
|
64
67
|
immediate: true,
|
|
68
|
+
agent: () => endpoint.agent,
|
|
69
|
+
onAgentRejected: () => {
|
|
70
|
+
endpoint.agent = undefined;
|
|
71
|
+
},
|
|
65
72
|
});
|
|
66
73
|
endpoints.set(session.id, endpoint);
|
|
67
74
|
return endpoint;
|
|
@@ -390,6 +397,13 @@ export function SessionRuntime(opts) {
|
|
|
390
397
|
setStatus(endpoint, "busy");
|
|
391
398
|
});
|
|
392
399
|
},
|
|
400
|
+
noteAgent(sessionId, agent) {
|
|
401
|
+
return whileRunning(undefined, async () => {
|
|
402
|
+
const endpoint = await findSession(sessionId);
|
|
403
|
+
if (endpoint)
|
|
404
|
+
endpoint.agent = agent;
|
|
405
|
+
});
|
|
406
|
+
},
|
|
393
407
|
queueForSession(sessionId) {
|
|
394
408
|
return endpoints.get(sessionId)?.queue ?? null;
|
|
395
409
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-collaboration",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Cross-session messaging for opencode — let independent sessions discover and text each other, modeled after Claude Code's cross-session messaging",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|