Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions external_plugins/zcode-grokbot-dispatch/.mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"mcpServers": {
"dispatch": {
"type": "stdio",
"command": "node",
"args": ["${ZCODE_PLUGIN_ROOT}/mcp/server.mjs"],
"timeoutMs": 60000
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "zcode-grokbot-dispatch",
"version": "0.3.0",
"description": "Grok Bot Dispatch for ZCode: bidirectional task dispatch between ZCode desktop and Grok Bot / DSH ([Project: X] routing, persistent session<->job mapping, automatic result reporting back to your Grok Bot peer: direct HTTP by default, Yobiru optional).",
"description_i18n": {
"en": "Grok Bot Dispatch for ZCode: bidirectional task dispatch between ZCode desktop and Grok Bot / DSH ([Project: X] routing, persistent session<->job mapping, automatic result reporting back to your Grok Bot peer: direct HTTP by default, Yobiru optional).",
"zh-CN": "Grok Bot 派活插件(ZCode 版):ZCode 桌面版与 Grok Bot / DSH 双向派活([Project: X] 路由、会话↔job 持久映射、任务结果自动回报给 Grok Bot 对端,默认 HTTP 直连,Yobiru 可选)。"
},
"author": {
"name": "semantic-craft",
"url": "https://github.com/semantic-craft/zcode-dispatch"
},
"homepage": "https://github.com/semantic-craft/zcode-dispatch",
"repository": "https://github.com/semantic-craft/zcode-dispatch",
"license": "MIT",
"keywords": [
"grok-bot",
"grokbot",
"dispatch",
"dsh",
"yobiru",
"hooks",
"mcp"
],
"displayName": "Grok Bot Dispatch for ZCode"
}
21 changes: 21 additions & 0 deletions external_plugins/zcode-grokbot-dispatch/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 semantic-craft

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
34 changes: 34 additions & 0 deletions external_plugins/zcode-grokbot-dispatch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Grok Bot Dispatch for ZCode (zcode-grokbot-dispatch)

> Plugin id: `zcode-grokbot-dispatch` (renamed from the pre-release id `zcode-dispatch`). Runtime paths are unchanged (`~/.config/zcode-dispatch/`, `~/.local/state/zcode-dispatch/`, `ZCODE_DISPATCH_*` env), so existing config keeps working; if you installed the old `zcode-dispatch` build, uninstall it and install `zcode-grokbot-dispatch`.


Bidirectional task dispatch between the ZCode desktop app and DSH (DeepSeek Harness) / Grok Bot, with `[Project: X]` routing, persistent session↔job mapping and automatic result reporting (direct HTTP by default, Yobiru optional).

## Components
- **Commands**: `/dispatch-dsh <project> <task>`, `/dispatch-grok <project> <task>`, `/dispatch-status <id>`
- **MCP server** (stdio, `node mcp/server.mjs`): tools `dispatch_to_dsh`, `dispatch_to_grokbot`, `dispatch_status`
- **Hooks**: `UserPromptSubmit` (records session↔job mapping) and `Stop` (reports the finished session's result to the dispatching peer)

## Install
1. ZCode → Settings → Plugins → `zcode-plugins-official` → install **zcode-grokbot-dispatch**.
2. Requires Node.js ≥ 22.5 on PATH. `/dispatch-dsh` also requires the `dsh` CLI (override with `DSH_BIN`).
3. Configure peers in `~/.config/zcode-dispatch/config.json` or env vars (env wins), e.g.:
```json
{ "transport": "direct", "peers": { "dsh": { "url": "http://<peer-host>:<port>", "keyFile": "~/.config/zcode-dispatch/dsh.key" } } }
```
Relevant env: `ZCODE_DISPATCH_CONFIG`, `ZCODE_DISPATCH_TRANSPORT` (direct|yobiru), `ZCODE_DISPATCH_GROK_PEER`, `ZCODE_DISPATCH_PROJECTS_ROOT` (default `~/Projects`), `ZCODE_DISPATCH_DISCOVERY=0` (disable Tailscale discovery), `DSH_BIN`, `DSH_PROFILE`.
No endpoint is built in; nothing is sent until you configure a peer.

## Side effects / privacy
- **Network**: the `Stop` hook and the dispatch tools send task text and up to ~3000 chars of the session's final result to the peer **you configured** — by HTTP (default, authenticated with your key file) or, if `transport` is `yobiru`, via the `yobiru` CLI (`yobiru send <peer> ...`). Sessions not started by a dispatch are not reported.
- **Commands executed**: `dsh --profile headless --json <task>` (for `/dispatch-dsh`), `yobiru` (only when transport is yobiru), and `tailscale status` for optional peer discovery (disable with `ZCODE_DISPATCH_DISCOVERY=0`).
- **Files written**: job/session mapping and reports under `~/.local/state/zcode-dispatch/` (and the plugin data dir), mode 0600/0700. Reads config/key files under `~/.config/zcode-dispatch/`.
- **Hooks**: runs on every `UserPromptSubmit` and `Stop` (timeouts 10s/20s); exits quietly when the session is not a dispatched job.
- No telemetry, no third-party services beyond the peer/Yobiru you configure.

## Third-party code
None bundled; uses only Node.js built-in modules.

## License
MIT © semantic-craft — source: https://github.com/semantic-craft/zcode-dispatch
34 changes: 34 additions & 0 deletions external_plugins/zcode-grokbot-dispatch/README_CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Grok Bot Dispatch for ZCode(zcode-grokbot-dispatch,Grok Bot 双向派活)

> 插件 id:`zcode-grokbot-dispatch`(由预发布 id `zcode-dispatch` 更名)。运行时路径不变(`~/.config/zcode-dispatch/`、`~/.local/state/zcode-dispatch/`、`ZCODE_DISPATCH_*` 环境变量),原配置继续可用;如装过旧的 `zcode-dispatch`,请先卸载再安装 `zcode-grokbot-dispatch`。


ZCode 桌面版与 DSH(DeepSeek Harness)/ Grok Bot 之间的双向派活插件:按 `[Project: X]` 路由、会话↔job 持久映射、任务完成后自动回报结果(默认 HTTP 直连,Yobiru 可选)。

## 组件
- **命令**:`/dispatch-dsh <项目> <任务>`、`/dispatch-grok <项目> <任务>`、`/dispatch-status <id>`
- **MCP 服务**(stdio,`node mcp/server.mjs`):工具 `dispatch_to_dsh`、`dispatch_to_grokbot`、`dispatch_status`
- **Hooks**:`UserPromptSubmit`(记录会话↔job 映射)、`Stop`(把派活会话的结果回报给派活方)

## 安装
1. ZCode → 设置 → Plugins → `zcode-plugins-official` → 安装 **zcode-grokbot-dispatch**。
2. 需要 PATH 中有 Node.js ≥ 22.5;`/dispatch-dsh` 还需要 `dsh` CLI(可用 `DSH_BIN` 指定)。
3. 在 `~/.config/zcode-dispatch/config.json` 或环境变量中配置对端(环境变量优先),例如:
```json
{ "transport": "direct", "peers": { "dsh": { "url": "http://<对端主机>:<端口>", "keyFile": "~/.config/zcode-dispatch/dsh.key" } } }
```
相关环境变量:`ZCODE_DISPATCH_CONFIG`、`ZCODE_DISPATCH_TRANSPORT`(direct|yobiru)、`ZCODE_DISPATCH_GROK_PEER`、`ZCODE_DISPATCH_PROJECTS_ROOT`(默认 `~/Projects`)、`ZCODE_DISPATCH_DISCOVERY=0`(关闭 Tailscale 发现)、`DSH_BIN`、`DSH_PROFILE`。
插件不内置任何 endpoint,未配置对端时不会对外发送任何内容。

## 副作用与隐私
- **网络**:`Stop` hook 与派活工具会把任务文本及会话最终结果(最多约 3000 字符)发送给**你自己配置的**对端——默认走 HTTP(用你的 key 文件鉴权);`transport` 设为 `yobiru` 时通过 `yobiru` CLI(`yobiru send <peer> ...`)发送。非派活产生的会话不会回报。
- **执行命令**:`dsh --profile headless --json <任务>`(`/dispatch-dsh`)、`yobiru`(仅 yobiru 模式)、`tailscale status`(可选的对端发现,`ZCODE_DISPATCH_DISCOVERY=0` 关闭)。
- **写入文件**:job/会话映射与回报记录写在 `~/.local/state/zcode-dispatch/`(及插件数据目录),权限 0600/0700;读取 `~/.config/zcode-dispatch/` 下的配置和 key。
- **Hooks**:每次 `UserPromptSubmit` 与 `Stop` 触发(超时 10s/20s);非派活会话直接静默退出。
- 无遥测;除你配置的对端/Yobiru 外不访问任何第三方服务。

## 第三方代码
无,仅使用 Node.js 内置模块。

## 许可证
MIT © semantic-craft —— 源码:https://github.com/semantic-craft/zcode-dispatch
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
description: 把任务派给 DSH(DeepSeek Harness,headless,按 [Project: X] 路由)
argument-hint: "<project> <任务描述>"
---
用 `dispatch_to_dsh` 工具把下面的任务派给 DSH。第一个参数是 ~/Projects 下的项目名($1),其余是任务正文;正文开头必须是 `[Project: $1]`(工具会自动补)。派完把返回的 job_id 告诉我,之后可用 `dispatch_status` 查结果;不要自己去做这个任务。

参数:$ARGUMENTS
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
description: 把任务派给 Grok Bot(经 Yobiru,按 [Project: X] 路由)
argument-hint: "<project> <任务描述>"
---
用 `dispatch_to_grokbot` 工具把下面的任务派给 Grok Bot。第一个参数是 ~/Projects 下的项目名($1),其余是任务正文;正文开头必须是 `[Project: $1]`(工具会自动补)。派完把返回的 request_id 告诉我,不要自己去做这个任务。

参数:$ARGUMENTS
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
description: 查询派出去的任务状态(DSH job id 或 Yobiru request id)
argument-hint: "<id>"
---
用 `dispatch_status` 工具查询 id `$1` 的状态,并简要汇报 state 和结果。
31 changes: 31 additions & 0 deletions external_plugins/zcode-grokbot-dispatch/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"description": "zcode-dispatch: bind session_id <-> job_id on UserPromptSubmit and auto-report dispatched-session results via Yobiru on Stop",
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "process",
"command": "node",
"args": ["${ZCODE_PLUGIN_ROOT}/hooks/stop-report.mjs"],
"timeoutMs": 10000,
"statusMessage": "zcode-dispatch: 记录会话↔job 映射…"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "process",
"command": "node",
"args": ["${ZCODE_PLUGIN_ROOT}/hooks/stop-report.mjs"],
"timeoutMs": 20000,
"statusMessage": "zcode-dispatch: 自动回报任务结果…"
}
]
}
]
}
}
116 changes: 116 additions & 0 deletions external_plugins/zcode-grokbot-dispatch/hooks/stop-report.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Hook handler for UserPromptSubmit and Stop.
*
* - On UserPromptSubmit: extracts `[job: <16-hex>]` from prompt and records `session_id <-> job_id`
* in the persistent mapping (`session-jobs.json` in ~/.local/state/zcode-dispatch and ZCODE_PLUGIN_DATA).
* - On Stop: resolves `job_id` for `session_id` (via persistent mapping, env, transcript, or tasks-index.sqlite),
* writes `~/.local/state/zcode-dispatch/reports/<job>.json` + `reports.jsonl`, and automatically reports the
* result back to the dispatcher peer via Yobiru (`[ZCode report <job>] ...`) when transport is "yobiru".
* With the default direct transport the receiver pushes results itself (HTTP callback), so this hook only records.
* - Never blocks ZCode: stdout stays empty, exit 0.
*/
import fs from 'node:fs';
import path from 'node:path';
import { resolveNetConfig } from '../lib/netconfig.mjs';
import { JOB_ID_RE, defaultStateDir, isGrokDispatched, resolveJob, upsertJob } from '../lib/job-map.mjs';
import { runYobiru, transportName } from '../lib/transport.mjs';

