opencode-collaboration 0.6.1 → 0.8.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 +11 -3
- package/README.zh-CN.md +11 -3
- package/dist/commands.d.ts +5 -0
- package/dist/commands.js +11 -4
- package/dist/config.d.ts +2 -1
- package/dist/config.js +1 -0
- package/dist/index.js +4 -0
- package/dist/scope.d.ts +14 -0
- package/dist/scope.js +23 -0
- package/dist/session-runtime.js +0 -16
- package/dist/tools/peers-tools.d.ts +5 -0
- package/dist/tools/peers-tools.js +14 -5
- package/dist/types.d.ts +10 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -98,6 +98,10 @@ Use send_message to tell "backend" that the login form now posts to /v2/login.
|
|
|
98
98
|
|
|
99
99
|
The receiving session gets the text immediately as a synthetic user message, including the sender's exact endpoint ID and how to reply. The receiver is told to reply via `send_message` only when a response is actually expected, and is explicitly told not to send acknowledgement-only messages — so two agents do not loop politely forever. `send_message` returns a tracking ID; use `peer_message_status` or `/peers-outbox` to distinguish transport receipt from final delivery.
|
|
100
100
|
|
|
101
|
+
By default only peers in the same working directory are visible and targetable (`peerScope: "directory"`); cross-directory peers stay hidden from `list_agents`/`/peers` and `send_message`. Set `peerScope: "all"` to collaborate across directories. Inbound messages are never restricted by scope.
|
|
102
|
+
|
|
103
|
+
An injected peer turn runs under the agent/mode of the session's **last human input** (message or command) — e.g. if the receiver last typed in `plan`, peer turns arrive in plan mode and cannot edit. opencode only persists a mode once a message is sent in it, so a bare UI mode toggle is not yet visible to the plugin. To have peers reach you in a specific mode, send any message in that mode first.
|
|
104
|
+
|
|
101
105
|
**Review held messages** (when `inboundPolicy` is `"hold"`):
|
|
102
106
|
|
|
103
107
|
```
|
|
@@ -123,6 +127,7 @@ Options can be passed via the tuple form in `opencode.json`:
|
|
|
123
127
|
|---|---|---|
|
|
124
128
|
| `inboundPolicy` | `"accept"` | `accept` delivers immediately; `auto` accepts only when sender and receiver directories match and otherwise holds; `hold` parks messages for review; `refuse` rejects them |
|
|
125
129
|
| `peerPermissions` | `"allow"` | Peer-origin permission requests: `allow` auto-approves ordinary requests, `ask` leaves native prompts untouched, `deny` rejects. Even in `allow`, OpenCode/plugin permission configuration, `AGENTS.md`, credentials/secrets and permission escalation are never auto-approved; existing OpenCode deny rules always win |
|
|
130
|
+
| `peerScope` | `"directory"` | `directory` (default) restricts `list_agents`/`/peers`/`send_message` to peers whose working directory is exactly this session's directory; `"all"` restores cross-directory visibility. Inbound delivery is never restricted |
|
|
126
131
|
| `name` | `<dir>-<hex4>` | display name other peers use to address you; the default appends a short hex suffix (from the instance ID) to the directory basename so same-directory instances are distinguishable, matching Claude Code's `my-app-3f` pattern |
|
|
127
132
|
| `showNameInTitle` | `true` | Append this process's peer name to its root session titles (e.g. `Fix login bug(张三)`); the suffix is removed on graceful exit |
|
|
128
133
|
| `storageDir` | `$XDG_DATA_HOME/opencode-collaboration` | where the registry and held inbox live |
|
|
@@ -214,12 +219,15 @@ The credential-free real-host test starts actual OpenCode processes and drives t
|
|
|
214
219
|
|
|
215
220
|
```bash
|
|
216
221
|
npm install
|
|
217
|
-
npm run build
|
|
218
|
-
npm
|
|
222
|
+
npm run build # tsc → dist/ (plain, for fast local iteration)
|
|
223
|
+
npm run build:obfuscated # clean + tsc + obfuscation → dist/ (the shippable artifact)
|
|
224
|
+
npm test # build:obfuscated + node --test tests/*.test.mjs
|
|
219
225
|
npm run typecheck
|
|
220
|
-
npm run dry-run
|
|
226
|
+
npm run dry-run # npm publish --dry-run
|
|
221
227
|
```
|
|
222
228
|
|
|
229
|
+
`npm test` runs against the shippable `dist/`; `tests/obfuscation.test.mjs` only passes there. Sourcemaps for the shipped build are written to `.obfuscate-maps/` and are never published.
|
|
230
|
+
|
|
223
231
|
Zero runtime dependencies beyond `@opencode-ai/plugin` (peer) and `zod` (tool schemas).
|
|
224
232
|
|
|
225
233
|
## License
|
package/README.zh-CN.md
CHANGED
|
@@ -98,6 +98,10 @@ Use send_message to tell "backend" that the login form now posts to /v2/login.
|
|
|
98
98
|
|
|
99
99
|
接收方会话会立即收到这条文本,以一条合成的用户消息形式出现,其中包含发送方精确的端点 ID 以及回复方式。接收方被要求**只在确实需要回应时**才通过 `send_message` 回复,并明确禁止仅确认/致谢类消息——避免两个 agent 无限礼貌往返。`send_message` 返回一个追踪 ID;用 `peer_message_status` 或 `/peers-outbox` 来区分"传输层已收到"和"最终已投递"。
|
|
100
100
|
|
|
101
|
+
默认情况下,只有与本会话工作目录完全相同的对端才可见、可寻址(`peerScope: "directory"`);跨目录的对端在 `list_agents`/`/peers` 和 `send_message` 中不可见。设置 `peerScope: "all"` 即可跨目录协作。入站消息永远不受 scope 限制。
|
|
102
|
+
|
|
103
|
+
注入的 peer 回合会以该会话**最后一条人类输入(消息/命令)的 agent/mode** 运行——例如接收方最后在 `plan` 模式下发过消息,那么 peer 回合就以 plan 模式到达、无法编辑。opencode 只有在某个 mode 下发过消息才会持久化它,因此"仅切换 UI mode"插件还无法感知。若希望 peer 以指定 mode 找到你,请先在该 mode 下发送一条消息。
|
|
104
|
+
|
|
101
105
|
**审阅待审消息**(当 `inboundPolicy` 为 `"hold"` 时):
|
|
102
106
|
|
|
103
107
|
```
|
|
@@ -123,6 +127,7 @@ Use send_message to tell "backend" that the login form now posts to /v2/login.
|
|
|
123
127
|
|---|---|---|
|
|
124
128
|
| `inboundPolicy` | `"accept"` | `accept` 立即投递;`auto` 仅当发送方与接收方目录相同时接受,否则进入待审;`hold` 暂存消息供人工审阅;`refuse` 直接拒绝 |
|
|
125
129
|
| `peerPermissions` | `"allow"` | 对端来源的权限请求:`allow` 自动批准普通请求,`ask` 保持原生提示不变,`deny` 拒绝。即使在 `allow` 模式下,OpenCode/插件权限配置、`AGENTS.md`、凭据/密钥以及权限升级也永远不会被自动批准;已存在的 OpenCode 拒绝规则始终优先 |
|
|
130
|
+
| `peerScope` | `"directory"` | `directory`(默认)将 `list_agents`/`/peers`/`send_message` 限定为与本会话工作目录完全相同的对端;`"all"` 恢复跨目录可见性。入站消息不受 scope 限制 |
|
|
126
131
|
| `name` | `<dir>-<hex4>` | 其他对端用来寻址你的显示名;默认值在目录名后追加一个短十六进制后缀(取自实例 ID),使同目录的多个实例可以区分,与 Claude Code 的 `my-app-3f` 命名方式一致 |
|
|
127
132
|
| `showNameInTitle` | `true` | 把本进程的 peer 名字追加到根会话标题后面(如 `Fix login bug(张三)`);优雅退出时会自动移除该后缀 |
|
|
128
133
|
| `storageDir` | `$XDG_DATA_HOME/opencode-collaboration` | 注册表与待审收件箱的存储目录 |
|
|
@@ -214,12 +219,15 @@ cd /tmp/proj-b && opencode serve --port 14101 &
|
|
|
214
219
|
|
|
215
220
|
```bash
|
|
216
221
|
npm install
|
|
217
|
-
npm run build
|
|
218
|
-
npm
|
|
222
|
+
npm run build # tsc → dist/(明文,本地快速迭代用)
|
|
223
|
+
npm run build:obfuscated # clean + tsc + 混淆 → dist/(发布产物)
|
|
224
|
+
npm test # build:obfuscated + node --test tests/*.test.mjs
|
|
219
225
|
npm run typecheck
|
|
220
|
-
npm run dry-run
|
|
226
|
+
npm run dry-run # npm publish --dry-run
|
|
221
227
|
```
|
|
222
228
|
|
|
229
|
+
`npm test` 跑的是发布产物 dist;`tests/obfuscation.test.mjs` 只在发布产物上通过。发布产物的 sourcemap 输出到 `.obfuscate-maps/`,不随包发布。
|
|
230
|
+
|
|
223
231
|
除 `@opencode-ai/plugin`(peer 依赖)和 `zod`(工具 schema)外,零运行时依赖。
|
|
224
232
|
|
|
225
233
|
## 许可证
|
package/dist/commands.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { RegistryInstance } from "./registry.js";
|
|
|
8
8
|
import type { QueueInstance } from "./queue.js";
|
|
9
9
|
import type { DeliveryInstance } from "./delivery.js";
|
|
10
10
|
import type { OutboxInstance } from "./outbox.js";
|
|
11
|
+
import type { PeerScope } from "./types.js";
|
|
11
12
|
export interface CommandContext {
|
|
12
13
|
registry: RegistryInstance;
|
|
13
14
|
queue: QueueInstance;
|
|
@@ -20,6 +21,10 @@ export interface CommandContext {
|
|
|
20
21
|
selfInstanceId: string;
|
|
21
22
|
/** Exact endpoint for the session that invoked this command. */
|
|
22
23
|
selfEndpointId?: string;
|
|
24
|
+
/** Working directory for peerScope filtering. */
|
|
25
|
+
directory: string;
|
|
26
|
+
/** Visibility scope: "directory" (default) or "all". */
|
|
27
|
+
peerScope: PeerScope;
|
|
23
28
|
outbox?: Pick<OutboxInstance, "list">;
|
|
24
29
|
}
|
|
25
30
|
export interface CommandResult {
|
package/dist/commands.js
CHANGED
|
@@ -6,17 +6,24 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { validateName } from "./config.js";
|
|
8
8
|
import { formatSessionList } from "./format.js";
|
|
9
|
+
import { sameDirectory } from "./scope.js";
|
|
9
10
|
export async function handlePeersCommand(ctx, command, args) {
|
|
10
11
|
// "list-agents" is an alias of "peers", matching Claude Code's /list-agents.
|
|
11
12
|
if (command === "peers" || command === "list-agents") {
|
|
12
13
|
const selfId = ctx.selfEndpointId ?? ctx.selfInstanceId;
|
|
13
14
|
const peers = (await ctx.registry.list()).filter((peer) => (peer.entry.version === 2 ? peer.entry.endpointId : peer.entry.instanceId) !== selfId);
|
|
14
|
-
const
|
|
15
|
+
const inScope = (peer) => ctx.peerScope !== "directory" || sameDirectory(peer.entry.directory, ctx.directory);
|
|
16
|
+
const listing = formatSessionList(peers.filter(inScope), Date.now());
|
|
15
17
|
const held = ctx.queue.held();
|
|
16
18
|
const pending = ctx.queue.size();
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
const suffixParts = [];
|
|
20
|
+
if (held.length > 0 || pending > 0) {
|
|
21
|
+
suffixParts.push(`(${pending} queued, ${held.length} held — /peers-inbox to review)`);
|
|
22
|
+
}
|
|
23
|
+
if (ctx.peerScope === "directory") {
|
|
24
|
+
suffixParts.push(`peerScope: "directory" — cross-directory peers are hidden; set peerScope "all" to show them.`);
|
|
25
|
+
}
|
|
26
|
+
const suffix = suffixParts.length > 0 ? `\n${suffixParts.join("\n")}` : "";
|
|
20
27
|
const selfLine = `You are "${ctx.getName()}" — /peers-name to change.`;
|
|
21
28
|
return { handled: true, message: `📋 ${selfLine}\n${listing}${suffix}` };
|
|
22
29
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PeerPermissionMode, PluginConfig } from "./types.js";
|
|
1
|
+
import type { PeerPermissionMode, PeerScope, PluginConfig } from "./types.js";
|
|
2
2
|
export interface ResolvedConfig {
|
|
3
3
|
storageDir: string;
|
|
4
4
|
peersDir: string;
|
|
@@ -8,6 +8,7 @@ export interface ResolvedConfig {
|
|
|
8
8
|
showNameInTitle: boolean;
|
|
9
9
|
inboundPolicy: "accept" | "auto" | "hold" | "refuse";
|
|
10
10
|
peerPermissions: PeerPermissionMode;
|
|
11
|
+
peerScope: PeerScope;
|
|
11
12
|
heartbeatMs: number;
|
|
12
13
|
staleMs: number;
|
|
13
14
|
maxQueue: number;
|
package/dist/config.js
CHANGED
|
@@ -17,6 +17,7 @@ export function resolveConfig(opts, env = process.env) {
|
|
|
17
17
|
showNameInTitle: opts?.showNameInTitle ?? true,
|
|
18
18
|
inboundPolicy: opts?.inboundPolicy ?? "accept",
|
|
19
19
|
peerPermissions: opts?.peerPermissions ?? "allow",
|
|
20
|
+
peerScope: opts?.peerScope ?? "directory",
|
|
20
21
|
heartbeatMs: opts?.heartbeatMs ?? 10_000,
|
|
21
22
|
staleMs: opts?.staleMs ?? 30_000,
|
|
22
23
|
maxQueue: opts?.maxQueue ?? 50,
|
package/dist/index.js
CHANGED
|
@@ -357,6 +357,8 @@ export const PeersPlugin = async (ctx, pluginOptions) => {
|
|
|
357
357
|
},
|
|
358
358
|
selfInstanceId: instanceId,
|
|
359
359
|
selfEndpointId: runtime.endpointIdForSession(input.sessionID) ?? instanceId,
|
|
360
|
+
directory: ctx.directory,
|
|
361
|
+
peerScope: config.peerScope,
|
|
360
362
|
outbox,
|
|
361
363
|
}, input.command, input.arguments || "");
|
|
362
364
|
message = result.message ?? "✅ Done.";
|
|
@@ -378,6 +380,8 @@ export const PeersPlugin = async (ctx, pluginOptions) => {
|
|
|
378
380
|
maxMessageBytes: config.maxMessageBytes,
|
|
379
381
|
selfName: () => currentName,
|
|
380
382
|
selfInstanceId: instanceId,
|
|
383
|
+
selfDirectory: ctx.directory,
|
|
384
|
+
peerScope: config.peerScope,
|
|
381
385
|
endpointForSession: (sessionId) => {
|
|
382
386
|
const endpointId = runtime.endpointIdForSession(sessionId);
|
|
383
387
|
const endpoint = runtime.registryEndpoints().find((entry) => entry.sessionId === sessionId);
|
package/dist/scope.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a filesystem path for scope comparison: resolve relative paths,
|
|
3
|
+
* strip a trailing separator (so `D:\a\b\` equals `D:\a\b`), and lowercase on
|
|
4
|
+
* Windows where the filesystem is case-insensitive. POSIX stays case-sensitive.
|
|
5
|
+
* Symlinks are not resolved — 8.3 short paths and network shares are out of
|
|
6
|
+
* scope for the directory-visibility feature.
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeDirectory(directory: string, platform?: NodeJS.Platform): string;
|
|
9
|
+
/**
|
|
10
|
+
* Exact same-directory match for peerScope: "directory". Directory strings in
|
|
11
|
+
* registry entries are session-scoped and can differ from the process launch
|
|
12
|
+
* directory in trailing slash or case, so both sides are normalized first.
|
|
13
|
+
*/
|
|
14
|
+
export declare function sameDirectory(a: string, b: string, platform?: NodeJS.Platform): boolean;
|
package/dist/scope.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { resolve, sep } from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Normalize a filesystem path for scope comparison: resolve relative paths,
|
|
4
|
+
* strip a trailing separator (so `D:\a\b\` equals `D:\a\b`), and lowercase on
|
|
5
|
+
* Windows where the filesystem is case-insensitive. POSIX stays case-sensitive.
|
|
6
|
+
* Symlinks are not resolved — 8.3 short paths and network shares are out of
|
|
7
|
+
* scope for the directory-visibility feature.
|
|
8
|
+
*/
|
|
9
|
+
export function normalizeDirectory(directory, platform = process.platform) {
|
|
10
|
+
let normalized = resolve(directory);
|
|
11
|
+
if (normalized.length > 1 && normalized.endsWith(sep)) {
|
|
12
|
+
normalized = normalized.slice(0, -1);
|
|
13
|
+
}
|
|
14
|
+
return platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Exact same-directory match for peerScope: "directory". Directory strings in
|
|
18
|
+
* registry entries are session-scoped and can differ from the process launch
|
|
19
|
+
* directory in trailing slash or case, so both sides are normalized first.
|
|
20
|
+
*/
|
|
21
|
+
export function sameDirectory(a, b, platform = process.platform) {
|
|
22
|
+
return normalizeDirectory(a, platform) === normalizeDirectory(b, platform);
|
|
23
|
+
}
|
package/dist/session-runtime.js
CHANGED
|
@@ -387,22 +387,6 @@ export function SessionRuntime(opts) {
|
|
|
387
387
|
setStatus(endpoint, event.type === "session.idle" ? "idle" : normalizeStatus(properties.status));
|
|
388
388
|
return true;
|
|
389
389
|
}
|
|
390
|
-
// The user toggled the session's agent/mode (e.g. build<->plan). Peer
|
|
391
|
-
// injections use the last-known agent, so refreshing here keeps the
|
|
392
|
-
// next injected turn under the mode the user actually selected — even
|
|
393
|
-
// when the toggle happens without a follow-up user message.
|
|
394
|
-
if (event.type === "session.next.agent.switched" ||
|
|
395
|
-
event.type === "session.next.agent.switched.1") {
|
|
396
|
-
const sessionId = properties.sessionID;
|
|
397
|
-
const agent = properties.agent;
|
|
398
|
-
if (!sessionId || !agent)
|
|
399
|
-
return false;
|
|
400
|
-
const endpoint = await findSession(sessionId);
|
|
401
|
-
if (!endpoint)
|
|
402
|
-
return false;
|
|
403
|
-
endpoint.agent = agent;
|
|
404
|
-
return true;
|
|
405
|
-
}
|
|
406
390
|
return false;
|
|
407
391
|
});
|
|
408
392
|
},
|
|
@@ -8,6 +8,7 @@ import type { ListedPeer, RegistryInstance } from "../registry.js";
|
|
|
8
8
|
import type { Sender } from "../sender.js";
|
|
9
9
|
import type { RateLimiter } from "../queue.js";
|
|
10
10
|
import type { OutboxInstance } from "../outbox.js";
|
|
11
|
+
import type { PeerScope } from "../types.js";
|
|
11
12
|
export interface ToolsDeps {
|
|
12
13
|
registry: RegistryInstance;
|
|
13
14
|
sender: ReturnType<typeof Sender>;
|
|
@@ -15,6 +16,10 @@ export interface ToolsDeps {
|
|
|
15
16
|
maxMessageBytes: number;
|
|
16
17
|
selfName: () => string;
|
|
17
18
|
selfInstanceId: string;
|
|
19
|
+
/** Fallback working directory (session-scoped endpointForSession takes precedence). */
|
|
20
|
+
selfDirectory: string;
|
|
21
|
+
/** Visibility scope: "directory" (default) or "all". */
|
|
22
|
+
peerScope: PeerScope;
|
|
18
23
|
endpointForSession?: (sessionId: string) => {
|
|
19
24
|
endpointId: string;
|
|
20
25
|
name: string;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { collapseToProcesses, sortPeers } from "../format.js";
|
|
9
|
+
import { sameDirectory } from "../scope.js";
|
|
9
10
|
function entryId(entry) {
|
|
10
11
|
return entry.version === 2 ? entry.endpointId : entry.instanceId;
|
|
11
12
|
}
|
|
@@ -59,7 +60,7 @@ export function buildPeerTools(deps) {
|
|
|
59
60
|
},
|
|
60
61
|
}),
|
|
61
62
|
list_agents: tool({
|
|
62
|
-
description: "List other opencode session endpoints on this machine that you can exchange plain-text messages with. Shows each endpoint's name, id, directory, session and inbound policy.",
|
|
63
|
+
description: "List other opencode session endpoints on this machine that you can exchange plain-text messages with. Shows each endpoint's name, id, directory, session and inbound policy. Only peers in the same working directory are shown (peerScope \"directory\"); set peerScope \"all\" to see cross-directory peers.",
|
|
63
64
|
args: {
|
|
64
65
|
include_offline: z
|
|
65
66
|
.boolean()
|
|
@@ -76,13 +77,16 @@ export function buildPeerTools(deps) {
|
|
|
76
77
|
}
|
|
77
78
|
const self = deps.endpointForSession?.(context.sessionID);
|
|
78
79
|
const selfId = self?.endpointId ?? deps.selfInstanceId;
|
|
80
|
+
const selfDir = self?.directory ?? deps.selfDirectory;
|
|
81
|
+
const inScope = (peer) => deps.peerScope !== "directory" || sameDirectory(peer.entry.directory, selfDir);
|
|
79
82
|
const shown = (args.include_offline ? peers : peers.filter((p) => p.alive))
|
|
80
|
-
.filter((peer) => entryId(peer.entry) !== selfId)
|
|
83
|
+
.filter((peer) => entryId(peer.entry) !== selfId)
|
|
84
|
+
.filter(inScope);
|
|
81
85
|
return formatPeerList(shown, self?.name ?? deps.selfName(), selfId);
|
|
82
86
|
},
|
|
83
87
|
}),
|
|
84
88
|
send_message: tool({
|
|
85
|
-
description: "Send a plain-text message immediately to an exact opencode session on this machine, including while it is busy. Text only — no files or conversation history. Resolve the target with list_agents first if unsure.",
|
|
89
|
+
description: "Send a plain-text message immediately to an exact opencode session on this machine, including while it is busy. Text only — no files or conversation history. Resolve the target with list_agents first if unsure. Targets are limited to peers in the same working directory unless peerScope is \"all\".",
|
|
86
90
|
args: {
|
|
87
91
|
to: z.string().describe("Peer name or instanceId (see list_agents)"),
|
|
88
92
|
message: z.string().describe("Plain-text message body"),
|
|
@@ -99,6 +103,8 @@ export function buildPeerTools(deps) {
|
|
|
99
103
|
return `Error: sender session "${context.sessionID}" is not registered.`;
|
|
100
104
|
}
|
|
101
105
|
const selfId = self?.endpointId ?? deps.selfInstanceId;
|
|
106
|
+
const selfDir = self?.directory ?? deps.selfDirectory;
|
|
107
|
+
const inScope = (peer) => deps.peerScope !== "directory" || sameDirectory(peer.entry.directory, selfDir);
|
|
102
108
|
const listed = await deps.registry.list();
|
|
103
109
|
const target = args.to.trim();
|
|
104
110
|
const knownExact = listed.find((peer) => entryId(peer.entry) === target);
|
|
@@ -109,14 +115,17 @@ export function buildPeerTools(deps) {
|
|
|
109
115
|
if (!knownExact.alive) {
|
|
110
116
|
return `Error: endpoint "${target}" appears offline (${knownExact.staleReason}).`;
|
|
111
117
|
}
|
|
118
|
+
if (!inScope(knownExact)) {
|
|
119
|
+
return `Error: endpoint "${target}" is running in a different working directory (peerScope is "directory"). Set peerScope "all" to collaborate across directories.`;
|
|
120
|
+
}
|
|
112
121
|
}
|
|
113
122
|
else if (isEndpointShaped(target)) {
|
|
114
123
|
return `Error: unknown endpoint ID "${target}".`;
|
|
115
124
|
}
|
|
116
|
-
const peers = listed.filter((p) => p.alive && entryId(p.entry) !== selfId);
|
|
125
|
+
const peers = listed.filter((p) => p.alive && entryId(p.entry) !== selfId && inScope(p));
|
|
117
126
|
let matches = knownExact ? [knownExact] : peers.filter((p) => p.entry.name === target);
|
|
118
127
|
if (matches.length === 0) {
|
|
119
|
-
const stale = listed.find((p) => !p.alive && p.entry.name === target);
|
|
128
|
+
const stale = listed.find((p) => !p.alive && inScope(p) && p.entry.name === target);
|
|
120
129
|
if (stale) {
|
|
121
130
|
return `Error: peer "${target}" appears offline (${stale.staleReason}).`;
|
|
122
131
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export type InboundPolicy = "accept" | "auto" | "hold" | "refuse";
|
|
2
2
|
export type PeerPermissionMode = "allow" | "ask" | "deny";
|
|
3
|
+
/** Visibility scope for other peers. "directory" shows/sends only to peers in
|
|
4
|
+
* the exact same working directory; "all" keeps the previous behavior. */
|
|
5
|
+
export type PeerScope = "directory" | "all";
|
|
3
6
|
export interface PeerFrom {
|
|
4
7
|
instanceId: string;
|
|
5
8
|
name: string;
|
|
@@ -143,6 +146,13 @@ export interface PluginConfig {
|
|
|
143
146
|
* peer-triggered turns.
|
|
144
147
|
*/
|
|
145
148
|
peerPermissions?: PeerPermissionMode;
|
|
149
|
+
/**
|
|
150
|
+
* Which peers are visible/targetable. "directory" (default) restricts
|
|
151
|
+
* list_agents/send_message to peers whose working directory is exactly this
|
|
152
|
+
* session's directory; "all" restores cross-directory visibility. Inbound
|
|
153
|
+
* delivery is never restricted. Default "directory".
|
|
154
|
+
*/
|
|
155
|
+
peerScope?: PeerScope;
|
|
146
156
|
/** Heartbeat interval ms. Default 10_000. */
|
|
147
157
|
heartbeatMs?: number;
|
|
148
158
|
/** A peer is stale if its heartbeat is older than this. Default 30_000. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-collaboration",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|
|
@@ -60,11 +60,14 @@
|
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "tsc -p tsconfig.json",
|
|
63
|
+
"clean": "node scripts/clean-dist.mjs",
|
|
64
|
+
"build:obfuscated": "npm run clean && tsc -p tsconfig.json && node scripts/obfuscate-dist.mjs",
|
|
65
|
+
"verify:obfuscated": "node scripts/verify-obfuscated.mjs",
|
|
63
66
|
"prepare": "npm run build",
|
|
64
|
-
"test": "npm run build && node --test tests/*.test.mjs",
|
|
67
|
+
"test": "npm run build:obfuscated && node --test tests/*.test.mjs",
|
|
65
68
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
66
69
|
"dry-run": "npm publish --dry-run",
|
|
67
|
-
"prepublishOnly": "npm run build"
|
|
70
|
+
"prepublishOnly": "npm run build:obfuscated && npm run verify:obfuscated"
|
|
68
71
|
},
|
|
69
72
|
"peerDependencies": {
|
|
70
73
|
"@opencode-ai/plugin": ">=1.18.0"
|
|
@@ -78,6 +81,7 @@
|
|
|
78
81
|
"@opentui/keymap": "^0.4.5",
|
|
79
82
|
"@opentui/solid": "^0.4.5",
|
|
80
83
|
"@types/node": "^22.0.0",
|
|
84
|
+
"javascript-obfuscator": "5.7.0",
|
|
81
85
|
"typescript": "^5.4.0"
|
|
82
86
|
},
|
|
83
87
|
"opencode": {
|