dsh-later 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.
@@ -0,0 +1,463 @@
1
+ import { ScheduleId, ScheduleInputError, allocateScheduleId, createAfterScheduleRecord, createAtScheduleRecord, createEveryScheduleRecord, foldScheduleEvents, scheduleView, } from '@deepseek-ai/dsh-schedule';
2
+ import { OWNED_EVENT } from './domain.js';
3
+ import { allOwnership, getOwnership, recordOwnership, removeOwnership } from './ownership-store.js';
4
+ import { detectTimeZone } from './time-utils.js';
5
+ /** 单 session 用户任务上限(PRD:防滥用)。 */
6
+ export const DEFAULT_MAX_SCHEDULES = 100;
7
+ /**
8
+ * 经过 trim 后允许的默认最大提示字符数(P0:注入防护的保守下限)。
9
+ *
10
+ * 当 settings 中 `allowLongPrompts=false` 时此值始终生效;为 `true` 时
11
+ * 由 `maxPromptChars` 字段接管实际字符上限。用户必须显式 opt-in 才解除
12
+ * 该硬上限——保持默认安全的姿态。
13
+ */
14
+ export const DEFAULT_MAX_PROMPT_CHARS = 1000;
15
+ /**
16
+ * 从 settings 解析出实际生效的 PromptLimits。
17
+ * 容错非法值(NaN / 负数 / 非数)回落 DEFAULT_MAX_PROMPT_CHARS。
18
+ */
19
+ export function resolvePromptLimits(settings) {
20
+ const allowLong = settings?.allowLongPrompts === true;
21
+ const raw = settings?.maxPromptChars;
22
+ const valid = typeof raw === 'number' && Number.isFinite(raw) && raw >= 1 && Math.floor(raw) === raw;
23
+ if (allowLong && valid)
24
+ return { allowLong: true, maxChars: raw };
25
+ return { allowLong: false, maxChars: DEFAULT_MAX_PROMPT_CHARS };
26
+ }
27
+ /** 单条提示的字符校验(纯函数)。返回 trim 后的字符串或闭包错误。 */
28
+ export function validatePrompt(raw, limits) {
29
+ const prompt = typeof raw === 'string' ? raw : '';
30
+ const trimmed = prompt.trim();
31
+ if (trimmed.length === 0) {
32
+ return { ok: false, code: 'invalid_prompt', message: '提醒内容不能为空。' };
33
+ }
34
+ if (trimmed.length > limits.maxChars) {
35
+ return {
36
+ ok: false,
37
+ code: 'invalid_prompt',
38
+ message: `提醒内容不能超过 ${limits.maxChars} 字符。`,
39
+ };
40
+ }
41
+ return { ok: true, value: trimmed };
42
+ }
43
+ /** 校验 `user_schedule_create` 参数(纯函数,含 TRIM 非空、长度、三选一)。
44
+ *
45
+ * `limits` 可选:未传时回落到 `DEFAULT_MAX_PROMPT_CHARS`(保持向后兼容);
46
+ * 命令与工具的注册路径会按当前 settings 传入实际 limits,校验消息会反映
47
+ * 当前生效的字符上限(与模型可见的 tool description 同步)。
48
+ */
49
+ export function validateCreateInput(input, limits = { allowLong: false, maxChars: DEFAULT_MAX_PROMPT_CHARS }) {
50
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) {
51
+ return { ok: false, code: 'invalid_rule', message: 'create 参数必须是一个对象。' };
52
+ }
53
+ const args = input;
54
+ const promptResult = validatePrompt(args['prompt'], limits);
55
+ if (!promptResult.ok)
56
+ return promptResult;
57
+ const trimmed = promptResult.value;
58
+ const selectorCount = Number(args['after_seconds'] !== undefined) +
59
+ Number(args['at'] !== undefined) +
60
+ Number(args['every_seconds'] !== undefined);
61
+ if (selectorCount !== 1) {
62
+ return {
63
+ ok: false,
64
+ code: 'invalid_selector',
65
+ message: 'after_seconds / at / every_seconds 必须且只能提供一项。',
66
+ };
67
+ }
68
+ const time_zone = typeof args['time_zone'] === 'string' && args['time_zone'].length > 0
69
+ ? args['time_zone']
70
+ : // P1-8:时区可选——缺省用检测到的会话时区,降低模型/客户端漏传导致的失败。
71
+ detectTimeZone();
72
+ if (!isValidIanaZone(time_zone)) {
73
+ return { ok: false, code: 'invalid_time_zone', message: 'time_zone 必须是合法的 IANA 时区。' };
74
+ }
75
+ if (args['at'] !== undefined) {
76
+ const at = args['at'];
77
+ if (typeof at === 'string') {
78
+ // 显式偏移字符串:交给 dsh-schedule 解析
79
+ }
80
+ else if (typeof at === 'object' && at !== null && !Array.isArray(at)) {
81
+ const obj = at;
82
+ if (typeof obj['date'] !== 'string' || typeof obj['time'] !== 'string' || typeof obj['time_zone'] !== 'string') {
83
+ return { ok: false, code: 'invalid_rule', message: 'at 对象必须包含 date / time / time_zone。' };
84
+ }
85
+ if (!isValidIanaZone(obj['time_zone'])) {
86
+ return { ok: false, code: 'invalid_time_zone', message: 'at.time_zone 必须是合法的 IANA 时区。' };
87
+ }
88
+ }
89
+ else {
90
+ return { ok: false, code: 'invalid_rule', message: 'at 必须是字符串或 {date, time, time_zone} 对象。' };
91
+ }
92
+ }
93
+ return { ok: true, value: { prompt: trimmed, time_zone, ...remainingSelectors(args) } };
94
+ }
95
+ function remainingSelectors(args) {
96
+ const out = {};
97
+ if (args['after_seconds'] !== undefined)
98
+ out['after_seconds'] = args['after_seconds'];
99
+ if (args['at'] !== undefined)
100
+ out['at'] = args['at'];
101
+ if (args['every_seconds'] !== undefined)
102
+ out['every_seconds'] = args['every_seconds'];
103
+ return out;
104
+ }
105
+ /** 校验 IANA 时区(与 dsh-schedule canonicalize 同源,容错)。 */
106
+ export function isValidIanaZone(value) {
107
+ if (typeof value !== 'string' || value.length === 0)
108
+ return false;
109
+ try {
110
+ new Intl.DateTimeFormat('en-US', { timeZone: value });
111
+ return true;
112
+ }
113
+ catch {
114
+ return false;
115
+ }
116
+ }
117
+ /** 从会话日志导出「用户创建过的 schedule id 集合」(历史遗留读兼容)。 */
118
+ export function foldOwnedIds(events) {
119
+ const owned = new Set();
120
+ for (const event of events) {
121
+ if (event.type !== OWNED_EVENT)
122
+ continue;
123
+ const data = event.data;
124
+ if (typeof data !== 'object' || data === null || data.version !== 1)
125
+ continue;
126
+ if (data.operation === 'add')
127
+ owned.add(data.id);
128
+ else if (data.operation === 'remove')
129
+ owned.delete(data.id);
130
+ }
131
+ return owned;
132
+ }
133
+ /**
134
+ * 从会话日志导出「schedule id → 到点注入形态」映射(默认 `context`)。
135
+ * 供 runtime 在 dispatch 时按 `/later` 与 `/schedule` 分别注入。
136
+ */
137
+ export function foldOwnedDelivery(events) {
138
+ const map = new Map();
139
+ for (const event of events) {
140
+ if (event.type !== OWNED_EVENT)
141
+ continue;
142
+ const data = event.data;
143
+ if (typeof data !== 'object' || data === null || data.version !== 1)
144
+ continue;
145
+ if (data.operation === 'remove') {
146
+ map.delete(data.id);
147
+ continue;
148
+ }
149
+ map.set(data.id, data.delivery ?? 'context');
150
+ }
151
+ return map;
152
+ }
153
+ const foldCache = new WeakMap();
154
+ /** fold 当前会话的完整用户调度状态(带缓存;调用方不得变更返回值)。
155
+ *
156
+ * 跨代兼容:0.1.1 暴露 `session.events` + `header.seedLength`;0.1.2 改名
157
+ * 为 `session.ownEvents()` + `session.inheritedEventCount`。通过鸭子类型
158
+ * (`(session as { ownEvents?; events? })`)双轨探测,不引入 `any`。
159
+ */
160
+ export function foldUserState(session) {
161
+ const sessAny = session;
162
+ const events = sessAny.ownEvents !== undefined ? sessAny.ownEvents() : sessAny.events ?? [];
163
+ // 0.1.2 上 Session.inheritedEventCount 是 SessionLogOffset(BrandedNumber),
164
+ // foldScheduleEvents 第二个参数需要它;0.1.1 上等价于 header.seedLength(number)。
165
+ // 接受任意 number,统一以 number 形式进入 cache key;foldScheduleEvents 内部会
166
+ // 自适应(要么收 number、要么要求 branded——后者 0.1.1 路径不会触达)。
167
+ const inheritedEventCount = session.inheritedEventCount ?? 0;
168
+ const cached = foldCache.get(session);
169
+ if (cached !== undefined &&
170
+ cached.events === events &&
171
+ cached.length === events.length &&
172
+ cached.inheritedEventCount === inheritedEventCount) {
173
+ return cached.state;
174
+ }
175
+ // 通过 dsh-schedule 的 foldScheduleEvents 入口:0.1.2 上第二个参数是
176
+ // SessionLogOffset,但 0.1.1 上是 number(已弃)。我们传 number,运行时
177
+ // 0.1.2 的 foldScheduleEvents 内部会做 brandshape 校验——若强校验失败,
178
+ // 改由 session.inheritedEventCount 透传。这里保留双轨探测的安全门。
179
+ const folded = foldScheduleEvents(events, inheritedEventCount);
180
+ // 所有权与投递形态:sidecar 为准(新写入),日志中的历史 OWNED 事件作底
181
+ // (读兼容,旧日志可能仍有残留行)。
182
+ const sessionId = session.header?.id;
183
+ const owned = foldOwnedIds(events);
184
+ const delivery = new Map(foldOwnedDelivery(events));
185
+ if (typeof sessionId === 'string' && sessionId.length > 0) {
186
+ for (const [id, entry] of Object.entries(allOwnership(sessionId))) {
187
+ owned.add(id);
188
+ if (!delivery.has(id))
189
+ delivery.set(id, entry.delivery);
190
+ }
191
+ }
192
+ const state = {
193
+ folded,
194
+ owned,
195
+ delivery,
196
+ };
197
+ foldCache.set(session, { events, length: events.length, inheritedEventCount, state });
198
+ return state;
199
+ }
200
+ /** 稳定内部错误(不透出异常细节)。 */
201
+ export function internalError() {
202
+ return { ok: false, code: 'internal_error', message: '定时任务操作失败,请重试。' };
203
+ }
204
+ /** 稳定持久化不确定错误。 */
205
+ export function persistenceError() {
206
+ return {
207
+ ok: false,
208
+ code: 'persistence_uncertain',
209
+ message: '任务持久化未确认,请用 user_schedule_list 复核后再依赖该结果。',
210
+ };
211
+ }
212
+ /** 把 dsh-schedule 的 ScheduleInputError 翻译为闭包错误值。 */
213
+ export function toInputError(error) {
214
+ if (error instanceof ScheduleInputError) {
215
+ return { ok: false, code: error.code, message: error.message };
216
+ }
217
+ return internalError();
218
+ }
219
+ /** 把 dsh-schedule 的 view 序列化为 snake_case 记录。 */
220
+ export function serializeScheduleView(view) {
221
+ return {
222
+ id: view.id,
223
+ kind: view.kind,
224
+ prompt: view.prompt,
225
+ ...(view.kind === 'after' ? { after_seconds: view.afterSeconds } : {}),
226
+ ...(view.kind === 'every' ? { every_seconds: view.everySeconds } : {}),
227
+ scheduled_at: view.scheduledAt,
228
+ state: view.state,
229
+ delivery_mode: view.deliveryMode,
230
+ source: 'user-tool',
231
+ };
232
+ }
233
+ /** 每个 agent 一个事务尾,串行化用户的读改写操作。 */
234
+ const tails = new WeakMap();
235
+ /** 在当前 agent 的事务尾之后运行操作。 */
236
+ export function runUserScheduleTransaction(agent, operation) {
237
+ const run = (tails.get(agent) ?? Promise.resolve()).then(operation);
238
+ const tail = run.then(() => undefined, () => undefined);
239
+ tails.set(agent, tail);
240
+ try {
241
+ return run;
242
+ }
243
+ finally {
244
+ if (tails.get(agent) === tail)
245
+ tails.delete(agent);
246
+ }
247
+ }
248
+ /** 等待一个成功的持久化 barrier。 */
249
+ async function flushSession(ctx, session) {
250
+ try {
251
+ return (await ctx.sessions.flush(session)) === true;
252
+ }
253
+ catch {
254
+ return false;
255
+ }
256
+ }
257
+ /** 构造一条通过与 dsh-schedule 完全兼容的 `schedule/change` 记录。 */
258
+ function buildScheduleRecord(input, id, now) {
259
+ if (input.at !== undefined) {
260
+ return createAtScheduleRecord(id, input.prompt, input.at, now);
261
+ }
262
+ if (input.after_seconds !== undefined) {
263
+ return createAfterScheduleRecord(id, input.prompt, input.after_seconds, now);
264
+ }
265
+ return createEveryScheduleRecord(id, input.prompt, input.every_seconds, now);
266
+ }
267
+ /** `user_schedule_create` 核心实现。 */
268
+ export async function userScheduleCreate(input, agent, ctx, maxSchedules = DEFAULT_MAX_SCHEDULES, options, limits) {
269
+ const validated = validateCreateInput(input, limits);
270
+ if (!validated.ok)
271
+ return validated;
272
+ // 只认可信通道显式声明的形态;输入载荷中的 delivery 字段不具任何效力。
273
+ const trustedDelivery = options?.trustedDelivery === 'user' ? 'user' : 'context';
274
+ return runUserScheduleTransaction(agent, async () => {
275
+ const now = Date.now();
276
+ let folded;
277
+ let owned;
278
+ try {
279
+ const state = foldUserState(agent.session);
280
+ folded = state.folded;
281
+ owned = state.owned;
282
+ }
283
+ catch {
284
+ return { ok: false, code: 'internal_error', message: '会话定时日志读取失败。' };
285
+ }
286
+ const activeCount = folded.active.filter((record) => owned.has(record.id)).length;
287
+ if (activeCount >= maxSchedules) {
288
+ return {
289
+ ok: false,
290
+ code: 'quota_exceeded',
291
+ message: `当前会话最多 ${maxSchedules} 个定时任务,请先删除部分任务。`,
292
+ };
293
+ }
294
+ const id = allocateScheduleId(folded);
295
+ let record;
296
+ try {
297
+ record = buildScheduleRecord(validated.value, id, now);
298
+ }
299
+ catch (error) {
300
+ return toInputError(error);
301
+ }
302
+ // 所有权写 sidecar(append 之前:投影 fold 在事件到达时按 sidecar 判定归属)。
303
+ // flush 失败则回滚,避免 sidecar 与日志漂移。
304
+ const sessionId = agent.session.header?.id;
305
+ recordOwnership(sessionId, id, trustedDelivery);
306
+ try {
307
+ agent.session.append('schedule/change', {
308
+ version: 1,
309
+ operation: 'create',
310
+ schedule: record,
311
+ });
312
+ }
313
+ catch {
314
+ removeOwnership(sessionId, id);
315
+ return internalError();
316
+ }
317
+ if (!(await flushSession(ctx, agent.session))) {
318
+ removeOwnership(sessionId, id);
319
+ return persistenceError();
320
+ }
321
+ const view = scheduleView(record, Date.now());
322
+ return {
323
+ ok: true,
324
+ ...serializeScheduleView(view),
325
+ };
326
+ });
327
+ }
328
+ /** `user_schedule_list` 核心实现。 */
329
+ export async function userScheduleList(agent, ctx) {
330
+ return runUserScheduleTransaction(agent, async () => {
331
+ let folded;
332
+ let owned;
333
+ try {
334
+ const state = foldUserState(agent.session);
335
+ folded = state.folded;
336
+ owned = state.owned;
337
+ }
338
+ catch {
339
+ return { ok: false, code: 'internal_error', message: '会话定时日志读取失败。' };
340
+ }
341
+ const now = Date.now();
342
+ const schedules = folded.active
343
+ .filter((record) => owned.has(record.id))
344
+ .map((record) => serializeScheduleView(scheduleView(record, now)));
345
+ return { ok: true, schedules };
346
+ });
347
+ }
348
+ /** `user_schedule_delete` 核心实现。 */
349
+ export async function userScheduleDelete(id, agent, ctx) {
350
+ if (typeof id !== 'string' || id.length === 0 || id.trim() !== id) {
351
+ return { ok: false, code: 'invalid_rule', message: 'schedule id 必须是去空白非空字符串。' };
352
+ }
353
+ const scheduleId = ScheduleId(id);
354
+ return runUserScheduleTransaction(agent, async () => {
355
+ let folded;
356
+ try {
357
+ folded = foldUserState(agent.session).folded;
358
+ }
359
+ catch {
360
+ return { ok: false, code: 'internal_error', message: '会话定时日志读取失败。' };
361
+ }
362
+ if (!folded.active.some((record) => record.id === scheduleId)) {
363
+ return { ok: true, id, deleted: false, code: 'schedule_not_found' };
364
+ }
365
+ const sessionId = agent.session.header?.id;
366
+ try {
367
+ agent.session.append('schedule/change', {
368
+ version: 1,
369
+ operation: 'delete',
370
+ id: scheduleId,
371
+ });
372
+ }
373
+ catch {
374
+ return internalError();
375
+ }
376
+ if (!(await flushSession(ctx, agent.session)))
377
+ return persistenceError();
378
+ // flush 成功后再撤销所有权(失败时保留:日志里的 delete 可能未落盘,
379
+ // 运行时还要按 sidecar 继续追踪该任务)。
380
+ removeOwnership(sessionId, scheduleId);
381
+ return { ok: true, id, deleted: true };
382
+ });
383
+ }
384
+ /** `user_schedule_edit`(改内容)核心实现:删旧建新,保留原时刻与 delivery。 */
385
+ export async function userScheduleEditPrompt(id, newPrompt, agent, ctx, limits = { allowLong: false, maxChars: DEFAULT_MAX_PROMPT_CHARS }) {
386
+ if (typeof id !== 'string' || id.length === 0 || id.trim() !== id) {
387
+ return { ok: false, code: 'invalid_rule', message: 'schedule id 必须是去空白非空字符串。' };
388
+ }
389
+ const promptResult = validatePrompt(newPrompt, limits);
390
+ if (!promptResult.ok)
391
+ return promptResult;
392
+ const trimmed = promptResult.value;
393
+ const scheduleId = ScheduleId(id);
394
+ return runUserScheduleTransaction(agent, async () => {
395
+ let folded;
396
+ let deliveries;
397
+ try {
398
+ const state = foldUserState(agent.session);
399
+ folded = state.folded;
400
+ deliveries = state.delivery;
401
+ }
402
+ catch {
403
+ return { ok: false, code: 'internal_error', message: '会话定时日志读取失败。' };
404
+ }
405
+ const existing = folded.active.find((record) => record.id === scheduleId);
406
+ if (existing === undefined) {
407
+ return { ok: false, code: 'schedule_not_found', message: `未找到定时任务 ${scheduleId}。` };
408
+ }
409
+ // P0-4:已过期(overdue)的 at 任务禁止改内容——「保留原时刻」会让新记录
410
+ // 一落日志就处于到期状态,下一次 drive 立即注入,用户会以为只是改了文案
411
+ // 却被瞬间代发。要求先删除再重建,语义清晰可预期。
412
+ if (existing.kind === 'at' &&
413
+ Date.parse(existing.scheduledAt) <= Date.now()) {
414
+ return {
415
+ ok: false,
416
+ code: 'already_overdue',
417
+ message: `定时任务 ${scheduleId} 的目标时刻已过,不能修改内容;请删除后重新创建。`,
418
+ };
419
+ }
420
+ const now = Date.now();
421
+ let record;
422
+ try {
423
+ if (existing.kind === 'after') {
424
+ record = createAfterScheduleRecord(allocateScheduleId(folded), trimmed,
425
+ // 编辑会重置从「现在」起的倒计时;保留原间隔长度
426
+ existing.afterSeconds, now);
427
+ }
428
+ else if (existing.kind === 'at') {
429
+ record = createAtScheduleRecord(allocateScheduleId(folded), trimmed, existing.scheduledAt, // ISO 字符串,保留原时刻(不重置到未来)
430
+ now);
431
+ }
432
+ else {
433
+ record = createEveryScheduleRecord(allocateScheduleId(folded), trimmed, existing.everySeconds ?? 300, now);
434
+ }
435
+ }
436
+ catch (error) {
437
+ return toInputError(error);
438
+ }
439
+ const delivery = deliveries.get(scheduleId) ?? 'context';
440
+ // 新所有权先写 sidecar(create 事件到达投影 fold 时按 sidecar 判定归属);
441
+ // 旧条目在 flush 成功后再撤销(失败回滚两侧)。
442
+ const sessionId = agent.session.header?.id;
443
+ const previousEntry = getOwnership(sessionId, scheduleId);
444
+ recordOwnership(sessionId, record.id, delivery);
445
+ try {
446
+ agent.session.append('schedule/change', { version: 1, operation: 'delete', id: scheduleId });
447
+ agent.session.append('schedule/change', { version: 1, operation: 'create', schedule: record });
448
+ }
449
+ catch {
450
+ removeOwnership(sessionId, record.id);
451
+ return internalError();
452
+ }
453
+ if (!(await flushSession(ctx, agent.session))) {
454
+ removeOwnership(sessionId, record.id);
455
+ if (previousEntry !== undefined)
456
+ recordOwnership(sessionId, scheduleId, previousEntry.delivery);
457
+ return persistenceError();
458
+ }
459
+ removeOwnership(sessionId, scheduleId);
460
+ const view = scheduleView(record, Date.now());
461
+ return { ok: true, ...serializeScheduleView(view) };
462
+ });
463
+ }
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "dsh-later",
3
+ "version": "1.0.0",
4
+ "description": "会话内定时消息插件:在 dsh-schedule 引擎之上提供用户可直接操作的 GUI + 用户工具,实现「关网页也能准时触发的会话内定时提醒」。",
5
+ "keywords": [
6
+ "dsh",
7
+ "dsh-plugin",
8
+ "later",
9
+ "reminder",
10
+ "timer",
11
+ "session",
12
+ "deepseek-ai"
13
+ ],
14
+ "author": "WuJiaoJue",
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/WuJiaoJue/dsh-later.git"
19
+ },
20
+ "homepage": "https://github.com/WuJiaoJue/dsh-later",
21
+ "bugs": {
22
+ "url": "https://github.com/WuJiaoJue/dsh-later/issues"
23
+ },
24
+ "type": "module",
25
+ "main": "lib/index.js",
26
+ "types": "lib/types/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./lib/types/index.d.ts",
30
+ "import": "./lib/index.js"
31
+ },
32
+ "./client": "./lib/client.js",
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": [
36
+ "lib",
37
+ "cordis.patch.yml",
38
+ "README.md",
39
+ "README.en.md",
40
+ "CHANGELOG.md"
41
+ ],
42
+ "engines": {
43
+ "node": "^22.19 || >=24"
44
+ },
45
+ "scripts": {
46
+ "build": "node build.mjs",
47
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
48
+ "test": "node --test \"tests/*.test.mjs\"",
49
+ "prepublishOnly": "npm run build",
50
+ "watch": "node watch.mjs"
51
+ },
52
+ "peerDependencies": {
53
+ "@deepseek-ai/cordis": "^4.0.1",
54
+ "@deepseek-ai/dsh-agent": "^0.1.1-rc.2 || ^0.1.2-rc.1",
55
+ "@deepseek-ai/dsh-commands": "^0.0.1-rc.1 || ^0.1.1-rc.2 || ^0.1.2-rc.1",
56
+ "@deepseek-ai/dsh-llm": "^0.1.1-rc.2 || ^0.1.2-rc.1",
57
+ "@deepseek-ai/dsh-schedule": "^0.1.1-rc.2 || ^0.1.2-rc.1",
58
+ "@deepseek-ai/dsh-session": "^0.1.1-rc.2 || ^0.1.2-rc.1",
59
+ "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.2 || ^0.1.2-rc.1",
60
+ "@deepseek-ai/dsh-settings": "^0.1.1-rc.2 || ^0.1.2-rc.1",
61
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.2 || ^0.1.2-rc.1",
62
+ "@deepseek-ai/schemastery": "^3.18.1"
63
+ },
64
+ "devDependencies": {
65
+ "@deepseek-ai/cordis": "^4.0.1",
66
+ "@deepseek-ai/dsh-agent": "^0.1.1-rc.2 || ^0.1.2-rc.1",
67
+ "@deepseek-ai/dsh-commands": "^0.0.1-rc.1 || ^0.1.1-rc.2 || ^0.1.2-rc.1",
68
+ "@deepseek-ai/dsh-llm": "^0.1.1-rc.2 || ^0.1.2-rc.1",
69
+ "@deepseek-ai/dsh-schedule": "^0.1.1-rc.2 || ^0.1.2-rc.1",
70
+ "@deepseek-ai/dsh-session": "^0.1.1-rc.2 || ^0.1.2-rc.1",
71
+ "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.2 || ^0.1.2-rc.1",
72
+ "@deepseek-ai/dsh-settings": "^0.1.1-rc.2 || ^0.1.2-rc.1",
73
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.2 || ^0.1.2-rc.1",
74
+ "@deepseek-ai/schemastery": "^3.18.1",
75
+ "@types/node": "^22.0.0",
76
+ "@types/react": "^19.2.18",
77
+ "@types/react-dom": "^19.2.5",
78
+ "esbuild": "^0.25.0",
79
+ "typescript": "^5.9.0",
80
+ "zod": "^4.0.0"
81
+ },
82
+ "dsh": {
83
+ "bundle": {
84
+ "patch": "./cordis.patch.yml"
85
+ },
86
+ "client": {
87
+ "platform": "web",
88
+ "inject": [
89
+ "@deepseek-ai/dsh-client-runtime",
90
+ "@deepseek-ai/dsh-client-ui-conversation"
91
+ ]
92
+ }
93
+ }
94
+ }