@mingmaple/dsh-handover 0.9.22 → 1.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.
- package/README.md +117 -113
- package/docs/migrate.md +52 -41
- package/docs/releases/0.9.12.md +2 -3
- package/docs/releases/0.9.14.md +1 -5
- package/docs/releases/0.9.22.md +1 -1
- package/docs/releases/1.0.0.md +33 -0
- package/docs/troubleshoot.md +221 -61
- package/package.json +5 -2
- package/scripts/behavior.mjs +398 -0
- package/scripts/blank-dsh.mjs +12 -2
- package/scripts/blank-smoke.mjs +95 -44
- package/scripts/check-docs.mjs +7 -0
- package/scripts/portability.mjs +71 -0
- package/scripts/style-gate.mjs +571 -0
- package/src/client.js +1028 -994
- package/src/index.mjs +273 -109
- package/src/lib/absrefs.mjs +1 -1
- package/src/lib/api.mjs +9 -3
- package/src/lib/archive.mjs +1 -1
- package/src/lib/brief.mjs +8 -6
- package/src/lib/config.mjs +21 -6
- package/src/lib/docmap.mjs +5 -8
- package/src/lib/drift.mjs +1 -1
- package/src/lib/events.mjs +1 -1
- package/src/lib/facts.mjs +19 -12
- package/src/lib/gates.mjs +12 -12
- package/src/lib/global-task.mjs +69 -0
- package/src/lib/guide.mjs +80 -75
- package/src/lib/handover-io.mjs +69 -6
- package/src/lib/identity.mjs +330 -0
- package/src/lib/legacy.mjs +89 -0
- package/src/lib/lint.mjs +14 -11
- package/src/lib/manual.mjs +117 -80
- package/src/lib/mover.mjs +18 -14
- package/src/lib/office.mjs +42 -2
- package/src/lib/pack-ops.mjs +10 -10
- package/src/lib/pathkey.mjs +24 -0
- package/src/lib/phase.mjs +8 -13
- package/src/lib/profile.mjs +1 -1
- package/src/lib/reminder.mjs +110 -0
- package/src/lib/role.mjs +1 -1
- package/src/lib/scaffold.mjs +30 -10
- package/src/lib/schedule.mjs +5 -5
- package/src/lib/session-create.mjs +2 -2
- package/src/lib/session-ops.mjs +76 -47
- package/src/lib/sessions.mjs +40 -2
- package/src/lib/sevenzip.mjs +10 -1
- package/src/lib/status.mjs +337 -336
- package/src/lib/succession.mjs +14 -14
- package/src/lib/system.mjs +364 -0
- package/src/templates/Changelog.skeleton.md +3 -3
- package/src/templates/Charter.skeleton.md +13 -9
- package/src/templates/Handover.global.skeleton.md +20 -13
- package/src/templates/Handover.template.md +89 -0
- package/src/templates/Plan.skeleton.md +34 -0
- package/src/templates/Prompt_Templates.md +91 -10
- package/src/templates/Runtime_Manual.md +254 -139
- package/src/templates/System.skeleton.md +31 -0
- package/src/templates/Systems.global.skeleton.md +13 -0
- package/src/templates/rules/Delegation_Rules.md +45 -0
- package/src/templates/rules/Handover_Rules.md +46 -24
- package/src/templates/rules/Migration_Rules.md +7 -7
- package/src/templates/rules/Misc_Task_Standard.md +17 -10
- package/src/templates/rules/Writing_Rules.md +33 -16
- package/src/templates/standards/README.md +19 -7
- package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203/README.md +120 -0
- package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203//344/276/235/346/215/256_/350/256/272/346/226/207.md +389 -0
- package/src/tools/ho_backup.mjs +8 -8
- package/src/tools/ho_changelog.mjs +4 -4
- package/src/tools/ho_charter.mjs +7 -6
- package/src/tools/ho_checkpoint.mjs +34 -31
- package/src/tools/ho_docmap.mjs +135 -49
- package/src/tools/ho_gate.mjs +9 -4
- package/src/tools/ho_handover.mjs +61 -35
- package/src/tools/ho_office.mjs +45 -4
- package/src/tools/ho_profile.mjs +3 -3
- package/src/tools/ho_sessions.mjs +43 -22
- package/src/tools/ho_status.mjs +14 -9
- package/src/tools/ho_succeed.mjs +3 -3
- package/src/tools/ho_workspace.mjs +83 -51
- package/src/tools/peer.mjs +219 -8
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
/** ho_workspace —— 工作区生命周期:list / create / delete / init
|
|
1
|
+
/** ho_workspace —— 工作区生命周期:list / create / delete / init / scaffold / migrate(旧体系迁移)+ 全局根目录迁移。 */
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { nameOf } from '../lib/names.mjs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { pathKey } from '../lib/pathkey.mjs';
|
|
5
6
|
import { textOutput } from '../lib/output.mjs';
|
|
6
7
|
import { NL } from '../lib/text.mjs';
|
|
7
8
|
import { workspaceRows } from '../lib/registry.mjs';
|
|
8
9
|
import { createGlobalWorkspace, ensureScaffold } from '../lib/scaffold.mjs';
|
|
10
|
+
import { legacyInfo, migrateLegacyDocmaps } from '../lib/legacy.mjs';
|
|
11
|
+
import { syncGlobalRefs } from '../lib/system.mjs';
|
|
9
12
|
import { probeMove, startMove, moveStatus, cleanupOld, stageSwitch, repairLink, stageRetarget } from '../lib/mover.mjs';
|
|
10
13
|
import { currentRoot } from '../lib/api.mjs';
|
|
11
14
|
|
|
12
|
-
const BAD = '
|
|
15
|
+
const BAD = '不能删除「全局」工作区:该工作区为管理会话所在工作区。';
|
|
13
16
|
|
|
14
17
|
function gb(n) {
|
|
15
18
|
const v = Number(n) || 0;
|
|
@@ -29,7 +32,7 @@ function renderProbe(p) {
|
|
|
29
32
|
if (p.freeBytes !== null && p.freeBytes !== undefined) out.push(' 目标盘可用 ' + gb(p.freeBytes));
|
|
30
33
|
for (const w of p.warnings || []) out.push(' 注意:' + w);
|
|
31
34
|
for (const x of p.problems || []) out.push(' 未通过:' + x);
|
|
32
|
-
if (p.ok) out.push('
|
|
35
|
+
if (p.ok) out.push('提示:复制完成后生成切换脚本;切换步骤须完全退出 DSH 后再执行(源目录被占用)。');
|
|
33
36
|
return out.join(NL);
|
|
34
37
|
}
|
|
35
38
|
|
|
@@ -37,7 +40,7 @@ export function pmWorkspaceTool(env) {
|
|
|
37
40
|
return {
|
|
38
41
|
name: nameOf('workspace'),
|
|
39
42
|
description:
|
|
40
|
-
'工作区:init
|
|
43
|
+
'工作区:init 首次建立全局体系、scaffold 补齐缺失目录与说明、migrate 旧体系迁移、list、create、delete、move-* 迁移整个全局根目录。删除只删注册。',
|
|
41
44
|
parameters: {
|
|
42
45
|
type: 'object',
|
|
43
46
|
additionalProperties: false,
|
|
@@ -45,18 +48,18 @@ export function pmWorkspaceTool(env) {
|
|
|
45
48
|
properties: {
|
|
46
49
|
action: {
|
|
47
50
|
type: 'string',
|
|
48
|
-
enum: ['init', 'scaffold', 'list', 'create', 'delete', 'move-probe', 'move', 'move-switch', 'move-status', 'move-retarget', 'move-repair', 'move-cleanup'],
|
|
49
|
-
description: 'init / scaffold / list / create / delete / move-probe / move / move-switch / move-status / move-retarget / move-repair / move-cleanup',
|
|
51
|
+
enum: ['init', 'scaffold', 'migrate', 'list', 'create', 'delete', 'move-probe', 'move', 'move-switch', 'move-status', 'move-retarget', 'move-repair', 'move-cleanup'],
|
|
52
|
+
description: 'init / scaffold / migrate / list / create / delete / move-probe / move / move-switch / move-status / move-retarget / move-repair / move-cleanup',
|
|
50
53
|
},
|
|
51
|
-
parent: { type: 'string', description: 'init
|
|
52
|
-
name: { type: 'string', description: 'init
|
|
53
|
-
register: { type: 'boolean', description: 'init
|
|
54
|
-
path: { type: 'string', description: 'create
|
|
55
|
-
title: { type: 'string', description: 'create、init 用:工作区显示标题(init
|
|
56
|
-
ensure: { type: 'boolean', description: 'create
|
|
57
|
-
workspace: { type: 'string', description: 'delete 用:工作区 id
|
|
58
|
-
to: { type: 'string', description: 'move
|
|
59
|
-
confirm: { type: 'boolean', description: 'move-cleanup
|
|
54
|
+
parent: { type: 'string', description: 'init 用:目标父目录的绝对路径(不存在时创建);全局根建于其中' },
|
|
55
|
+
name: { type: 'string', description: 'init 用:全局根目录名,缺省 _Workspace' },
|
|
56
|
+
register: { type: 'boolean', description: 'init 用:建立后一并注册为工作区(缺省 false)' },
|
|
57
|
+
path: { type: 'string', description: 'create 用:目录绝对路径;缺省要求已存在,ensure=true 时自动创建' },
|
|
58
|
+
title: { type: 'string', description: 'create、init 用:工作区显示标题(init 缺省「全局」)' },
|
|
59
|
+
ensure: { type: 'boolean', description: 'create 用:目录不存在时自动创建(缺省 false,不代建目录)' },
|
|
60
|
+
workspace: { type: 'string', description: 'delete 用:工作区 id、标题或路径' },
|
|
61
|
+
to: { type: 'string', description: 'move 用:新根绝对路径(父目录可自动创建)' },
|
|
62
|
+
confirm: { type: 'boolean', description: 'move-cleanup 用:为 true 才实际删除旧备份' },
|
|
60
63
|
},
|
|
61
64
|
},
|
|
62
65
|
output: textOutput(),
|
|
@@ -73,20 +76,20 @@ export function pmWorkspaceTool(env) {
|
|
|
73
76
|
out.push('未建立:' + r.error);
|
|
74
77
|
if (r.root) out.push('目标:' + r.root);
|
|
75
78
|
out.push('');
|
|
76
|
-
out.push('用法:ho_workspace(action="init", parent="<已存在的目录绝对路径>")
|
|
79
|
+
out.push('用法:ho_workspace(action="init", parent="<已存在的目录绝对路径>")。插件仅新建,不覆盖。');
|
|
77
80
|
return { text: out.join(NL) };
|
|
78
81
|
}
|
|
79
82
|
out.push('已建立全局文件体系:' + r.root);
|
|
80
83
|
out.push(' 目录 ' + r.created.length + ' 个:' + r.created.slice(0, 5).join('、') + (r.created.length > 5 ? ' 等' : ''));
|
|
81
|
-
out.push(' 文件 ' + r.written.length + ' 个:' + (r.written.length ? r.written.join('、') : '
|
|
82
|
-
if (r.existed) out.push('
|
|
83
|
-
if (r.createdParent) out.push('
|
|
84
|
+
out.push(' 文件 ' + r.written.length + ' 个:' + (r.written.length ? r.written.join('、') : '(均已存在,未覆盖)'));
|
|
85
|
+
if (r.existed) out.push(' 目标原为空目录,直接使用。');
|
|
86
|
+
if (r.createdParent) out.push(' 父目录原不存在,已创建。');
|
|
84
87
|
if (r.skipped.length) out.push(' 跳过(已存在、未覆盖):' + r.skipped.join('、'));
|
|
85
88
|
let registered = null;
|
|
86
89
|
if (args.register === true) {
|
|
87
90
|
if (!reg || typeof reg.create !== 'function') {
|
|
88
91
|
out.push('');
|
|
89
|
-
out.push('注册未完成:workspaceRegistry
|
|
92
|
+
out.push('注册未完成:workspaceRegistry 不可用。请将下列路径粘贴到 DSH 的新建工作区窗口:');
|
|
90
93
|
out.push(' ' + r.root);
|
|
91
94
|
return { text: out.join(NL) };
|
|
92
95
|
}
|
|
@@ -99,7 +102,7 @@ export function pmWorkspaceTool(env) {
|
|
|
99
102
|
} catch (e) {
|
|
100
103
|
out.push('');
|
|
101
104
|
out.push('注册失败:' + String(e && e.message ? e.message : e));
|
|
102
|
-
out.push('
|
|
105
|
+
out.push('可将下列路径粘贴到 DSH 的新建工作区窗口:');
|
|
103
106
|
out.push(' ' + r.root);
|
|
104
107
|
return { text: out.join(NL) };
|
|
105
108
|
}
|
|
@@ -107,12 +110,12 @@ export function pmWorkspaceTool(env) {
|
|
|
107
110
|
out.push('');
|
|
108
111
|
out.push('下一步:');
|
|
109
112
|
if (!registered) {
|
|
110
|
-
out.push(' 1. 注册为工作区:ho_workspace(action="create", path="' + r.root + '", title="全局")
|
|
111
|
-
out.push(' 2.
|
|
113
|
+
out.push(' 1. 注册为工作区:ho_workspace(action="create", path="' + r.root + '", title="全局"),或将该路径粘贴到 DSH 的新建工作区窗口;');
|
|
114
|
+
out.push(' 2. 新建一个标题含「全局」的会话,闸门、交接体检与会话水位即接入。');
|
|
112
115
|
} else {
|
|
113
|
-
out.push('
|
|
116
|
+
out.push(' 新建一个标题含「全局」的会话,闸门、交接体检与会话水位即接入。');
|
|
114
117
|
}
|
|
115
|
-
out.push('
|
|
118
|
+
out.push('(也可在设置页的 dsh-handover 中完成「首次安装」,效果相同。)');
|
|
116
119
|
return { text: out.join(NL) };
|
|
117
120
|
}
|
|
118
121
|
|
|
@@ -129,7 +132,36 @@ export function pmWorkspaceTool(env) {
|
|
|
129
132
|
out.push(' 新建目录 ' + r.created.length + ' 个:' + (r.created.length ? r.created.join('、') : '(无缺失)'));
|
|
130
133
|
out.push(' 新建文件 ' + r.written.length + ' 个:' + (r.written.length ? r.written.join('、') : '(无缺失)'));
|
|
131
134
|
out.push(' 已存在未覆盖 ' + r.skipped.length + ' 个' + (r.skipped.length ? ':' + r.skipped.join('、') : ''));
|
|
132
|
-
|
|
135
|
+
if (r.missingTemplates && r.missingTemplates.length) {
|
|
136
|
+
out.push(' 内置模板缺失 ' + r.missingTemplates.length + ' 个(未写空文件):' + r.missingTemplates.join('、'));
|
|
137
|
+
}
|
|
138
|
+
out.push('只新建,不覆盖、不删除:已有文件的内容未作任何改动。');
|
|
139
|
+
const legacy = legacyInfo(root);
|
|
140
|
+
if (legacy.exists) out.push('提示:检测到旧文档定位(' + legacy.dir + '),执行 ho_workspace(action="migrate") 并入体系注册。');
|
|
141
|
+
return { text: out.join(NL) };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (action === 'migrate') {
|
|
145
|
+
const root = currentRoot(env);
|
|
146
|
+
if (!root) throw new Error('未找到全局根(工作区未接入)');
|
|
147
|
+
const pre = legacyInfo(root);
|
|
148
|
+
if (!pre.exists) {
|
|
149
|
+
out.push('没有旧文档定位:' + pre.dir);
|
|
150
|
+
out.push('判定:无需迁移。');
|
|
151
|
+
return { text: out.join(NL) };
|
|
152
|
+
}
|
|
153
|
+
const r = migrateLegacyDocmaps(root);
|
|
154
|
+
out.push('旧文档定位迁移:' + (r.ok ? '完成' : '部分完成'));
|
|
155
|
+
out.push(' 已并入体系注册 ' + r.converted.length + ' 份' + (r.converted.length ? ':' + r.converted.join('、') : ''));
|
|
156
|
+
out.push(' 全局旧内容保留为工作区文档清单 ' + r.kept.length + ' 份' + (r.kept.length ? ':' + r.kept.join('、') : ''));
|
|
157
|
+
if (r.archived) out.push(' 旧目录已归档(未删除):' + r.archived);
|
|
158
|
+
for (const p of r.problems || []) out.push(' 未迁移:' + p);
|
|
159
|
+
try {
|
|
160
|
+
const g = syncGlobalRefs(root);
|
|
161
|
+
out.push(' 全局总注册已重建:' + g.rows.length + ' 个任务');
|
|
162
|
+
} catch (e) {
|
|
163
|
+
out.push(' 全局总注册重建失败:' + String(e && e.message ? e.message : e));
|
|
164
|
+
}
|
|
133
165
|
return { text: out.join(NL) };
|
|
134
166
|
}
|
|
135
167
|
|
|
@@ -147,7 +179,7 @@ export function pmWorkspaceTool(env) {
|
|
|
147
179
|
}
|
|
148
180
|
}
|
|
149
181
|
const rows = workspaceRows();
|
|
150
|
-
out.push('
|
|
182
|
+
out.push('工作区(退化读取 workspace.json,' + rows.length + ' 个)');
|
|
151
183
|
for (const r of rows) out.push(' ' + r.id + ' ' + r.title + ' ' + r.path + ' 会话 ' + r.sessions);
|
|
152
184
|
return { text: out.join(NL) };
|
|
153
185
|
}
|
|
@@ -161,13 +193,13 @@ export function pmWorkspaceTool(env) {
|
|
|
161
193
|
if ((!st || !st.isDirectory()) && args.ensure === true) {
|
|
162
194
|
try { fs.mkdirSync(p, { recursive: true }); st = fs.statSync(p); } catch (e) { throw new Error('建目录失败:' + p + '(' + String(e && e.message ? e.message : e) + ')'); }
|
|
163
195
|
}
|
|
164
|
-
if (!st || !st.isDirectory()) throw new Error('目录不存在或不是目录:' + p + '
|
|
196
|
+
if (!st || !st.isDirectory()) throw new Error('目录不存在或不是目录:' + p + '(缺省不代建目录;需要创建时传 ensure=true,或先行手工建好)');
|
|
165
197
|
const title = String(args.title || '').trim() || p.split(/[\\/]/).filter(Boolean).pop() || p;
|
|
166
198
|
let w = null;
|
|
167
199
|
try { w = await reg.create(p, title); } catch (e) { throw new Error('建工作区失败:' + String(e && e.message ? e.message : e)); }
|
|
168
200
|
out.push('已注册工作区 ' + String(w && w.id ? w.id : '?') + ' ' + String(w && w.title ? w.title : title));
|
|
169
201
|
out.push('路径 ' + String(w && w.path ? w.path : p));
|
|
170
|
-
out.push('
|
|
202
|
+
out.push('同一路径重复调用返回原记录,不改标题;新登记的工作区排在列表最前。');
|
|
171
203
|
return { text: out.join(NL) };
|
|
172
204
|
}
|
|
173
205
|
|
|
@@ -177,13 +209,13 @@ export function pmWorkspaceTool(env) {
|
|
|
177
209
|
if (!key) throw new Error('delete 需要 workspace(id、标题、路径)');
|
|
178
210
|
let list = [];
|
|
179
211
|
try { list = reg.list() || []; } catch { list = []; }
|
|
180
|
-
const norm = (s) => String(s || '')
|
|
212
|
+
const norm = (s) => pathKey(String(s || ''));
|
|
181
213
|
const hit = list.find((w) => String(w.id) === key || w.title === key || norm(w.path) === norm(key));
|
|
182
214
|
if (!hit) throw new Error('找不到工作区:' + key);
|
|
183
215
|
if (String(hit.title || '').indexOf('全局') !== -1) throw new Error(BAD);
|
|
184
216
|
const ok = await reg.delete(hit.id);
|
|
185
217
|
out.push((ok ? '已删除工作区注册 ' : '该工作区不存在(无操作)') + String(hit.id) + ' ' + String(hit.title || '-'));
|
|
186
|
-
out.push('
|
|
218
|
+
out.push('注意:只删注册;目录与全部会话日志均保留在原处。');
|
|
187
219
|
return { text: out.join(NL) };
|
|
188
220
|
}
|
|
189
221
|
|
|
@@ -200,11 +232,11 @@ export function pmWorkspaceTool(env) {
|
|
|
200
232
|
if (!p.ok) return { text: renderProbe(p) + NL + '未开始复制。' };
|
|
201
233
|
const r = await startMove({ src, to });
|
|
202
234
|
if (!r.ok) {
|
|
203
|
-
out.push('
|
|
235
|
+
out.push('复制完成,但校验存在差异,未生成切换脚本。');
|
|
204
236
|
out.push(' 文件 源 ' + r.report.files + ' / 目标 ' + r.report.copies);
|
|
205
237
|
out.push(' 缺失 ' + r.report.missingCount + ' 条' + (r.report.missing.length ? ':' + r.report.missing.join('、') : ''));
|
|
206
238
|
out.push(' 多出 ' + r.report.extraCount + ' 条' + (r.report.extra.length ? ':' + r.report.extra.join('、') : ''));
|
|
207
|
-
out.push('
|
|
239
|
+
out.push('处置:清空目标目录后重跑,或先停止正在写入文件的会话。');
|
|
208
240
|
return { text: out.join(NL) };
|
|
209
241
|
}
|
|
210
242
|
out.push('复制并校验通过:' + r.report.files + ' 文件 · ' + gb(r.report.bytes));
|
|
@@ -213,20 +245,20 @@ export function pmWorkspaceTool(env) {
|
|
|
213
245
|
out.push(' 切换助手 ' + r.marker.script.helper);
|
|
214
246
|
out.push(' 手工启动器(Windows、POSIX) ' + r.marker.script.cmd + ' 、 ' + r.marker.script.sh);
|
|
215
247
|
out.push('');
|
|
216
|
-
out.push('
|
|
248
|
+
out.push('下一步(3 步,顺序不可调换):');
|
|
217
249
|
out.push(' ① 完全退出 DSH(源目录被占用,改名会失败)');
|
|
218
|
-
out.push(' ② 执行 ho_workspace(action="move-switch")
|
|
250
|
+
out.push(' ② 执行 ho_workspace(action="move-switch") 派出后台助手(推荐用法);该助手会在 DSH 退出后自动完成');
|
|
219
251
|
out.push(' ③ 重新启动 DSH;确认无误后可在设置页删除旧备份');
|
|
220
252
|
return { text: out.join(NL) };
|
|
221
253
|
}
|
|
222
254
|
|
|
223
255
|
if (action === 'move-switch') {
|
|
224
256
|
const r = stageSwitch({ relaunch: args.relaunch === true });
|
|
225
|
-
if (r.already) { out.push('
|
|
257
|
+
if (r.already) { out.push('已处于切换态(原路径即链接),无需再次切换。'); return { text: out.join(NL) }; }
|
|
226
258
|
out.push('已派出后台切换助手(pid ' + r.pid + ')。');
|
|
227
|
-
out.push('
|
|
259
|
+
out.push(' 该助手会在完全退出 DSH 后自动完成:旧目录改名、原位建立链接、校验指向' + (r.relaunch ? ',然后重新启动 DSH' : '') + '。');
|
|
228
260
|
out.push(' 日志:' + r.log);
|
|
229
|
-
out.push('
|
|
261
|
+
out.push(' 提醒:此后不得修改该工作区内的文件(改动只会落在仍在使用的旧目录上)。');
|
|
230
262
|
return { text: out.join(NL) };
|
|
231
263
|
}
|
|
232
264
|
|
|
@@ -234,14 +266,14 @@ export function pmWorkspaceTool(env) {
|
|
|
234
266
|
const st = moveStatus(); // 以标记里的源路径为准:修改登记后当前根已是新位置,拿它比对会误报
|
|
235
267
|
const label = { none: '没有迁移记录', verified: '已复制并校验通过,待切换', switched: '已切换(原路径为链接)', partial: '复制有差异,未生成脚本' };
|
|
236
268
|
out.push('迁移状态:' + (label[st.phase] || st.phase));
|
|
237
|
-
out.push(' 源 ' + st.src + (st.linkTarget ? '
|
|
269
|
+
out.push(' 源 ' + st.src + (st.linkTarget ? '(当前为链接,指向 ' + st.linkTarget + ')' : ''));
|
|
238
270
|
if (st.dst) out.push(' 目标 ' + st.dst);
|
|
239
271
|
if (st.marker && st.marker.files) out.push(' 清单 ' + st.marker.files + ' 文件 · ' + (st.marker.digest || '').slice(0, 16) + '…');
|
|
240
272
|
if (st.retarget) {
|
|
241
273
|
if (st.retarget.needed) {
|
|
242
|
-
out.push(' DSH
|
|
274
|
+
out.push(' DSH 登记仍指向旧路径:' + st.retarget.total + ' 处');
|
|
243
275
|
for (const f of st.retarget.files) out.push(' ' + f.path + ' ×' + f.count);
|
|
244
|
-
out.push(' 修改登记:ho_workspace(action="move-retarget")(退出 DSH
|
|
276
|
+
out.push(' 修改登记:ho_workspace(action="move-retarget")(退出 DSH 后由助手改写,改写前先备份)');
|
|
245
277
|
} else {
|
|
246
278
|
out.push(' DSH 登记已在新位置(无需修改登记)');
|
|
247
279
|
}
|
|
@@ -250,7 +282,7 @@ export function pmWorkspaceTool(env) {
|
|
|
250
282
|
for (const o of st.olds) out.push(' 旧备份 ' + o.path + ' ' + gb(o.bytes));
|
|
251
283
|
out.push(' 删除:ho_workspace(action="move-cleanup", confirm=true)');
|
|
252
284
|
} else if (st.phase === 'switched') {
|
|
253
|
-
out.push('
|
|
285
|
+
out.push(' 无旧备份');
|
|
254
286
|
}
|
|
255
287
|
return { text: out.join(NL) };
|
|
256
288
|
}
|
|
@@ -258,32 +290,32 @@ export function pmWorkspaceTool(env) {
|
|
|
258
290
|
if (action === 'move-retarget') {
|
|
259
291
|
const r = stageRetarget();
|
|
260
292
|
if (r.already) { out.push('DSH 登记已在新位置,无需修改登记。'); return { text: out.join(NL) }; }
|
|
261
|
-
if (!r.ok) { out.push('
|
|
262
|
-
out.push('
|
|
263
|
-
out.push('
|
|
293
|
+
if (!r.ok) { out.push('派出助手失败:' + r.error); out.push(' 手工执行:' + (r.script && r.script.helper)); return { text: out.join(NL) }; }
|
|
294
|
+
out.push('已派出修改登记的助手(pid ' + r.pid + ')。');
|
|
295
|
+
out.push(' 该助手会在完全退出 DSH 后改写下列登记(每份先备份):');
|
|
264
296
|
for (const f of (r.plan.files || [])) if (f.count) out.push(' ' + f.path + ' ×' + f.count);
|
|
265
|
-
out.push('
|
|
297
|
+
out.push(' 只改登记文本,不改动任何数据文件;日志:' + (r.script && r.script.log));
|
|
266
298
|
return { text: out.join(NL) };
|
|
267
299
|
}
|
|
268
300
|
|
|
269
301
|
if (action === 'move-repair') {
|
|
270
302
|
const r = repairLink();
|
|
271
303
|
if (r.already) { out.push('原路径已是指向新根的链接,无需重建。'); return { text: out.join(NL) }; }
|
|
272
|
-
out.push('已重建链接:' + r.src + '
|
|
273
|
-
out.push('
|
|
304
|
+
out.push('已重建链接:' + r.src + ' 指向 ' + r.dst + '(' + r.kind + ')');
|
|
305
|
+
out.push(' 说明:仅建立链接,未改动任何文件。');
|
|
274
306
|
return { text: out.join(NL) };
|
|
275
307
|
}
|
|
276
308
|
|
|
277
309
|
if (action === 'move-cleanup') {
|
|
278
|
-
if (args.confirm !== true) throw new Error('move-cleanup 需要 confirm=true
|
|
310
|
+
if (args.confirm !== true) throw new Error('move-cleanup 需要 confirm=true(将实际删除旧备份目录)');
|
|
279
311
|
const r = await cleanupOld({ confirm: true });
|
|
280
312
|
out.push(r.note);
|
|
281
|
-
for (const p of r.removed) out.push('
|
|
313
|
+
for (const p of r.removed) out.push(' 已删除 ' + p);
|
|
282
314
|
if (r.bytes) out.push(' 释放 ' + gb(r.bytes));
|
|
283
315
|
return { text: out.join(NL) };
|
|
284
316
|
}
|
|
285
317
|
|
|
286
|
-
throw new Error('action 只能是 init / scaffold / list / create / delete / move-probe / move / move-switch / move-status / move-retarget / move-repair / move-cleanup');
|
|
318
|
+
throw new Error('action 只能是 init / scaffold / migrate / list / create / delete / move-probe / move / move-switch / move-status / move-retarget / move-repair / move-cleanup');
|
|
287
319
|
},
|
|
288
320
|
};
|
|
289
321
|
}
|
package/src/tools/peer.mjs
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* 动作级权限在这里再判一次:角色矩阵只决定「这个工具可不可见」,不细分动作。
|
|
11
11
|
*/
|
|
12
12
|
import { randomUUID } from 'node:crypto';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
13
16
|
import { textOutput } from '../lib/output.mjs';
|
|
14
17
|
import { PEER_NAME } from '../lib/names.mjs';
|
|
15
18
|
import { NL } from '../lib/text.mjs';
|
|
@@ -18,9 +21,197 @@ import {
|
|
|
18
21
|
projectName, pick, peerMarker, resolveWorkspaceId, findSessionLog, decodeSessionLog, conversation,
|
|
19
22
|
} from '../lib/peer.mjs';
|
|
20
23
|
|
|
21
|
-
export const ACTIONS = ['list', 'status', 'send', 'peek', 'rename', 'new'];
|
|
24
|
+
export const ACTIONS = ['list', 'status', 'send', 'wait', 'collect', 'collab', 'peek', 'rename', 'new'];
|
|
22
25
|
const GLOBAL_ONLY = ['list', 'peek', 'rename'];
|
|
23
26
|
|
|
27
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
|
+
const COLLECT_POLL_MS = 30000;
|
|
29
|
+
const WAIT_MIN_MS = 120000;
|
|
30
|
+
|
|
31
|
+
/** 协作游标:交办者 → 目标 → { since }。只存时间戳,不存消息正文。 */
|
|
32
|
+
function collectFile() {
|
|
33
|
+
const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
|
|
34
|
+
return path.join(home, '.dsh', 'storages', 'dsh-handover.collect.json');
|
|
35
|
+
}
|
|
36
|
+
function loadCollect() {
|
|
37
|
+
try {
|
|
38
|
+
const data = JSON.parse(fs.readFileSync(collectFile(), 'utf8'));
|
|
39
|
+
if (data && typeof data === 'object' && data.cursors) {
|
|
40
|
+
if (!Array.isArray(data.groups)) data.groups = [];
|
|
41
|
+
return data;
|
|
42
|
+
}
|
|
43
|
+
} catch (error) { void error; }
|
|
44
|
+
return { version: 1, cursors: {}, groups: [] };
|
|
45
|
+
}
|
|
46
|
+
function saveCollect(data) {
|
|
47
|
+
const file = collectFile();
|
|
48
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
49
|
+
const tmp = file + '.tmp-' + process.pid;
|
|
50
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf8');
|
|
51
|
+
fs.renameSync(tmp, file);
|
|
52
|
+
}
|
|
53
|
+
function callerOf(exec) {
|
|
54
|
+
return String(exec?.agent?.session?.header?.id ?? 'unknown');
|
|
55
|
+
}
|
|
56
|
+
function sinceOf(data, caller, target) {
|
|
57
|
+
const row = data.cursors[caller] && data.cursors[caller][target];
|
|
58
|
+
return row && Number.isFinite(row.since) ? row.since : null;
|
|
59
|
+
}
|
|
60
|
+
function setSince(data, caller, target, since) {
|
|
61
|
+
if (!data.cursors[caller]) data.cursors[caller] = {};
|
|
62
|
+
data.cursors[caller][target] = { since };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 协作注册:被交办方在协作期间不得向交办者或同组成员发明文消息;收集完自动解除。 */
|
|
66
|
+
function groupsOf(state) {
|
|
67
|
+
return Array.isArray(state.groups) ? state.groups : [];
|
|
68
|
+
}
|
|
69
|
+
function groupOf(state, sid) {
|
|
70
|
+
return groupsOf(state).find((g) => String(sid) === String(g.coordinator) || (Array.isArray(g.members) && g.members.indexOf(String(sid)) !== -1)) || null;
|
|
71
|
+
}
|
|
72
|
+
function assertNotReplyingInCollab(state, sender, target) {
|
|
73
|
+
const g = groupOf(state, sender);
|
|
74
|
+
if (!g) return;
|
|
75
|
+
if (String(sender) === String(g.coordinator)) return;
|
|
76
|
+
const inGroup = String(target) === String(g.coordinator)
|
|
77
|
+
|| (Array.isArray(g.members) && g.members.indexOf(String(target)) !== -1);
|
|
78
|
+
if (!inGroup) return;
|
|
79
|
+
throw new Error(
|
|
80
|
+
'协作注册期间,被交办方不得向交办者或同组成员发送明文消息(target=' + target + '):'
|
|
81
|
+
+ '完成即结束回合,最终回复即为结果,协调者用 peer(action="collect") 取回。'
|
|
82
|
+
+ '如需请示,将问题写入最终回复并结束回合;普通对话不受此限制。',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 供宿主工具守卫使用:某会话是协作成员(非协调者)时返回拒绝理由,否则空串。
|
|
88
|
+
* 用于拦住 DSH 自带的 send_message 等明文投递通道。
|
|
89
|
+
*/
|
|
90
|
+
export function collabMemberReason(sid) {
|
|
91
|
+
try {
|
|
92
|
+
const state = loadCollect();
|
|
93
|
+
const g = groupOf(state, sid);
|
|
94
|
+
if (!g) return '';
|
|
95
|
+
if (String(sid) === String(g.coordinator)) return '';
|
|
96
|
+
return '协作注册期间,被交办方不得发送明文消息:完成即结束回合,最终回复由协调者用 peer(action="collect") 取回。';
|
|
97
|
+
} catch (error) { void error; }
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 协作注册:开始或结束一组协作;结束即解除明文交流限制。 */
|
|
102
|
+
function collabText(args, exec) {
|
|
103
|
+
const caller = callerOf(exec);
|
|
104
|
+
const state = loadCollect();
|
|
105
|
+
// 结束模式取 collabMode(schema 参数名);兼容早期把 mode 当结束开关的调用。
|
|
106
|
+
const ending = args.collabMode === 'end' || args.mode === 'end';
|
|
107
|
+
if (ending) {
|
|
108
|
+
const before = groupsOf(state).length;
|
|
109
|
+
state.groups = groupsOf(state).filter((g) => String(g.coordinator) !== String(caller));
|
|
110
|
+
saveCollect(state);
|
|
111
|
+
return '已结束协作注册(' + String(before - state.groups.length) + ' 组),明文交流限制已解除。';
|
|
112
|
+
}
|
|
113
|
+
const members = (Array.isArray(args.sessions) ? args.sessions : [])
|
|
114
|
+
.map((x) => String(x || '').trim()).filter(Boolean).filter((s) => s !== caller);
|
|
115
|
+
if (!members.length) throw new Error('peer(action="collab"):sessions 至少列一个协作会话');
|
|
116
|
+
state.groups = groupsOf(state).filter((g) => String(g.coordinator) !== String(caller));
|
|
117
|
+
state.groups.push({ coordinator: caller, members, at: Date.now() });
|
|
118
|
+
saveCollect(state);
|
|
119
|
+
return '已注册协作:协调者 ' + caller + ',成员 ' + members.join('、')
|
|
120
|
+
+ '。协作期间成员不得向协调者或同组成员发送明文消息;collect 取回全部结果后自动解除。';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 助手文本:读目标日志里 since 之后的助手回复,不定长截断。 */
|
|
124
|
+
function assistantReplies(file, sinceMs) {
|
|
125
|
+
const events = decodeSessionLog(file);
|
|
126
|
+
if (events === null) return null;
|
|
127
|
+
const items = conversation(events, 500).filter((it) => it.who === 'assistant' && Number(it.t) > sinceMs);
|
|
128
|
+
return items.map((it) => it.text);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 单个目标的完成判定:这一轮已结束,且 since 之后有助手文本。 */
|
|
132
|
+
function inspectTarget(sid, since) {
|
|
133
|
+
const file = findSessionLog(sid);
|
|
134
|
+
if (file === null) return { sid, state: 'missing', text: '' };
|
|
135
|
+
let replies = null;
|
|
136
|
+
try { replies = assistantReplies(file, since); } catch (error) { void error; }
|
|
137
|
+
if (replies === null) return { sid, state: 'nozstd', text: '' };
|
|
138
|
+
const cache = readProjCache(sid);
|
|
139
|
+
const busy = cache === null ? null : isBusy(cache);
|
|
140
|
+
if (busy === false && replies.length) {
|
|
141
|
+
return { sid, state: 'done', text: replies[replies.length - 1], at: Date.now() };
|
|
142
|
+
}
|
|
143
|
+
return { sid, state: busy === null ? 'unknown' : busy ? 'running' : 'ended-no-text', text: '' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 一对一:等单个目标的回复。 */
|
|
147
|
+
async function waitText(args) {
|
|
148
|
+
const sid = String(args.session_id || '').trim();
|
|
149
|
+
if (!sid) throw new Error('peer(action="wait"):session_id 不能为空');
|
|
150
|
+
const timeout = Math.max(WAIT_MIN_MS, Math.min(3600000, Number(args.timeout_ms) || WAIT_MIN_MS));
|
|
151
|
+
const since = Number(args.since_ms) || Date.now();
|
|
152
|
+
const deadline = Date.now() + timeout;
|
|
153
|
+
let lastError = '';
|
|
154
|
+
for (;;) {
|
|
155
|
+
const one = inspectTarget(sid, since);
|
|
156
|
+
if (one.state === 'done') return '回复 ' + sid + ':' + NL + one.text;
|
|
157
|
+
if (one.state === 'missing') throw new Error('peer(action="wait"):找不到目标会话日志:' + sid);
|
|
158
|
+
if (one.state === 'nozstd') throw new Error('peer(action="wait"):本 Node 构建没有 zstd 支持,读不了会话日志');
|
|
159
|
+
lastError = one.state;
|
|
160
|
+
if (Date.now() >= deadline) break;
|
|
161
|
+
await sleep(2000);
|
|
162
|
+
}
|
|
163
|
+
return '等待超时(' + String(Math.round(timeout / 1000)) + ' 秒):' + sid + ' 状态 ' + lastError
|
|
164
|
+
+ '。可先用 peer(action="status") 查看状态,稍后重试 wait。';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 多会话收集:列一串会话,谁先完成先返回;同时完成多个就一起返回。
|
|
169
|
+
* 一次调用最多等 timeout_ms(最低 120 秒,由助手按任务预估设置);到点一个都没完成则返回仍在运行的名单。
|
|
170
|
+
*/
|
|
171
|
+
async function collectText(args, exec) {
|
|
172
|
+
const caller = callerOf(exec);
|
|
173
|
+
const state = loadCollect();
|
|
174
|
+
const list = Array.isArray(args.sessions)
|
|
175
|
+
? args.sessions.map((x) => String(x || '').trim()).filter(Boolean)
|
|
176
|
+
: [];
|
|
177
|
+
const targets = list.length ? list : Object.keys(state.cursors[caller] || {});
|
|
178
|
+
if (!targets.length) throw new Error('peer(action="collect"):sessions 为空,且没有已交办的目标');
|
|
179
|
+
const timeout = Math.max(WAIT_MIN_MS, Math.min(3600000, Number(args.timeout_ms) || WAIT_MIN_MS));
|
|
180
|
+
const deadline = Date.now() + timeout;
|
|
181
|
+
const explicitSince = Number(args.since_ms) || null;
|
|
182
|
+
const since = {};
|
|
183
|
+
for (const sid of targets) since[sid] = sinceOf(state, caller, sid) ?? explicitSince ?? Date.now();
|
|
184
|
+
const done = [];
|
|
185
|
+
const running = new Map(targets.map((sid) => [sid, 'running']));
|
|
186
|
+
for (;;) {
|
|
187
|
+
for (const sid of targets) {
|
|
188
|
+
if (done.some((d) => d.sid === sid)) continue;
|
|
189
|
+
const one = inspectTarget(sid, since[sid]);
|
|
190
|
+
running.set(sid, one.state);
|
|
191
|
+
if (one.state === 'done') done.push(one);
|
|
192
|
+
}
|
|
193
|
+
if (done.length) {
|
|
194
|
+
for (const one of done) setSince(state, caller, one.sid, one.at);
|
|
195
|
+
const groups = groupsOf(state);
|
|
196
|
+
const g = groups.find((x) => String(x.coordinator) === caller);
|
|
197
|
+
if (g) {
|
|
198
|
+
g.members = (Array.isArray(g.members) ? g.members : []).filter((m) => !done.some((d) => d.sid === m));
|
|
199
|
+
if (!g.members.length) state.groups = groups.filter((x) => x !== g);
|
|
200
|
+
}
|
|
201
|
+
saveCollect(state);
|
|
202
|
+
const out = ['已完成 ' + String(done.length) + ' 个:'];
|
|
203
|
+
for (const one of done) out.push('[' + one.sid + ']' + NL + one.text);
|
|
204
|
+
return out.join(NL);
|
|
205
|
+
}
|
|
206
|
+
if (Date.now() >= deadline) break;
|
|
207
|
+
await sleep(COLLECT_POLL_MS);
|
|
208
|
+
}
|
|
209
|
+
const lines = ['本轮没有完成的会话(已等待 ' + String(Math.round(timeout / 1000)) + ' 秒):'];
|
|
210
|
+
for (const sid of targets) lines.push(' ' + sid + ' ' + (running.get(sid) || 'running'));
|
|
211
|
+
lines.push('可稍后重试 collect;已完成的会立即返回。');
|
|
212
|
+
return lines.join(NL);
|
|
213
|
+
}
|
|
214
|
+
|
|
24
215
|
function roleOf(env, exec) {
|
|
25
216
|
try { return env.roleOf(exec) || { role: 'plain' }; } catch { return { role: 'plain' }; }
|
|
26
217
|
}
|
|
@@ -85,11 +276,23 @@ async function sendText(env, args, exec, info) {
|
|
|
85
276
|
const sender = exec?.agent?.session?.header?.id ?? 'unknown';
|
|
86
277
|
const marker = peerMarker(sender);
|
|
87
278
|
const body = message.startsWith(marker) ? message : marker + NL + message;
|
|
279
|
+
const before = loadCollect();
|
|
280
|
+
assertNotReplyingInCollab(before, sender, target);
|
|
281
|
+
const since = Date.now();
|
|
88
282
|
const receipt = await controller.prompt(
|
|
89
283
|
{ requestId: randomUUID(), sessionId: target, mode, content: [{ type: 'text', text: body }] },
|
|
90
284
|
new AbortController().signal,
|
|
91
285
|
);
|
|
92
|
-
|
|
286
|
+
try {
|
|
287
|
+
const state = loadCollect();
|
|
288
|
+
setSince(state, sender, target, since);
|
|
289
|
+
saveCollect(state);
|
|
290
|
+
} catch (error) { void error; }
|
|
291
|
+
const head = 'accepted=' + String(receipt.accepted) + ' mode=' + mode + ' target=' + target + ' from=' + sender;
|
|
292
|
+
if (args.wait === true) {
|
|
293
|
+
return head + NL + await waitText({ session_id: target, since_ms: since, timeout_ms: args.timeout_ms });
|
|
294
|
+
}
|
|
295
|
+
return head;
|
|
93
296
|
}
|
|
94
297
|
|
|
95
298
|
async function peekText(args) {
|
|
@@ -132,7 +335,7 @@ async function newText(env, args, info) {
|
|
|
132
335
|
// 会话创建仅属全局;杂项可以把会话建在 Misc 下(落到本任务体系内)。
|
|
133
336
|
const wantsWorkspace = typeof args.workspace === 'string' && args.workspace.trim() !== '';
|
|
134
337
|
if (wantsWorkspace && args.cwd !== undefined) {
|
|
135
|
-
throw new Error('peer(action="new"):传 workspace
|
|
338
|
+
throw new Error('peer(action="new"):传 workspace 时不得再传 cwd,session.create 只接受其中之一(cwd 会自动取该工作区的 path)');
|
|
136
339
|
}
|
|
137
340
|
const targetText = String(args.workspace || args.cwd || '').toLowerCase();
|
|
138
341
|
const allowed = info.role === 'global' || (info.role === 'misc' && targetText.indexOf('misc') !== -1);
|
|
@@ -171,17 +374,22 @@ export function pmPeerTool(env) {
|
|
|
171
374
|
return {
|
|
172
375
|
name: PEER_NAME,
|
|
173
376
|
description:
|
|
174
|
-
'平级会话:action=list
|
|
377
|
+
'平级会话:action=list 会话一览、status 单会话详情、send 投递消息(wait=true 时同轮等待回复)、wait 等待单个回复、collect 收集多个会话中先完成的最终回复、collab 注册或结束协作、peek 读取历史、rename 修改标题、new 新建会话。list、peek、rename 与 new 仅全局会话可用。',
|
|
175
378
|
parameters: {
|
|
176
379
|
type: 'object',
|
|
177
380
|
additionalProperties: false,
|
|
178
381
|
required: ['action'],
|
|
179
382
|
properties: {
|
|
180
|
-
action: { type: 'string', enum: ACTIONS, description: '
|
|
181
|
-
session_id: { type: 'string', description: 'status、send、peek、rename:目标会话 id' },
|
|
383
|
+
action: { type: 'string', enum: ACTIONS, description: '要执行的动作' },
|
|
384
|
+
session_id: { type: 'string', description: 'status、send、wait、peek、rename:目标会话 id' },
|
|
385
|
+
sessions: { type: 'array', items: { type: 'string' }, description: 'collect:要等待的会话 id 列表;留空则取已交办的目标' },
|
|
182
386
|
message: { type: 'string', description: 'send:要投递的正文' },
|
|
183
|
-
mode: { type: 'string', enum: ['queue', 'steer'], description: 'send:queue
|
|
184
|
-
|
|
387
|
+
mode: { type: 'string', enum: ['queue', 'steer'], description: 'send:queue(缺省)或 steer' },
|
|
388
|
+
wait: { type: 'boolean', description: 'send:投递后在同一轮等待回复(一对一对话用)' },
|
|
389
|
+
collabMode: { type: 'string', enum: ['start', 'end'], description: 'collab:start 注册协作(缺省)、end 结束并解除限制' },
|
|
390
|
+
timeout_ms: { type: 'number', description: 'wait、collect:单次等待上限(毫秒,最低 120000,按任务预估设置)' },
|
|
391
|
+
since_ms: { type: 'number', description: 'wait、collect:从哪一时刻之后查找回复(缺省取交办时刻)' },
|
|
392
|
+
limit: { type: 'number', description: 'list:最多多少行(缺省 40);peek:最近多少条(缺省 8,上限 40)' },
|
|
185
393
|
include_subagents: { type: 'boolean', description: 'list:同时列出子会话' },
|
|
186
394
|
title: { type: 'string', description: 'new、rename:显示标题' },
|
|
187
395
|
workspace: { type: 'string', description: 'new:工作区 id、标题或路径(与 cwd 互斥)' },
|
|
@@ -197,6 +405,9 @@ export function pmPeerTool(env) {
|
|
|
197
405
|
if (action === 'list') return { text: await listText(env, args) };
|
|
198
406
|
if (action === 'status') return { text: await statusText(env, args) };
|
|
199
407
|
if (action === 'send') return { text: await sendText(env, args, exec, info) };
|
|
408
|
+
if (action === 'wait') return { text: await waitText(args) };
|
|
409
|
+
if (action === 'collect') return { text: await collectText(args, exec) };
|
|
410
|
+
if (action === 'collab') return { text: collabText(args, exec) };
|
|
200
411
|
if (action === 'peek') return { text: await peekText(args) };
|
|
201
412
|
if (action === 'rename') return { text: await renameText(env, args) };
|
|
202
413
|
return { text: await newText(env, args, info) };
|