let raw = '';
process.stdin.setEncoding('utf8');
for await (const c of process.stdin) raw += c;

let input = {};
try { input = JSON.parse(raw); } catch {}

const event = input.hook_event_name || input.hookEventName || 'Stop';
const sessionId = input.session_id ?? input.sessionId ?? process.env.ZCODE_SESSION_ID ?? null;
const jobInfo = resolveJob({
sessionId,
prompt: input.prompt,
transcriptPath: input.transcript_path ?? input.transcriptPath,
cwd: input.cwd,
env: process.env,
});

if (!jobInfo || !JOB_ID_RE.test(jobInfo.job_id || '')) process.exit(0);

if (event === 'UserPromptSubmit' || event === 'SessionStart') {
process.stderr.write(`[zcode-dispatch] mapped session ${sessionId} <-> job ${jobInfo.job_id}\n`);
process.exit(0);
}

const job = jobInfo.job_id;
const text = String(input.last_assistant_message ?? input.responseText ?? '').slice(0, 20000);
const notify = process.env.ZCODE_DISPATCH_NOTIFY ?? jobInfo.notify ?? null;
// Yobiru-sourced jobs are answered by the adapter via `request respond`; a Stop-hook `send` would
// duplicate the result even when the request came from Grok.
const viaAdapter = (jobInfo.source || process.env.ZCODE_DISPATCH_SOURCE) === 'yobiru';
let viaYobiru = false; try { viaYobiru = transportName() === 'yobiru'; } catch {}
const shouldNotifyGrok = viaYobiru && !viaAdapter && isGrokDispatched(jobInfo, process.env);
const peer = process.env.ZCODE_DISPATCH_PEER || process.env.ZCODE_DISPATCH_GROK_PEER || jobInfo.notify_peer || resolveNetConfig().grokPeer;

