opencode-session-spawn 2.0.1 → 2.1.1

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 (3) hide show
  1. package/README.md +22 -15
  2. package/index.js +68 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,59 +8,66 @@ opencode 插件:**会话生成原语**。只做一件事——**创建一个
8
8
 
9
9
  | 入口 | 用法 | 谁触发 |
10
10
  |------|------|--------|
11
- | 工具 `spawn_session` | `spawn_session({ prompt, title? })` | **模型自主调用**(打通无人值守) |
12
- | 命令 | `@spawn <起始语句>`(兼容 `@relay spawn`) | 人工触发 |
11
+ | 工具 `spawn_session` | `spawn_session({ prompt, title?, agent?, directory? })` | **模型自主调用**(打通无人值守) |
12
+ | 命令 | `@spawn [--agent <名称>] [--title <标题>] <起始语句>`(兼容 `@relay spawn`) | 人工触发 |
13
13
 
14
14
  参数 / 行为:
15
15
 
16
16
  - `prompt`(必填):注入新会话的起始语句。可内含读取文件、复述要点、执行下一步等引导。
17
17
  - `title`(可选):新会话标题。缺省取 `prompt` 首行(清洗非法字符、截断 60)。
18
- - 内部调用:`client.session.create({ body: { title } })` → `client.session.promptAsync({ path: { id }, body: { parts: [{ type: "text", text: prompt }] } })`。
18
+ - `agent`(可选,工具参数):新会话以某 agent/模式启动(如 `leader`)。仅在传了非空白值时透传,交服务端校验。
19
+ - `directory`(可选,工具参数):新会话绑定的项目目录(如 `D:\dev\bbcare`)。仅在传了非空白值时透传,交服务端校验。
20
+ - 命令 flag 说明:`--agent` / `--title` 值均为**单个 token**(不处理引号/空格);仅解析命令后**前导** flag,起始语句内部再出现的 `--xxx` 原样保留;flag 缺值或起始语句为空 → warning toast 且不建会话。`--title` 优先于起始语句首行。
21
+ - 内部调用:`client.session.create({ body: { title }, query: { directory } })` → `client.session.promptAsync({ path: { id }, body: { agent, parts: [{ type: "text", text: prompt }] } })`;`agent` / `directory` 未传时不出现对应键。
19
22
  - 优先 `promptAsync`(异步 fire-and-forget,**不阻塞父会话**);缺失则回退同步 `prompt`。
20
23
  - create 失败 → 不建会话;create 成功但注入失败 → **保留已建会话**并提示 id,不产生重复会话。
21
24
 
22
- ## 安装(部署到桌面版)
25
+ ## 安装
23
26
 
24
- 桌面版从用户配置目录加载活插件:
27
+ 在全局或项目 `opencode.json(c)` 的 `plugin` 数组注册包名,重启桌面版生效(opencode 自动拉取最新发布并装入包缓存):
25
28
 
26
- ```bash
27
- cp index.js package.json "$HOME/.config/opencode/plugins/session-relay/"
29
+ ```jsonc
30
+ {
31
+ "plugin": ["opencode-session-spawn"]
32
+ }
28
33
  ```
29
34
 
30
- 在全局 `opencode.jsonc` 的 `plugin` 数组注册 `./plugins/session-relay`,重启桌面版生效。
35
+ 本地开发也可注册本地目录(opencode 按路径加载):把 `index.js` + `package.json` 复制到自选目录,在 `plugin` 中写该目录相对路径(如 `./plugins/session-spawn`)。
31
36
 
32
37
  ## 编排示例
33
38
 
34
- 模型在长任务中决定交接,直接调用工具:
39
+ 模型在长任务中决定交接,直接调用工具(可选 `agent` / `directory`):
35
40
 
36
41
  ```
