@whaletalk/dsh-codex-collab 0.1.7 → 0.1.9
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/CHANGELOG.md +35 -0
- package/README.md +96 -5
- package/codex-plugin/plugins/dsh/.codex-plugin/plugin.json +1 -1
- package/codex-plugin/plugins/dsh/skills/dsh-collab/SKILL.md +41 -5
- package/codex-plugin/plugins/dsh/skills/dsh-collab/scripts/task.mjs +134 -11
- package/docs/CODEX_DSH_TASK_REPLY_PLAN.md +206 -0
- package/docs/DSH_TO_CODEX_INBOX.md +167 -0
- package/docs/DSH_TO_CODEX_MESSAGING_PLAN.md +140 -0
- package/docs/codex-desktop-attachment-evidence.md +116 -0
- package/harness/codex-associations.mjs +265 -0
- package/harness/codex-outbox.mjs +432 -0
- package/harness/dsh-bridge.mjs +538 -35
- package/harness/fs-lock.mjs +208 -0
- package/harness/session-target.mjs +134 -0
- package/harness/task-table.mjs +32 -0
- package/mcp/codex-desktop-probe.mjs +407 -0
- package/mcp/codex-inbox.mjs +271 -0
- package/mcp/dsh-mcp.mjs +147 -17
- package/package.json +3 -1
package/mcp/dsh-mcp.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// 客户端(Secure MCP Tunnel / cloudflared)桥接给 ChatGPT 等远程产品。
|
|
7
7
|
const GATEWAY = process.env.DSH_BRIDGE_URL || 'http://127.0.0.1:3080'
|
|
8
8
|
const SERVER_NAME = 'dsh-bridge'
|
|
9
|
-
const SERVER_VERSION = '0.1.
|
|
9
|
+
const SERVER_VERSION = '0.1.9'
|
|
10
10
|
const PROTOCOL_VERSION = '2024-11-05'
|
|
11
11
|
|
|
12
12
|
const log = (...args) => console.error('[dsh-mcp]', ...args)
|
|
@@ -21,7 +21,10 @@ const tools = [
|
|
|
21
21
|
'lane(并行通道名,默认 main), model(fast=deepseek-v4-flash 快而省 / pro=deepseek-v4-pro 质量优先), ' +
|
|
22
22
|
'sessionId(已有会话 id), sessionQuery(按标题/内容搜已有会话,需唯一命中,否则返回候选), deliver(queue=排队(默认) / steer=插入当前回合), ' +
|
|
23
23
|
'dryRun(true 时只解析目标并回显, 不投递任何消息), ' +
|
|
24
|
+
'originThreadId(发起本任务的 Codex 对话 id,必须显式传入), ' +
|
|
24
25
|
'wait(默认 true,阻塞至任务完成并返回 DeepSeek 汇报), timeoutSec(默认 600,最长 900)。' +
|
|
26
|
+
'每个回合派活都要显式传 originThreadId=当前对话 id(传入本工具调用的那个对话):它把任务与发起对话持久绑定,' +
|
|
27
|
+
'之后任何回合都能用 originThreadId+taskId 精确取回这次任务的结果。**不要**用常驻进程环境变量或"最近对话"推断来源。' +
|
|
25
28
|
'注意: 验证"这条查询会命中哪条会话"必须用 dryRun 或 dsh_sessions——**绝不要投递一条消息去试探**(那会污染用户会话)。',
|
|
26
29
|
inputSchema: {
|
|
27
30
|
type: 'object',
|
|
@@ -34,6 +37,7 @@ const tools = [
|
|
|
34
37
|
sessionQuery: { type: 'string', description: '按标题或内容搜索已有会话;唯一命中才派发,多条或落空会返回 candidates 让你改用 sessionId。' },
|
|
35
38
|
deliver: { type: 'string', enum: ['queue', 'steer'], description: '投递方式:queue 排队(默认),steer 插入当前回合。' },
|
|
36
39
|
dryRun: { type: 'boolean', description: '只解析目标并回显(含命中路径 matchedBy 与候选),不投递任何消息。用来确认命中,替代"发消息探测"。' },
|
|
40
|
+
originThreadId: { type: 'string', description: '发起本任务的 Codex 对话 id(来源关联,显式传入)。用于之后按 originThreadId+taskId 精确收取结果。' },
|
|
37
41
|
wait: { type: 'boolean', description: '默认 true:阻塞直到任务完成并返回汇报。' },
|
|
38
42
|
timeoutSec: { type: 'number', description: '最长等待秒数(默认 600,最大 900)。' },
|
|
39
43
|
},
|
|
@@ -57,21 +61,29 @@ const tools = [
|
|
|
57
61
|
},
|
|
58
62
|
{
|
|
59
63
|
name: 'dsh_task_status',
|
|
60
|
-
description:
|
|
64
|
+
description:
|
|
65
|
+
'查询 DeepSeek 协作任务的状态与结果。两种用法:' +
|
|
66
|
+
'(1) taskId + originThreadId → 精确取这次任务的状态/结果(跨回合、网关重载后都能按来源从持久关联记录恢复;' +
|
|
67
|
+
'恢复到的未完成任务会返回 status=unknown 并明确"执行/交付状态未能确认、请勿重发");' +
|
|
68
|
+
'(2) 只给 originThreadId → 列出该对话登记过的任务,用来找回丢失的 taskId。' +
|
|
69
|
+
'originThreadId 必须显式传当前对话 id;只给 taskId 时保持旧的兼容查询。',
|
|
61
70
|
inputSchema: {
|
|
62
71
|
type: 'object',
|
|
63
|
-
properties: {
|
|
64
|
-
|
|
72
|
+
properties: {
|
|
73
|
+
taskId: { type: 'string', description: '任务 ID(dsh_task 返回)。与 originThreadId 一起用时为精确取值。' },
|
|
74
|
+
originThreadId: { type: 'string', description: '发起该任务的 Codex 对话 id(显式传入当前对话)。只给来源时列出该对话的任务。' },
|
|
75
|
+
},
|
|
65
76
|
},
|
|
66
77
|
},
|
|
67
78
|
{
|
|
68
79
|
name: 'dsh_task_cancel',
|
|
69
|
-
description: '取消一个运行中的 DeepSeek 协作任务。参数: taskId(必填), force(session 目标的任务默认拒绝取消,因为它会打断用户自己的回合;确认要打断时传 true)。',
|
|
80
|
+
description: '取消一个运行中的 DeepSeek 协作任务。参数: taskId(必填), force(session 目标的任务默认拒绝取消,因为它会打断用户自己的回合;确认要打断时传 true), originThreadId(可选,已关联任务必须传同一个来源)。',
|
|
70
81
|
inputSchema: {
|
|
71
82
|
type: 'object',
|
|
72
83
|
properties: {
|
|
73
84
|
taskId: { type: 'string', description: '任务 ID。' },
|
|
74
85
|
force: { type: 'boolean', description: '仅对投递进已有会话的任务有意义:true 才真正打断该会话。' },
|
|
86
|
+
originThreadId: { type: 'string', description: '该任务的来源对话 id(已关联任务必须一致,避免别的对话取消它)。' },
|
|
75
87
|
},
|
|
76
88
|
required: ['taskId'],
|
|
77
89
|
},
|
|
@@ -80,13 +92,14 @@ const tools = [
|
|
|
80
92
|
name: 'dsh_review',
|
|
81
93
|
description:
|
|
82
94
|
'让独立评审子代理审查某工作目录的代码变更。评审员会读磁盘真实文件、静态审查,并实际运行构建/测试验证可跑通,返回结构化评审报告。' +
|
|
83
|
-
'参数: cwd(必填), diff(变更 diff 文本), focus(审查重点), wait(默认 true), timeoutSec(默认 900)。',
|
|
95
|
+
'参数: cwd(必填), diff(变更 diff 文本), focus(审查重点), originThreadId(发起本次评审的当前对话 id,显式传入;评审同样产生 taskId,因此同样支持按 origin+taskId 收取), wait(默认 true), timeoutSec(默认 900)。',
|
|
84
96
|
inputSchema: {
|
|
85
97
|
type: 'object',
|
|
86
98
|
properties: {
|
|
87
99
|
cwd: { type: 'string', description: '评审目标所在工作目录(绝对路径)。' },
|
|
88
100
|
diff: { type: 'string', description: '变更 diff 文本(git diff 输出)。省略时评审员自行读取目录现状。' },
|
|
89
101
|
focus: { type: 'string', description: '审查重点。' },
|
|
102
|
+
originThreadId: { type: 'string', description: '发起本次评审的 Codex 对话 id(来源关联,显式传入当前对话)。' },
|
|
90
103
|
wait: { type: 'boolean', description: '默认 true:阻塞直到评审完成。' },
|
|
91
104
|
timeoutSec: { type: 'number', description: '最长等待秒数(默认 900)。' },
|
|
92
105
|
},
|
|
@@ -110,6 +123,82 @@ const tools = [
|
|
|
110
123
|
]
|
|
111
124
|
|
|
112
125
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
126
|
+
const MAX_ORIGIN_LENGTH = 200
|
|
127
|
+
|
|
128
|
+
/** 显式来源必须合法:非空字符串、≤200 字符、无控制字符。**不合法就拒绝,绝不丢掉或改用环境变量。** */
|
|
129
|
+
const isValidOrigin = (value) => typeof value === 'string'
|
|
130
|
+
&& value.trim() !== ''
|
|
131
|
+
&& value.trim().length <= MAX_ORIGIN_LENGTH
|
|
132
|
+
&& !/[\u0000-\u001f\u007f]/.test(value)
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 只读核验网关是否真的支持"持久来源绑定"。
|
|
136
|
+
* **不能靠包版本猜**:旧网关会忽略 originThreadId 并照常 accepted,从而产生未绑定任务。
|
|
137
|
+
* 这里要求网关在 /status(无参数)里明确上报 `capabilities.originBinding.supported === true`;
|
|
138
|
+
* 拿不到(旧网关 / 关联存储不可用)一律视为不支持。
|
|
139
|
+
*/
|
|
140
|
+
async function originBindingSupported() {
|
|
141
|
+
try {
|
|
142
|
+
const res = await fetch(GATEWAY + '/api/dsh-bridge/status')
|
|
143
|
+
const body = await res.json()
|
|
144
|
+
return body !== null && typeof body === 'object'
|
|
145
|
+
&& body.capabilities !== null && typeof body.capabilities === 'object'
|
|
146
|
+
&& body.capabilities.originBinding !== null && typeof body.capabilities.originBinding === 'object'
|
|
147
|
+
&& body.capabilities.originBinding.supported === true
|
|
148
|
+
} catch (e) {
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const ORIGIN_UNSUPPORTED = {
|
|
154
|
+
ok: false,
|
|
155
|
+
reason: 'origin-binding-unsupported',
|
|
156
|
+
error: 'this gateway does not support persistent origin binding (it does not advertise capabilities.originBinding); refusing to dispatch an unbound task. Upgrade the dsh-codex-collab plugin in the DSH profile, or dispatch without originThreadId.',
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const INVALID_ORIGIN = {
|
|
160
|
+
ok: false,
|
|
161
|
+
reason: 'invalid-origin',
|
|
162
|
+
error: 'originThreadId must be a non-empty string (<= 200 characters, no control characters); omit it entirely for the legacy unbound mode',
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 网关自己给出的"明确拒绝"reason:原样透传(这些不是"成功了但没确认")。 */
|
|
166
|
+
const REFUSAL_REASONS = new Set([
|
|
167
|
+
'origin-required',
|
|
168
|
+
'origin-mismatch',
|
|
169
|
+
'invalid-origin',
|
|
170
|
+
'unknown-task',
|
|
171
|
+
'execution-state-unconfirmed',
|
|
172
|
+
])
|
|
173
|
+
|
|
174
|
+
/** 网关必须**回显并匹配**我们请求的 originThreadId,才算确认了这次绑定/取值。 */
|
|
175
|
+
const originConfirmed = (payload, originThreadId) => payload !== null && typeof payload === 'object' && payload.originThreadId === originThreadId
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 能力检查通过 ≠ 这次绑定被确认:网关可能在中途换了处理者(重载/旧 handler),接受了请求却没写关联。
|
|
179
|
+
* 此时任务**可能已经执行**,所以既不能说成功,也绝不能重发;只如实上报未确认。
|
|
180
|
+
* 注意:只挑 taskId/status,**绝不**把网关响应里的 result 透出去。
|
|
181
|
+
*/
|
|
182
|
+
const unconfirmedPayload = (payload) => ({
|
|
183
|
+
ok: false,
|
|
184
|
+
phase: 'unconfirmed',
|
|
185
|
+
// 网关自称的 status 不可信(它可能压根没绑定):一律按"未确认"上报。
|
|
186
|
+
status: 'unknown',
|
|
187
|
+
reason: 'origin-not-confirmed',
|
|
188
|
+
resend: false,
|
|
189
|
+
error: 'the gateway accepted the request but did not confirm the source binding (no matching originThreadId in its response); it may have executed unbound — do not resend',
|
|
190
|
+
...(payload !== null && typeof payload === 'object' && typeof payload.taskId === 'string' ? { taskId: payload.taskId } : {}),
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
/** 解析显式来源:undefined=未传(旧模式);否则必须合法,不合法直接拒绝(不回落、不改用 ENV)。 */
|
|
194
|
+
function readOrigin(args) {
|
|
195
|
+
if (args === null || typeof args !== 'object') return { ok: true, originThreadId: undefined }
|
|
196
|
+
if (!Object.prototype.hasOwnProperty.call(args, 'originThreadId')) return { ok: true, originThreadId: undefined }
|
|
197
|
+
const raw = args.originThreadId
|
|
198
|
+
if (raw === undefined) return { ok: true, originThreadId: undefined }
|
|
199
|
+
if (!isValidOrigin(raw)) return { ok: false, value: INVALID_ORIGIN }
|
|
200
|
+
return { ok: true, originThreadId: raw.trim() }
|
|
201
|
+
}
|
|
113
202
|
|
|
114
203
|
async function callGateway(method, body) {
|
|
115
204
|
const res = await fetch(GATEWAY + method, {
|
|
@@ -120,19 +209,25 @@ async function callGateway(method, body) {
|
|
|
120
209
|
return res.json()
|
|
121
210
|
}
|
|
122
211
|
|
|
123
|
-
async function waitTask(taskId, timeoutSec) {
|
|
212
|
+
async function waitTask(taskId, timeoutSec, originThreadId) {
|
|
124
213
|
const deadline = Date.now() + Math.min(Math.max(timeoutSec || 600, 5), 900) * 1000
|
|
214
|
+
const suffix = typeof originThreadId === 'string' && originThreadId !== '' ? '&originThreadId=' + encodeURIComponent(originThreadId) : ''
|
|
125
215
|
while (Date.now() < deadline) {
|
|
126
216
|
await sleep(3000)
|
|
127
|
-
const res = await fetch(GATEWAY + '/api/dsh-bridge/status?taskId=' + encodeURIComponent(taskId))
|
|
217
|
+
const res = await fetch(GATEWAY + '/api/dsh-bridge/status?taskId=' + encodeURIComponent(taskId) + suffix)
|
|
128
218
|
const status = await res.json()
|
|
129
|
-
|
|
219
|
+
// unknown:恢复到的任务执行/交付状态未能确认——**必须停止轮询**,不能无限等。终态同理。
|
|
220
|
+
if (status.status === 'done' || status.status === 'error' || status.status === 'cancelled' || status.status === 'unknown') return status
|
|
130
221
|
}
|
|
131
|
-
return { taskId, status: 'waiting', note: '仍在后台执行,可用 dsh_task_status 查询' }
|
|
222
|
+
return { taskId, status: 'waiting', note: '仍在后台执行,可用 dsh_task_status 查询', ...(suffix === '' ? {} : { originThreadId }) }
|
|
132
223
|
}
|
|
133
224
|
|
|
134
225
|
async function handleToolCall(name, args) {
|
|
135
226
|
if (name === 'dsh_task') {
|
|
227
|
+
const origin = readOrigin(args)
|
|
228
|
+
if (!origin.ok) return origin.value
|
|
229
|
+
// 显式来源:先只读核验网关能力(GET,不会派活),不支持就**零执行**拒绝。
|
|
230
|
+
if (origin.originThreadId !== undefined && !(await originBindingSupported())) return ORIGIN_UNSUPPORTED
|
|
136
231
|
const task = await callGateway('/api/dsh-bridge/task', {
|
|
137
232
|
instruction: args.instruction,
|
|
138
233
|
cwd: args.cwd,
|
|
@@ -142,18 +237,25 @@ async function handleToolCall(name, args) {
|
|
|
142
237
|
sessionQuery: args.sessionQuery,
|
|
143
238
|
deliver: args.deliver,
|
|
144
239
|
dryRun: args.dryRun === true,
|
|
240
|
+
originThreadId: origin.originThreadId,
|
|
145
241
|
})
|
|
146
242
|
// dryRun 只解析目标,不产生任务:原样回显给你看命中结果。
|
|
147
243
|
if (args.dryRun === true) return { ok: task.resolved === true, phase: 'dry-run', ...task }
|
|
148
|
-
// 4xx 拒绝(缺目标 / 会话不存在 / 搜索结果不唯一 /
|
|
244
|
+
// 4xx 拒绝(缺目标 / 会话不存在 / 搜索结果不唯一 / 通道忙 / 非法来源)不带 taskId,绝不静默新建。
|
|
149
245
|
if (task.taskId === undefined) return { ok: false, phase: 'rejected', ...task }
|
|
246
|
+
// 能力检查通过 ≠ 这次绑定被确认:网关必须回显匹配的 originThreadId。没有 → 未确认、不要重发。
|
|
247
|
+
if (origin.originThreadId !== undefined && !originConfirmed(task, origin.originThreadId)) return unconfirmedPayload(task)
|
|
150
248
|
if (task.status === 'error') return { ok: false, ...task }
|
|
151
249
|
if (args.wait === false) return { ok: true, phase: 'submitted', ...task }
|
|
152
|
-
const done = await waitTask(task.taskId, args.timeoutSec || 600)
|
|
250
|
+
const done = await waitTask(task.taskId, args.timeoutSec || 600, origin.originThreadId)
|
|
153
251
|
// 会话目标报错但 delivered=true:消息已在目标会话队列里,不要重发。
|
|
154
252
|
if (done.delivered === true && done.status === 'error') {
|
|
155
253
|
return { ok: false, phase: 'queued-then-failed', resend: false, ...done }
|
|
156
254
|
}
|
|
255
|
+
// 恢复到的任务:执行/交付状态未知——如实上报并明确"不要重发"。
|
|
256
|
+
if (done.status === 'unknown') {
|
|
257
|
+
return { ok: false, phase: 'unconfirmed', resend: false, ...done }
|
|
258
|
+
}
|
|
157
259
|
return { ok: done.status === 'done', phase: 'finished', ...done }
|
|
158
260
|
}
|
|
159
261
|
if (name === 'dsh_sessions') {
|
|
@@ -170,17 +272,45 @@ async function handleToolCall(name, args) {
|
|
|
170
272
|
return { ok: false, ...found }
|
|
171
273
|
}
|
|
172
274
|
if (name === 'dsh_task_status') {
|
|
173
|
-
|
|
174
|
-
|
|
275
|
+
// 两种用法:
|
|
276
|
+
// taskId [+ originThreadId] → 精确取值(跨回合/网关重载后也能按 origin 从持久记录恢复)
|
|
277
|
+
// 只给 originThreadId → 列出该对话登记过的任务,用来找回丢失的 taskId
|
|
278
|
+
const origin = readOrigin(args)
|
|
279
|
+
if (!origin.ok) return origin.value
|
|
280
|
+
// 显式来源=精确恢复语义:网关不支持时必须明确失败,绝不能把旧网关的普通响应当成精确恢复。
|
|
281
|
+
if (origin.originThreadId !== undefined && !(await originBindingSupported())) return ORIGIN_UNSUPPORTED
|
|
282
|
+
const params = []
|
|
283
|
+
if (typeof args.taskId === 'string' && args.taskId !== '') params.push('taskId=' + encodeURIComponent(args.taskId))
|
|
284
|
+
if (origin.originThreadId !== undefined) params.push('originThreadId=' + encodeURIComponent(origin.originThreadId))
|
|
285
|
+
if (params.length === 0) return { ok: false, error: 'taskId or originThreadId is required' }
|
|
286
|
+
const res = await fetch(GATEWAY + '/api/dsh-bridge/status?' + params.join('&'))
|
|
287
|
+
const payload = await res.json()
|
|
288
|
+
// 精确取值必须由网关**确认来源**:没有匹配的 originThreadId 就不算精确恢复(也绝不透出 result)。
|
|
289
|
+
if (origin.originThreadId !== undefined
|
|
290
|
+
&& !(payload !== null && typeof payload === 'object' && typeof payload.reason === 'string' && REFUSAL_REASONS.has(payload.reason))
|
|
291
|
+
&& !originConfirmed(payload, origin.originThreadId)) {
|
|
292
|
+
return unconfirmedPayload(payload)
|
|
293
|
+
}
|
|
294
|
+
return payload
|
|
175
295
|
}
|
|
176
296
|
if (name === 'dsh_task_cancel') {
|
|
177
|
-
|
|
297
|
+
const origin = readOrigin(args)
|
|
298
|
+
if (!origin.ok) return origin.value
|
|
299
|
+
if (origin.originThreadId !== undefined && !(await originBindingSupported())) return ORIGIN_UNSUPPORTED
|
|
300
|
+
const payload = await callGateway('/api/dsh-bridge/cancel', { taskId: args.taskId, force: args.force === true, ...(origin.originThreadId === undefined ? {} : { originThreadId: origin.originThreadId }) })
|
|
301
|
+
if (origin.originThreadId !== undefined && payload !== null && typeof payload === 'object' && payload.cancelled === true && !originConfirmed(payload, origin.originThreadId)) return unconfirmedPayload(payload)
|
|
302
|
+
return payload
|
|
178
303
|
}
|
|
179
304
|
if (name === 'dsh_review') {
|
|
180
|
-
const
|
|
305
|
+
const origin = readOrigin(args)
|
|
306
|
+
if (!origin.ok) return origin.value
|
|
307
|
+
if (origin.originThreadId !== undefined && !(await originBindingSupported())) return ORIGIN_UNSUPPORTED
|
|
308
|
+
const task = await callGateway('/api/dsh-bridge/review', { cwd: args.cwd, diff: args.diff, focus: args.focus, originThreadId: origin.originThreadId })
|
|
309
|
+
if (origin.originThreadId !== undefined && task.taskId !== undefined && !originConfirmed(task, origin.originThreadId)) return unconfirmedPayload(task)
|
|
181
310
|
if (task.status === 'error') return { ok: false, ...task }
|
|
182
311
|
if (args.wait === false) return { ok: true, phase: 'submitted', ...task }
|
|
183
|
-
const done = await waitTask(task.taskId, args.timeoutSec || 900)
|
|
312
|
+
const done = await waitTask(task.taskId, args.timeoutSec || 900, origin.originThreadId)
|
|
313
|
+
if (done.status === 'unknown') return { ok: false, phase: 'unconfirmed', resend: false, ...done }
|
|
184
314
|
return { ok: done.status === 'done', phase: 'finished', ...done }
|
|
185
315
|
}
|
|
186
316
|
if (name === 'dsh_read_file') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whaletalk/dsh-codex-collab",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Codex × DeepSeek Harness 双向编码协作:Codex 主导派活与验收,DeepSeek Harness 编码子代理在共享工作目录执行并回传,独立评审子代理实际跑通构建/测试后出具报告。提供 DSH 宿主网关插件、零依赖 MCP 服务器与 Codex 技能三件套。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -24,8 +24,10 @@
|
|
|
24
24
|
"mcp/",
|
|
25
25
|
"codex-plugin/",
|
|
26
26
|
"templates/",
|
|
27
|
+
"docs/",
|
|
27
28
|
"cordis.patch.yml",
|
|
28
29
|
"README.md",
|
|
30
|
+
"CHANGELOG.md",
|
|
29
31
|
"LICENSE",
|
|
30
32
|
"dsh-banner.png.png"
|
|
31
33
|
],
|