const rep = {
agent: 'zcode',
job_id: job,
source: jobInfo.source || process.env.ZCODE_DISPATCH_SOURCE || null,
project: jobInfo.project || null,
session_id: sessionId || jobInfo.session_id || null,
automation_id: jobInfo.automation_id || null,
cwd: input.cwd ?? jobInfo.cwd ?? null,
at: new Date().toISOString(),
text,
yobiru_reported: false,
yobiru_peer: null,
yobiru_receipt: null,
};

// Report once per dispatched job, and only when the job mapping indicates Grok dispatched it
// (inbound Yobiru from grok, or source==='grok'/'grok-bot' / notify==='grok' from Grok inbound).
// dsh, zcode, and local/manual sessions never notify Grok; ZCODE_DISPATCH_NOTIFY=none disables all.
const alreadyReported = Boolean(jobInfo.yobiru_reported);
if (!viaYobiru) {
rep.yobiru_skipped = 'transport-direct';
} else if (viaAdapter) {
rep.yobiru_skipped = 'adapter-responds';
} else if (!shouldNotifyGrok) {
rep.yobiru_skipped = notify === 'none' || notify === '0' || notify === 'false' ? 'notify-disabled' : 'non-grok-source';
}
if (shouldNotifyGrok && !peer) rep.yobiru_skipped = 'grok-peer-not-configured';
if (shouldNotifyGrok && peer && !alreadyReported) {
try {
const out = runYobiru(['send', peer, `[ZCode report ${job}] ${text.slice(0, 3000)}`, '--as', 'zcode', '--wake', 'none'], process.env);
rep.yobiru_reported = true;
rep.yobiru_peer = peer;
rep.yobiru_receipt = String(out || '').trim().slice(0, 500) || 'sent';
} catch (e) {
rep.yobiru_error = String(e.message || e).slice(0, 500);
process.stderr.write(`[zcode-dispatch] yobiru notify failed for job ${job}: ${e.message}\n`);
}
}

