opencode-session-spawn 2.0.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +67 -0
  3. package/index.js +136 -0
  4. package/package.json +35 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Qiu Kejian
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # opencode-session-spawn
2
+
3
+ opencode 插件:**会话生成原语**。只做一件事——**创建一个新的顶层会话,并把起始语句注入为该会话的首条消息**。
4
+
5
+ 它的用途是**无人值守长任务编排**:当一条任务超出单会话上下文时,当前会话的模型可以在需要时自行生成一段交接语句、调用工具开出一个新会话继续,新会话拿到语句立即开跑。插件不落文书、不读文书、不追踪链、无状态文件——需要交接文书时,把「读取某份文书路径」等引导写进起始语句即可,与其他工具天然兼容。
6
+
7
+ ## 两个入口(共用同一实现)
8
+
9
+ | 入口 | 用法 | 谁触发 |
10
+ |------|------|--------|
11
+ | 工具 `spawn_session` | `spawn_session({ prompt, title? })` | **模型自主调用**(打通无人值守) |
12
+ | 命令 | `@spawn <起始语句>`(兼容 `@relay spawn`) | 人工触发 |
13
+
14
+ 参数 / 行为:
15
+
16
+ - `prompt`(必填):注入新会话的起始语句。可内含读取文件、复述要点、执行下一步等引导。
17
+ - `title`(可选):新会话标题。缺省取 `prompt` 首行(清洗非法字符、截断 60)。
18
+ - 内部调用:`client.session.create({ body: { title } })` → `client.session.promptAsync({ path: { id }, body: { parts: [{ type: "text", text: prompt }] } })`。
19
+ - 优先 `promptAsync`(异步 fire-and-forget,**不阻塞父会话**);缺失则回退同步 `prompt`。
20
+ - create 失败 → 不建会话;create 成功但注入失败 → **保留已建会话**并提示 id,不产生重复会话。
21
+
22
+ ## 安装(部署到桌面版)
23
+
24
+ 桌面版从用户配置目录加载活插件:
25
+
26
+ ```bash
27
+ cp index.js package.json "$HOME/.config/opencode/plugins/session-relay/"
28
+ ```
29
+
30
+ 在全局 `opencode.jsonc` 的 `plugin` 数组注册 `./plugins/session-relay`,重启桌面版生效。
31
+
32
+ ## 编排示例
33
+
34
+ 模型在长任务中决定交接,直接调用工具:
35
+
36
+ ```
37
+ spawn_session({
38
+ prompt: `你是本任务的接手会话。先读取交接文书:
39
+ E:\\proj\\handoff\\migration-3.md
40
+ 通读后复述要点(已完成/未完成/下一步第 1 件事),确认后继续执行。`,
41
+ title: "[迁移] 第 3 棒"
42
+ })
43
+ ```
44
+
45
+ 或人工在会话里发:
46
+
47
+ ```
48
+ @spawn 读取 handoff/migration-3.md 后继续迁移任务
49
+ ```
50
+
51
+ ## 开发与测试
52
+
53
+ ```bash
54
+ npm install # 安装 devDependency @opencode-ai/plugin(唯一依赖,仅测试用)
55
+ node test/test.mjs # 12 用例 / 39 断言
56
+ ```
57
+
58
+ - `test/test.mjs` 默认经相对路径 `../index.js` 测项目版;环境变量 `RELAY_MAIN` 覆盖为其他路径(如生产活实例)。生产版从 `.config/opencode/node_modules` 解析 `@opencode-ai/plugin`。
59
+ - 以尾部 `==== 结果: N 通过, 0 失败 ====` 为准。
60
+
61
+ ## 发布
62
+
63
+ `index.js` + `package.json` 成对同步生产 → `RELAY_MAIN=<生产路径> node test/test.mjs` 回归全绿 → 重启桌面版实测 → commit → push → `npm publish`(不打 git tag;发布前 `npm whoami` 确认登录态,凭据不入库)。详见 `AGENTS.md`。
64
+
65
+ ## 历史
66
+
67
+ 前身 `opencode-session-relay` 的接力链、两阶段交接文书、空闲事件驱动、哨兵兜底、`@relay status/leave/refresh/verify` 等机制已全部移除,仅保留「建会话 + 注入语句」这一核心原语。
package/index.js ADDED
@@ -0,0 +1,136 @@
1
+ import { tool } from "@opencode-ai/plugin/tool";
2
+
3
+ /**
4
+ * session-spawn —— 会话生成原语(v2.0.0,2026-09-12 卍解简化定稿)
5
+ *
6
+ * 只做一件事:创建一个新的顶层会话,并把起始语句注入为该会话首条消息。
7
+ * create: client.session.create({ body: { title } })
8
+ * inject: client.session.promptAsync({ path: { id }, body: { parts: [{ type: "text", text }] } })
9
+ *
10
+ * 设计约束(用户定稿):
11
+ * - 插件只负责「建会话 + 注入语句」,不落文书、不读文书、不追踪链、无状态文件。
12
+ * - 编排中若需交接文书,由调用方把「读取文书路径」等引导写进起始语句即可(与其他工具天然兼容)。
13
+ * - 优先 promptAsync(异步 fire-and-forget,不阻塞父会话);缺失则回退同步 prompt。
14
+ * - 两个入口共用同一 spawn():① 工具 spawn_session(模型自主触发,打通无人值守)
15
+ * ② 命令 @spawn / @relay spawn(人工触发)。
16
+ *
17
+ * 历史:本仓库前身 session-relay 的接力链、两阶段文书、空闲事件驱动、哨兵等机制已全部移除。
18
+ */
19
+
20
+ // 标题清洗:首行 → 去 Windows 非法字符与控制符 → 截断 60;空则回退 "spawn"。
21
+ function defaultTitle(prompt) {
22
+ return (
23
+ String(prompt || "")
24
+ .split(/\r?\n/)[0]
25
+ .replace(/[<>:"/\\|?*\x00-\x1f]/g, "")
26
+ .trim()
27
+ .slice(0, 60) || "spawn"
28
+ );
29
+ }
30
+
31
+ function userText(parts) {
32
+ if (!Array.isArray(parts)) return "";
33
+ return parts
34
+ .filter((p) => p && p.type === "text" && typeof p.text === "string")
35
+ .map((p) => p.text)
36
+ .join(" ");
37
+ }
38
+
39
+ export const SessionSpawn = async (ctx) => {
40
+ const { client } = ctx;
41
+
42
+ const notify = async (title, message, variant = "info") => {
43
+ try {
44
+ if (client && client.tui && client.tui.showToast) {
45
+ await client.tui.showToast({ title, message, variant, duration: 15000 });
46
+ }
47
+ } catch (e) {
48
+ console.error(`[session-spawn] toast 通知失败: ${e.message}`);
49
+ }
50
+ };
51
+
52
+ // 核心:建新会话 + 注入起始语句。
53
+ // 返回 { id, title, injected, error? };create 失败则抛出(未建会话)。
54
+ async function spawn(prompt, title) {
55
+ const text = typeof prompt === "string" ? prompt : "";
56
+ if (!text.trim()) throw new Error("起始语句为空");
57
+ if (!client || !client.session || typeof client.session.create !== "function") {
58
+ throw new Error("client.session.create 不可用");
59
+ }
60
+ const t = (title && String(title).trim()) || defaultTitle(text);
61
+
62
+ const created = await client.session.create({ body: { title: t } });
63
+ const id = created && (created.data?.id || created.id);
64
+ if (!id) throw new Error("create 未返回会话 id");
65
+
66
+ const body = { parts: [{ type: "text", text }] };
67
+ const injectFn =
68
+ typeof client.session.promptAsync === "function"
69
+ ? client.session.promptAsync.bind(client.session)
70
+ : typeof client.session.prompt === "function"
71
+ ? client.session.prompt.bind(client.session)
72
+ : null;
73
+ if (!injectFn) return { id, title: t, injected: false, error: "client.session.promptAsync/prompt 均不可用" };
74
+
75
+ try {
76
+ await injectFn({ path: { id }, body });
77
+ return { id, title: t, injected: true };
78
+ } catch (e) {
79
+ console.error(`[session-spawn] 注入失败(会话 ${id} 已建保留): ${e.message}`);
80
+ return { id, title: t, injected: false, error: e.message };
81
+ }
82
+ }
83
+
84
+ return {
85
+ // 工具:模型自主调用 → 无人值守长任务编排的最短路径。
86
+ tool: {
87
+ spawn_session: tool({
88
+ description:
89
+ "创建一个新的顶层会话并向其注入起始语句,用于无人值守长任务编排:当前会话模型在需要时自行决定把工作交给一个新会话继续。新会话会立即开始执行该起始语句。若需让其读取某份交接文书,请把文书路径与读取要求写进 prompt。",
90
+ args: {
91
+ prompt: tool.schema.string().describe("注入新会话的起始语句(可内含读取交接文书路径等引导)"),
92
+ title: tool.schema.string().optional().describe("新会话标题;缺省取起始语句首行"),
93
+ },
94
+ async execute(args) {
95
+ try {
96
+ const r = await spawn(args.prompt, args.title);
97
+ if (!r.injected) {
98
+ return `会话「${r.title}」(id=${r.id}) 已创建,但起始语句注入失败:${r.error}。请手动打开该会话补发指令。`;
99
+ }
100
+ return `已创建会话「${r.title}」(id=${r.id}) 并注入起始语句,新会话已开始执行。`;
101
+ } catch (e) {
102
+ return `创建会话失败:${e.message}`;
103
+ }
104
+ },
105
+ }),
106
+ },
107
+
108
+ // 人工命令:@spawn <起始语句>(兼容 @relay spawn)。
109
+ "chat.message": async ({ sessionID }, { parts }) => {
110
+ try {
111
+ const text = userText(parts);
112
+ const m = /^\s*@(?:relay\s+)?spawn\b([\s\S]*)$/i.exec(text);
113
+ if (!m) return;
114
+ const prompt = m[1].trim();
115
+ if (!prompt) {
116
+ await notify("spawn", "用法:@spawn <起始语句>", "warning");
117
+ return;
118
+ }
119
+ try {
120
+ const r = await spawn(prompt);
121
+ if (!r.injected) {
122
+ await notify("spawn", `会话「${r.title}」(id=${r.id}) 已创建,但注入失败:${r.error}`, "warning");
123
+ } else {
124
+ await notify("spawn", `已创建会话「${r.title}」(id=${r.id}) 并注入起始语句。`);
125
+ }
126
+ } catch (e) {
127
+ await notify("spawn", `创建失败:${e.message}`, "error");
128
+ }
129
+ } catch (e) {
130
+ console.error(`[session-spawn] chat.message 处理失败: ${e.message}`);
131
+ }
132
+ },
133
+ };
134
+ };
135
+
136
+ export default SessionSpawn;
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "opencode-session-spawn",
3
+ "version": "2.0.0",
4
+ "type": "module",
5
+ "description": "opencode 会话生成原语:创建新顶层会话并注入起始语句,用于无人值守长任务编排(工具 spawn_session + 命令 @spawn)。",
6
+ "main": "index.js",
7
+ "files": [
8
+ "index.js",
9
+ "package.json",
10
+ "README.md",
11
+ "LICENSE"
12
+ ],
13
+ "keywords": [
14
+ "opencode",
15
+ "plugin",
16
+ "session",
17
+ "spawn",
18
+ "orchestration",
19
+ "unattended"
20
+ ],
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/Qiu-Kejian/opencode-session-relay.git"
25
+ },
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "peerDependencies": {
30
+ "@opencode-ai/plugin": "*"
31
+ },
32
+ "devDependencies": {
33
+ "@opencode-ai/plugin": "^1.1.36"
34
+ }
35
+ }