37
42
  spawn_session({
38
43
  prompt: `你是本任务的接手会话。先读取交接文书:
39
44
  E:\\proj\\handoff\\migration-3.md
40
45
  通读后复述要点(已完成/未完成/下一步第 1 件事),确认后继续执行。`,
41
- title: "[迁移] 第 3 棒"
46
+ title: "[迁移] 第 3 棒",
47
+ agent: "leader",
48
+ directory: "D:\\dev\\bbcare"
42
49
  })
43
50
  ```
44
51
 
45
- 或人工在会话里发:
52
+ 或人工在会话里发(前导 flag 可省略、顺序不限):
46
53
 
47
54
  ```
48
- @spawn 读取 handoff/migration-3.md 后继续迁移任务
55
+ @spawn --agent leader --title "[P1-10] sprint" 读取 handoff/migration-3.md 后继续迁移任务
49
56
  ```
50
57
 
51
58
  ## 开发与测试
52
59
 
53
60
  ```bash
54
61
  npm install # 安装 devDependency @opencode-ai/plugin(唯一依赖,仅测试用)
55
- node test/test.mjs # 12 用例 / 39 断言
62
+ node test/test.mjs # 23 用例 / 62 断言
56
63
  ```
57
64
 
58
- - `test/test.mjs` 默认经相对路径 `../index.js` 测项目版;环境变量 `RELAY_MAIN` 覆盖为其他路径(如生产活实例)。生产版从 `.config/opencode/node_modules` 解析 `@opencode-ai/plugin`。
65
+ - `test/test.mjs` 默认经相对路径 `../index.js` 测项目版;环境变量 `RELAY_MAIN` 覆盖为其他路径(tarball 解包版 / 包缓存活实例)。`RELAY_MAIN` 目标须能向上解析到 `@opencode-ai/plugin`(解包放项目内即可)。
59
66
  - 以尾部 `==== 结果: N 通过, 0 失败 ====` 为准。
60
67
 
61
68
  ## 发布
62
69
 
63
- `index.js` + `package.json` 成对同步生产 → `RELAY_MAIN=<生产路径> node test/test.mjs` 回归全绿 → 重启桌面版实测 → commit → push → `npm publish`(不打 git tag;发布前 `npm whoami` 确认登录态,凭据不入库)。详见 `AGENTS.md`。
70
+ bump 版本号(`package.json` 与 `index.js` 头注释同步)→ `node test/test.mjs` 全绿 → `npm pack` 解包核对哈希 + `RELAY_MAIN` 回归全绿 → commit → push → `npm publish`(不打 git tag;先 `npm whoami` 确认登录态,凭据不入库)→ 重启桌面版实测 `@spawn` / `spawn_session`;实测不过修复补丁重发。详见 `AGENTS.md`。
64
71
 
65
72
  ## 历史
66
73
 
package/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { tool } from "@opencode-ai/plugin/tool";
2
2
 
3
3
  /**
4
- * session-spawn —— 会话生成原语(v2.0.0,2026-09-12 卍解简化定稿)
4
+ * session-spawn —— 会话生成原语(v2.1.1,2026-09-12 起支持 agent/directory)
5
5
  *
6
6
  * 只做一件事:创建一个新的顶层会话,并把起始语句注入为该会话首条消息。
7
- * create: client.session.create({ body: { title } })
8
- * inject: client.session.promptAsync({ path: { id }, body: { parts: [{ type: "text", text }] } })
7
+ * create: client.session.create({ body: { title }, query: { directory } })
8
+ * inject: client.session.promptAsync({ path: { id }, body: { agent, parts: [{ type: "text", text }] } })
9
+ * (agent / directory 均可选,仅在显式传入且非空白时透传;不做校验,交服务端)
9
10
  *
10
11
  * 设计约束(用户定稿):
11
12
  * - 插件只负责「建会话 + 注入语句」,不落文书、不读文书、不追踪链、无状态文件。
@@ -36,6 +37,42 @@ function userText(parts) {
36
37
  .join(" ");
37
38
  }
38
39
 
40
+ // 可选参数透传判定:空串 / 纯空白视为未传(返回 undefined)。
41
+ function nonEmpty(v) {
42
+ return typeof v === "string" && v.trim() !== "" ? v : undefined;
43
+ }
44
+
45
+ const SPAWN_USAGE = "用法:@spawn [--agent <名称>] [--title <标题>] <起始语句>";
46
+
47
+ // 解析人工命令:@spawn / @relay spawn,支持前导 --agent / --title。
48
+ // 返回 null(非 spawn 命令)或 { prompt, agent, title } / { error, flag?, agent?, title? }。
49
+ // 规则:仅解析命令后前导的 flag;值均为单个空白分隔 token;剩余内容原样作为起始语句。
50
+ function parseSpawnCommand(text) {
51
+ const m = /^\s*@(?:relay\s+)?spawn\b([\s\S]*)$/i.exec(String(text || ""));
52
+ if (!m) return null;
53
+
54
+ let rest = m[1];
55
+ let agent;
56
+ let title;
57
+ while (true) {
58
+ const fm = /^\s*(--agent|--title)(?=\s|$)/.exec(rest);
59
+ if (!fm) break;
60
+ rest = rest.slice(fm[0].length);
61
+ const vm = /^\s+(\S+)/.exec(rest);
62
+ const value = vm && vm[1];
63
+ if (!value || value.startsWith("--")) {
64
+ return { error: "missing-value", flag: fm[1], agent, title };
65
+ }
66
+ rest = rest.slice(vm[0].length);
67
+ if (fm[1] === "--agent") agent = value;
68
+ else title = value;
69
+ }
70
+
71
+ const prompt = rest.trim();
72
+ if (!prompt) return { error: "empty-prompt", agent, title };
73
+ return { prompt, agent, title };
74
+ }
75
+
39
76
  export const SessionSpawn = async (ctx) => {
40
77
  const { client } = ctx;
41
78
 
@@ -50,34 +87,42 @@ export const SessionSpawn = async (ctx) => {
50
87
  };
51
88
 
52
89
  // 核心:建新会话 + 注入起始语句。
53
- // 返回 { id, title, injected, error? };create 失败则抛出(未建会话)。
54
- async function spawn(prompt, title) {
90
+ // opts.agent / opts.directory 可选;仅在非空白时透传(不做校验,交服务端)。
91
+ // 返回 { id, title, injected, agent?, directory?, error? };create 失败则抛出(未建会话)。
92
+ async function spawn(prompt, title, opts = {}) {
55
93
  const text = typeof prompt === "string" ? prompt : "";
56
94
  if (!text.trim()) throw new Error("起始语句为空");
57
95
  if (!client || !client.session || typeof client.session.create !== "function") {
58
96
  throw new Error("client.session.create 不可用");
59
97
  }
60
98
  const t = (title && String(title).trim()) || defaultTitle(text);
99
+ const agent = nonEmpty(opts.agent);
100
+ const directory = nonEmpty(opts.directory);
61
101
 
62
- const created = await client.session.create({ body: { title: t } });
102
+ const createReq = { body: { title: t } };
103
+ if (directory !== undefined) createReq.query = { directory };
104
+ const created = await client.session.create(createReq);
63
105
  const id = created && (created.data?.id || created.id);
64
106
  if (!id) throw new Error("create 未返回会话 id");
65
107
 
66
108
  const body = { parts: [{ type: "text", text }] };
109
+ if (agent !== undefined) body.agent = agent;
67
110
  const injectFn =
68
111
  typeof client.session.promptAsync === "function"
69
112
  ? client.session.promptAsync.bind(client.session)
70
113
  : typeof client.session.prompt === "function"
71
114
  ? client.session.prompt.bind(client.session)
72
115
  : null;
73
- if (!injectFn) return { id, title: t, injected: false, error: "client.session.promptAsync/prompt 均不可用" };
116
+ if (!injectFn) {
117
+ return { id, title: t, injected: false, agent, directory, error: "client.session.promptAsync/prompt 均不可用" };
118
+ }
74
119
 
75
120
  try {
76
121
  await injectFn({ path: { id }, body });
77
- return { id, title: t, injected: true };
122
+ return { id, title: t, injected: true, agent, directory };
78
123
  } catch (e) {
79
124
  console.error(`[session-spawn] 注入失败(会话 ${id} 已建保留): ${e.message}`);
80
- return { id, title: t, injected: false, error: e.message };
125
+ return { id, title: t, injected: false, agent, directory, error: e.message };
81
126
  }
82
127
  }
83
128
 
@@ -86,14 +131,16 @@ export const SessionSpawn = async (ctx) => {
86
131
  tool: {
87
132
  spawn_session: tool({
88
133
  description:
89
- "创建一个新的顶层会话并向其注入起始语句,用于无人值守长任务编排:当前会话模型在需要时自行决定把工作交给一个新会话继续。新会话会立即开始执行该起始语句。若需让其读取某份交接文书,请把文书路径与读取要求写进 prompt。",
134
+ "创建一个新的顶层会话并向其注入起始语句,用于无人值守长任务编排:当前会话模型在需要时自行决定把工作交给一个新会话继续。新会话会立即开始执行该起始语句。若需让其读取某份交接文书,请把文书路径与读取要求写进 prompt。agent 可指定新会话以某 agent/模式启动(如 leader);directory 可绑定项目目录。",
90
135
  args: {
91
136
  prompt: tool.schema.string().describe("注入新会话的起始语句(可内含读取交接文书路径等引导)"),
92
137
  title: tool.schema.string().optional().describe("新会话标题;缺省取起始语句首行"),
138
+ agent: tool.schema.string().optional().describe("新会话以某 agent/模式启动(如 leader);缺省由服务端默认"),
139
+ directory: tool.schema.string().optional().describe("新会话绑定的项目目录(绝对路径,如 D:\\dev\\bbcare);缺省由服务端默认"),
93
140
  },
94
141
  async execute(args) {
95
142
  try {
96
- const r = await spawn(args.prompt, args.title);
143
+ const r = await spawn(args.prompt, args.title, { agent: args.agent, directory: args.directory });
97
144
  if (!r.injected) {
98
145
  return `会话「${r.title}」(id=${r.id}) 已创建,但起始语句注入失败:${r.error}。请手动打开该会话补发指令。`;
99
146
  }
@@ -105,19 +152,22 @@ export const SessionSpawn = async (ctx) => {
105
152
  }),
106
153
  },
107
154
 
108
- // 人工命令:@spawn <起始语句>(兼容 @relay spawn)。
155
+ // 人工命令:@spawn [--agent <名称>] [--title <标题>] <起始语句>(兼容 @relay spawn)。
109
156
  "chat.message": async ({ sessionID }, { parts }) => {
110
157
  try {
111
158
  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");
159
+ const parsed = parseSpawnCommand(text);
160
+ if (!parsed) return;
161
+ if (parsed.error === "missing-value") {
162
+ await notify("spawn", `${parsed.flag} 缺少值。${SPAWN_USAGE}`, "warning");
163
+ return;
164
+ }
165
+ if (parsed.error === "empty-prompt") {
166
+ await notify("spawn", SPAWN_USAGE, "warning");
117
167
  return;
118
168
  }
119
169
  try {
120
- const r = await spawn(prompt);
170
+ const r = await spawn(parsed.prompt, parsed.title, { agent: parsed.agent });
121
171
  if (!r.injected) {
122
172
  await notify("spawn", `会话「${r.title}」(id=${r.id}) 已创建,但注入失败:${r.error}`, "warning");
123
173
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-session-spawn",
3
- "version": "2.0.1",
3
+ "version": "2.1.1",
4
4
  "type": "module",
5
5
  "description": "opencode 会话生成原语:创建新顶层会话并注入起始语句,用于无人值守长任务编排(工具 spawn_session + 命令 @spawn)。",
6
6
  "main": "index.js",