upsertJob({
job_id: job,
session_id: rep.session_id || undefined,
cwd: rep.cwd || undefined,
reported_at: rep.at,
last_reported_text: text,
yobiru_reported: rep.yobiru_reported || Boolean(jobInfo.yobiru_reported),
yobiru_peer: rep.yobiru_peer || jobInfo.yobiru_peer || undefined,
yobiru_receipt: rep.yobiru_receipt || jobInfo.yobiru_receipt || undefined,
report_count: (jobInfo.report_count || 0) + 1,
});

const writeReportDir = (baseDir) => {
const dir = path.join(baseDir, 'reports');
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
fs.writeFileSync(path.join(dir, `${job}.json`), JSON.stringify(rep, null, 2), { mode: 0o600 });
fs.appendFileSync(path.join(dir, 'reports.jsonl'), JSON.stringify(rep) + '\n', { mode: 0o600 });
};

const stateDir = defaultStateDir(process.env);
writeReportDir(stateDir);
if (process.env.ZCODE_PLUGIN_DATA && path.resolve(process.env.ZCODE_PLUGIN_DATA) !== path.resolve(stateDir)) {
try { writeReportDir(process.env.ZCODE_PLUGIN_DATA); } catch {}
}

process.stderr.write(`[zcode-dispatch] report written for job ${job} (session=${rep.session_id || 'none'}, yobiru=${rep.yobiru_reported})\n`);
Loading