@dshtrading/watchlist 0.1.5 → 0.2.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.
@@ -1,6 +1,7 @@
1
- import { SelectionStore, WatchlistStore } from "./index.js";
1
+ import { SelectionStore, WatchlistGroupsStore, WatchlistStore } from "./index.js";
2
2
  //#region src/file-store.d.ts
3
3
  declare function createFileWatchlistStore(filePath: string): WatchlistStore;
4
+ declare function createFileWatchlistGroupsStore(filePath: string): WatchlistGroupsStore;
4
5
  declare function createFileSelectionStore(filePath: string): SelectionStore;
5
6
  //#endregion
6
- export { createFileSelectionStore, createFileWatchlistStore };
7
+ export { createFileSelectionStore, createFileWatchlistGroupsStore, createFileWatchlistStore };
package/lib/file-store.js CHANGED
@@ -1,38 +1,14 @@
1
1
  import { WATCHLIST_SEEDS } from "./seeds.js";
2
- import { dirname } from "node:path";
3
- import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
2
+ import { newWatchlistGroupId, normalizeWatchlistRow } from "./index.js";
3
+ import { writeJsonAtomic } from "@dshtrading/dsh-home";
4
+ import { readFile } from "node:fs/promises";
4
5
  //#region src/file-store.ts
5
6
  /**
6
7
  * 文件持久化版自选/选中 store(Node.js 宿主侧使用,tmp+rename 原子写,
7
8
  * indicators/custom-fs 同款模式——issue #32 规格)。
8
9
  */
9
- /**
10
- * 跨平台健壮原子写入:rename 遇 Windows EPERM/EBUSY(目标被占用)短暂退避重试;
11
- * 重试耗尽或任何失败一律保留旧文件、清理 tmp、log + throw——目标文件永远只被
12
- * 原子 rename 触碰,绝不非原子直写(防止半截写损坏 JSON 导致 load 静默重置)。
13
- */
14
- async function safeAtomicWrite(filePath, data) {
15
- const dir = dirname(filePath);
16
- const tmpPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
17
- try {
18
- await mkdir(dir, { recursive: true });
19
- await writeFile(tmpPath, data, "utf8");
20
- let lastError;
21
- for (let attempt = 0; attempt < 3; attempt++) try {
22
- await rename(tmpPath, filePath);
23
- return;
24
- } catch (err) {
25
- if (err?.code !== "EPERM" && err?.code !== "EBUSY") throw err;
26
- lastError = err;
27
- await new Promise((resolve) => setTimeout(resolve, 25 * (attempt + 1)));
28
- }
29
- throw lastError;
30
- } catch (error) {
31
- console.error(`[dsh-trading/watchlist] failed to atomic flush watchlists to ${filePath}:`, error);
32
- await unlink(tmpPath).catch(() => {});
33
- throw error;
34
- }
35
- }
10
+ /** 原子写唯一实现在 @dshtrading/dsh-home(tmp+rename、EPERM/EBUSY 重试、失败保留旧文件并清理 tmp)。 */
11
+ const safeAtomicWrite = (filePath, data) => writeJsonAtomic(filePath, data, "[dsh-trading/watchlist] failed to atomic flush watchlists to");
36
12
  function createFileWatchlistStore(filePath) {
37
13
  let cache = null;
38
14
  let pendingWrite = Promise.resolve();
@@ -55,7 +31,7 @@ function createFileWatchlistStore(filePath) {
55
31
  }
56
32
  /** 仅在 enqueue 队列内调用:写盘不能二次入队(外层已持队,内层 enqueue 会自等死锁)。 */
57
33
  function writeNow(map) {
58
- return safeAtomicWrite(filePath, JSON.stringify(map, null, 2));
34
+ return safeAtomicWrite(filePath, map);
59
35
  }
60
36
  async function flush(map) {
61
37
  return enqueue(async () => {
@@ -77,7 +53,7 @@ function createFileWatchlistStore(filePath) {
77
53
  if (rows.some((row) => row.symbol === instrument.symbol)) return false;
78
54
  const next = {
79
55
  ...map,
80
- [market]: [...rows, { ...instrument }]
56
+ [market]: [...rows, normalizeWatchlistRow(instrument)]
81
57
  };
82
58
  cache = next;
83
59
  await writeNow(next);
@@ -99,6 +75,134 @@ function createFileWatchlistStore(filePath) {
99
75
  await writeNow(next);
100
76
  return true;
101
77
  });
78
+ },
79
+ async assignGroup(market, symbol, groupId, member) {
80
+ return enqueue(async () => {
81
+ const map = await load();
82
+ const rows = map[market];
83
+ if (!Array.isArray(rows)) return false;
84
+ const index = rows.findIndex((row) => row.symbol === symbol);
85
+ if (index < 0) return false;
86
+ const base = rows[index];
87
+ if (base === void 0) return false;
88
+ const current = Array.isArray(base.groups) ? base.groups : [];
89
+ if (member === current.includes(groupId)) return false;
90
+ const nextGroups = member ? [...current, groupId] : current.filter((id) => id !== groupId);
91
+ const nextRows = [...rows];
92
+ nextRows[index] = normalizeWatchlistRow({
93
+ market: base.market,
94
+ symbol: base.symbol,
95
+ ...base.name !== void 0 ? { name: base.name } : {},
96
+ ...nextGroups.length > 0 ? { groups: nextGroups } : {}
97
+ });
98
+ const next = {
99
+ ...map,
100
+ [market]: nextRows
101
+ };
102
+ cache = next;
103
+ await writeNow(next);
104
+ return true;
105
+ });
106
+ },
107
+ async stripGroup(groupId) {
108
+ return enqueue(async () => {
109
+ const map = await load();
110
+ let cleaned = 0;
111
+ const next = {};
112
+ for (const [market, rows] of Object.entries(map)) {
113
+ if (!Array.isArray(rows)) continue;
114
+ let changed = false;
115
+ const nextRows = rows.map((row) => {
116
+ const groups = Array.isArray(row.groups) ? row.groups : [];
117
+ if (!groups.includes(groupId)) return row;
118
+ changed = true;
119
+ cleaned++;
120
+ const rest = groups.filter((id) => id !== groupId);
121
+ return normalizeWatchlistRow({
122
+ market: row.market,
123
+ symbol: row.symbol,
124
+ ...row.name !== void 0 ? { name: row.name } : {},
125
+ ...rest.length > 0 ? { groups: rest } : {}
126
+ });
127
+ });
128
+ next[market] = changed ? nextRows : rows;
129
+ }
130
+ if (cleaned > 0) {
131
+ cache = next;
132
+ await writeNow(next);
133
+ }
134
+ return cleaned;
135
+ });
136
+ }
137
+ };
138
+ }
139
+ function createFileWatchlistGroupsStore(filePath) {
140
+ let cache = null;
141
+ let pendingWrite = Promise.resolve();
142
+ const enqueue = (fn) => {
143
+ const next = pendingWrite.then(fn, fn);
144
+ pendingWrite = next.then(() => {}, () => {});
145
+ return next;
146
+ };
147
+ async function load() {
148
+ if (cache !== null) return cache;
149
+ try {
150
+ const content = await readFile(filePath, "utf8");
151
+ const parsed = JSON.parse(content);
152
+ cache = Array.isArray(parsed.groups) ? parsed.groups.filter((group) => typeof group === "object" && group !== null && typeof group.id === "string" && group.id !== "" && typeof group.name === "string") : [];
153
+ } catch (err) {
154
+ if (err?.code !== "ENOENT") console.error(`[dsh-trading/watchlist] failed to read watchlist groups from ${filePath}:`, err);
155
+ cache = [];
156
+ }
157
+ return cache;
158
+ }
159
+ function writeNow(groups) {
160
+ return safeAtomicWrite(filePath, { groups });
161
+ }
162
+ return {
163
+ async list() {
164
+ return [...await load()];
165
+ },
166
+ async create(name) {
167
+ return enqueue(async () => {
168
+ const groups = await load();
169
+ if (groups.some((group) => group.name === name)) return { error: "duplicate" };
170
+ const group = {
171
+ id: newWatchlistGroupId(),
172
+ name,
173
+ createdAt: Date.now()
174
+ };
175
+ cache = [...groups, group];
176
+ await writeNow(cache);
177
+ return { group };
178
+ });
179
+ },
180
+ async rename(id, name) {
181
+ return enqueue(async () => {
182
+ const groups = await load();
183
+ const existing = groups.find((group) => group.id === id);
184
+ if (existing === void 0) return { error: "not-found" };
185
+ if (groups.some((group) => group.id !== id && group.name === name)) return { error: "duplicate" };
186
+ cache = groups.map((group) => group.id === id ? {
187
+ ...group,
188
+ name
189
+ } : group);
190
+ await writeNow(cache);
191
+ return { group: {
192
+ ...existing,
193
+ name
194
+ } };
195
+ });
196
+ },
197
+ async remove(id) {
198
+ return enqueue(async () => {
199
+ const groups = await load();
200
+ const next = groups.filter((group) => group.id !== id);
201
+ if (next.length === groups.length) return false;
202
+ cache = next;
203
+ await writeNow(next);
204
+ return true;
205
+ });
102
206
  }
103
207
  };
104
208
  }
@@ -124,7 +228,7 @@ function createFileSelectionStore(filePath) {
124
228
  }
125
229
  async function flush(record) {
126
230
  return enqueue(async () => {
127
- await safeAtomicWrite(filePath, JSON.stringify(record, null, 2));
231
+ await safeAtomicWrite(filePath, record);
128
232
  });
129
233
  }
130
234
  return {
@@ -139,4 +243,4 @@ function createFileSelectionStore(filePath) {
139
243
  };
140
244
  }
141
245
  //#endregion
142
- export { createFileSelectionStore, createFileWatchlistStore };
246
+ export { createFileSelectionStore, createFileWatchlistGroupsStore, createFileWatchlistStore };
package/lib/index.d.ts CHANGED
@@ -11,10 +11,13 @@ import { WATCHLIST_SEEDS, effectiveWatchlistRows, watchlistRowSource } from "./s
11
11
  * 本模块(纯类型 + 内存 store,浏览器安全):
12
12
  * - `WatchlistsMap`:按 market 分桶的多列表行(rows 只存用户定制行;市场种子列表
13
13
  * 属客户端展示回退,不进 host store——空 market = 未定制,客户端回落种子)。
14
+ * 行可带 `groups`(自定义分组 id 多归属,issue #82);分组注册表另存
15
+ * watchlist-groups.json(`./file-store.ts` 的 groups store)。
14
16
  * - `SelectionRecord`:跨市场选中标的(watchlist_select 工具与左栏点击同源)。
15
- * - `./plugin` 子路径:file store(~/.dsh/watchlists.json、~/.dsh/selection.json,
16
- * tmp+rename 原子写)+ host 平面工具 watchlist_list/add/remove/select(全会话可见,
17
- * owner 裁决 D4;select/add/remove 后 emit tradingEvents('watchlists'|'selection'))。
17
+ * - `./plugin` 子路径:file store(~/.dsh/watchlists.json、~/.dsh/selection.json、
18
+ * ~/.dsh/watchlist-groups.json,tmp+rename 原子写)+ host 平面工具
19
+ * watchlist_list/add/remove/select(全会话可见,owner 裁决 D4;select/add/remove
20
+ * 后 emit tradingEvents('watchlists'|'selection'))。
18
21
  *
19
22
  * 词汇纪律:symbol 用市场规范形(docs/symbol-vocabulary.md),本包不做归一化
20
23
  * (写入方负责——工具参数与桥端点均原样落盘)。
@@ -27,9 +30,33 @@ interface WatchlistInstrument {
27
30
  symbol: string;
28
31
  /** 展示名(可选;工具添加可缺省,客户端以 symbol 兜底展示)。 */
29
32
  name?: string;
33
+ /**
34
+ * 所属自定义分组 id 数组(issue #82;多归属,一行可入多组;缺省/空 = 未分组)。
35
+ * 分组注册表(名称等元数据)在 ./file-store.ts 的 groups store,行上只存 id。
36
+ */
37
+ groups?: string[];
30
38
  }
31
39
  /** 多列表自选:market → 用户定制行数组(空数组/缺键 = 未定制)。 */
32
40
  type WatchlistsMap = Record<string, WatchlistInstrument[]>;
41
+ /** 自定义分组注册表行(issue #82;成员关系在标的行的 groups 字段,此处只存元数据)。 */
42
+ interface WatchlistGroup {
43
+ id: string;
44
+ /** 展示名(桥面保证 trim 非空;同名单向拒绝)。 */
45
+ name: string;
46
+ createdAt: number;
47
+ }
48
+ /** 分组写操作结果(桥面转业务 ok:false + reason;'duplicate' = 同名已存在,'not-found' = id 不存在)。 */
49
+ interface WatchlistGroupWriteResult {
50
+ group?: WatchlistGroup;
51
+ error?: 'duplicate' | 'not-found';
52
+ }
53
+ /** 自定义分组注册表 store(元数据;成员关系的读写走 WatchlistStore.assignGroup/stripGroup)。 */
54
+ interface WatchlistGroupsStore {
55
+ list(): Promise<WatchlistGroup[]>;
56
+ create(name: string): Promise<WatchlistGroupWriteResult>;
57
+ rename(id: string, name: string): Promise<WatchlistGroupWriteResult>;
58
+ remove(id: string): Promise<boolean>;
59
+ }
33
60
  /** 选中标的记录(中栏切图的 SSOT)。 */
34
61
  interface SelectionRecord {
35
62
  instrument: WatchlistInstrument | null;
@@ -43,14 +70,24 @@ interface WatchlistStore {
43
70
  add(market: string, instrument: WatchlistInstrument): Promise<boolean>;
44
71
  /** 移除一行;返回是否 existed。 */
45
72
  remove(market: string, symbol: string): Promise<boolean>;
73
+ /** 加入/移出分组(issue #82):行缺席或归属无变化返回 false(读改写入队串行化)。 */
74
+ assignGroup(market: string, symbol: string, groupId: string, member: boolean): Promise<boolean>;
75
+ /** 清掉所有行上的某分组归属(删分组时调用);返回清洗行数,无变化不落盘。 */
76
+ stripGroup(groupId: string): Promise<number>;
46
77
  }
47
78
  interface SelectionStore {
48
79
  get(): Promise<SelectionRecord>;
49
80
  set(record: SelectionRecord): Promise<void>;
50
81
  }
82
+ /** 行规范化副本:groups 空数组不落键,保持落盘/输出形状干净。 */
83
+ declare function normalizeWatchlistRow(row: WatchlistInstrument): WatchlistInstrument;
84
+ /** 新分组 id(时间戳 base36 + 随机尾,进程内唯一即可——注册表单文件单写者)。 */
85
+ declare function newWatchlistGroupId(): string;
51
86
  /** 内存版自选 store(单测用)。 */
52
87
  declare function createMemoryWatchlistStore(initial?: WatchlistsMap): WatchlistStore;
88
+ /** 内存版分组注册表 store(单测用)。同名拒绝(trim 后精确匹配);调用方负责非空校验。 */
89
+ declare function createMemoryWatchlistGroupsStore(initial?: WatchlistGroup[]): WatchlistGroupsStore;
53
90
  /** 内存版选中 store(单测用)。 */
54
91
  declare function createMemorySelectionStore(initial?: SelectionRecord): SelectionStore;
55
92
  //#endregion
56
- export { SelectionRecord, SelectionStore, WATCHLIST_SEEDS, WatchlistInstrument, WatchlistStore, WatchlistsMap, createMemorySelectionStore, createMemoryWatchlistStore, effectiveWatchlistRows, watchlistRowSource };
93
+ export { SelectionRecord, SelectionStore, WATCHLIST_SEEDS, WatchlistGroup, WatchlistGroupWriteResult, WatchlistGroupsStore, WatchlistInstrument, WatchlistStore, WatchlistsMap, createMemorySelectionStore, createMemoryWatchlistGroupsStore, createMemoryWatchlistStore, effectiveWatchlistRows, newWatchlistGroupId, normalizeWatchlistRow, watchlistRowSource };
package/lib/index.js CHANGED
@@ -1,5 +1,19 @@
1
1
  import { WATCHLIST_SEEDS, effectiveWatchlistRows, watchlistRowSource } from "./seeds.js";
2
2
  //#region src/index.ts
3
+ /** 行规范化副本:groups 空数组不落键,保持落盘/输出形状干净。 */
4
+ function normalizeWatchlistRow(row) {
5
+ const groups = Array.isArray(row.groups) ? [...new Set(row.groups.filter((item) => typeof item === "string" && item))] : [];
6
+ return {
7
+ market: row.market,
8
+ symbol: row.symbol,
9
+ ...row.name !== void 0 ? { name: row.name } : {},
10
+ ...groups.length > 0 ? { groups } : {}
11
+ };
12
+ }
13
+ /** 新分组 id(时间戳 base36 + 随机尾,进程内唯一即可——注册表单文件单写者)。 */
14
+ function newWatchlistGroupId() {
15
+ return `g_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
16
+ }
3
17
  /** 内存版自选 store(单测用)。 */
4
18
  function createMemoryWatchlistStore(initial = {}) {
5
19
  let map = { ...initial };
@@ -15,7 +29,7 @@ function createMemoryWatchlistStore(initial = {}) {
15
29
  if (rows.some((row) => row.symbol === instrument.symbol)) return false;
16
30
  map = {
17
31
  ...map,
18
- [market]: [...rows, { ...instrument }]
32
+ [market]: [...rows, normalizeWatchlistRow(instrument)]
19
33
  };
20
34
  return true;
21
35
  },
@@ -29,6 +43,91 @@ function createMemoryWatchlistStore(initial = {}) {
29
43
  [market]: next
30
44
  };
31
45
  return true;
46
+ },
47
+ async assignGroup(market, symbol, groupId, member) {
48
+ const rows = map[market];
49
+ if (!Array.isArray(rows)) return false;
50
+ const index = rows.findIndex((row) => row.symbol === symbol);
51
+ if (index < 0) return false;
52
+ const base = rows[index];
53
+ if (base === void 0) return false;
54
+ const current = Array.isArray(base.groups) ? base.groups : [];
55
+ if (member === current.includes(groupId)) return false;
56
+ const nextGroups = member ? [...current, groupId] : current.filter((id) => id !== groupId);
57
+ const nextRows = [...rows];
58
+ nextRows[index] = normalizeWatchlistRow({
59
+ market: base.market,
60
+ symbol: base.symbol,
61
+ ...base.name !== void 0 ? { name: base.name } : {},
62
+ ...nextGroups.length > 0 ? { groups: nextGroups } : {}
63
+ });
64
+ map = {
65
+ ...map,
66
+ [market]: nextRows
67
+ };
68
+ return true;
69
+ },
70
+ async stripGroup(groupId) {
71
+ let cleaned = 0;
72
+ const next = {};
73
+ for (const [market, rows] of Object.entries(map)) {
74
+ if (!Array.isArray(rows)) continue;
75
+ let changed = false;
76
+ const nextRows = rows.map((row) => {
77
+ const groups = Array.isArray(row.groups) ? row.groups : [];
78
+ if (!groups.includes(groupId)) return row;
79
+ changed = true;
80
+ cleaned++;
81
+ const rest = groups.filter((id) => id !== groupId);
82
+ return normalizeWatchlistRow({
83
+ market: row.market,
84
+ symbol: row.symbol,
85
+ ...row.name !== void 0 ? { name: row.name } : {},
86
+ ...rest.length > 0 ? { groups: rest } : {}
87
+ });
88
+ });
89
+ next[market] = changed ? nextRows : rows;
90
+ }
91
+ if (cleaned > 0) map = next;
92
+ return cleaned;
93
+ }
94
+ };
95
+ }
96
+ /** 内存版分组注册表 store(单测用)。同名拒绝(trim 后精确匹配);调用方负责非空校验。 */
97
+ function createMemoryWatchlistGroupsStore(initial = []) {
98
+ let groups = [...initial];
99
+ return {
100
+ async list() {
101
+ return [...groups];
102
+ },
103
+ async create(name) {
104
+ if (groups.some((group) => group.name === name)) return { error: "duplicate" };
105
+ const group = {
106
+ id: newWatchlistGroupId(),
107
+ name,
108
+ createdAt: Date.now()
109
+ };
110
+ groups = [...groups, group];
111
+ return { group };
112
+ },
113
+ async rename(id, name) {
114
+ const existing = groups.find((group) => group.id === id);
115
+ if (existing === void 0) return { error: "not-found" };
116
+ if (groups.some((group) => group.id !== id && group.name === name)) return { error: "duplicate" };
117
+ groups = groups.map((group) => group.id === id ? {
118
+ ...group,
119
+ name
120
+ } : group);
121
+ return { group: {
122
+ ...existing,
123
+ name
124
+ } };
125
+ },
126
+ async remove(id) {
127
+ const next = groups.filter((group) => group.id !== id);
128
+ if (next.length === groups.length) return false;
129
+ groups = next;
130
+ return true;
32
131
  }
33
132
  };
34
133
  }
@@ -48,4 +147,4 @@ function createMemorySelectionStore(initial = { instrument: null }) {
48
147
  };
49
148
  }
50
149
  //#endregion
51
- export { WATCHLIST_SEEDS, createMemorySelectionStore, createMemoryWatchlistStore, effectiveWatchlistRows, watchlistRowSource };
150
+ export { WATCHLIST_SEEDS, createMemorySelectionStore, createMemoryWatchlistGroupsStore, createMemoryWatchlistStore, effectiveWatchlistRows, newWatchlistGroupId, normalizeWatchlistRow, watchlistRowSource };
package/lib/plugin.d.ts CHANGED
@@ -1,14 +1,16 @@
1
- import { SelectionStore, WatchlistStore, createMemorySelectionStore, createMemoryWatchlistStore } from "./index.js";
2
- import { createFileSelectionStore, createFileWatchlistStore } from "./file-store.js";
3
- import { Context } from "@deepseek-ai/cordis";
1
+ import { SelectionStore, WatchlistGroupsStore, WatchlistStore, createMemorySelectionStore, createMemoryWatchlistGroupsStore, createMemoryWatchlistStore } from "./index.js";
2
+ import { createFileSelectionStore, createFileWatchlistGroupsStore, createFileWatchlistStore } from "./file-store.js";
3
+ import { Context, Service } from "@deepseek-ai/cordis";
4
4
  //#region src/plugin.d.ts
5
5
  /** Cordis 插件名 = patch 行 id(TEMPLATES §8),市场无关共享行命名空间。 */
6
6
  declare const name = "dsh-trading-watchlist";
7
7
  /** 本插件不硬依赖任何服务;tools 经 ctx.inject 声明。 */
8
8
  declare const inject: string[];
9
- /** 默认存储路径。 */
9
+ /** 默认存储路径:$DSH_HOME/watchlists.json(缺省 ~/.dsh)。 */
10
10
  declare function defaultWatchlistStorePath(): string;
11
11
  declare function defaultSelectionStorePath(): string;
12
+ /** 默认分组注册表路径:$DSH_HOME/watchlist-groups.json(issue #82)。 */
13
+ declare function defaultWatchlistGroupsStorePath(): string;
12
14
  /** tradingEvents 的最小发布面(鸭式;总线缺席时静默降级)。 */
13
15
  interface TradingEventsPublisher {
14
16
  emit(store: 'watchlists' | 'selection'): void;
@@ -16,6 +18,8 @@ interface TradingEventsPublisher {
16
18
  interface WatchlistToolDeps {
17
19
  watchlists: WatchlistStore;
18
20
  selection: SelectionStore;
21
+ /** 分组注册表(issue #86 / G5):缺席时分组工具明确报错,不静默降级。 */
22
+ groups?: WatchlistGroupsStore;
19
23
  onWatchlistsChanged?: () => void;
20
24
  onSelectionChanged?: () => void;
21
25
  }
@@ -23,12 +27,37 @@ declare function createWatchlistListTool(deps: WatchlistToolDeps): import("@deep
23
27
  declare function createWatchlistAddTool(deps: WatchlistToolDeps): import("@deepseek-ai/dsh-tools").ToolDefinition;
24
28
  declare function createWatchlistRemoveTool(deps: WatchlistToolDeps): import("@deepseek-ai/dsh-tools").ToolDefinition;
25
29
  declare function createWatchlistSelectTool(deps: WatchlistToolDeps): import("@deepseek-ai/dsh-tools").ToolDefinition;
30
+ declare function createWatchlistGroupCreateTool(deps: WatchlistToolDeps): import("@deepseek-ai/dsh-tools").ToolDefinition;
31
+ declare function createWatchlistGroupRenameTool(deps: WatchlistToolDeps): import("@deepseek-ai/dsh-tools").ToolDefinition;
32
+ declare function createWatchlistGroupDeleteTool(deps: WatchlistToolDeps): import("@deepseek-ai/dsh-tools").ToolDefinition;
33
+ declare function createWatchlistGroupAssignTool(deps: WatchlistToolDeps): import("@deepseek-ai/dsh-tools").ToolDefinition;
26
34
  interface WatchlistPluginDeps {
27
35
  watchlists: WatchlistStore;
28
36
  selection: SelectionStore;
37
+ groups: WatchlistGroupsStore;
29
38
  }
30
39
  declare function registerWatchlistTools(ctx: Context, deps: WatchlistPluginDeps): void;
40
+ /** SDK 服务键:自选/选中 store 单实例(桥与工具共享同一缓存;2026-09-08 审查 H1 收口)。 */
41
+ declare const TRADING_WATCHLIST_KEY = "tradingWatchlist";
42
+ /**
43
+ * 自选 + 选中 store 服务(单实例共享点,strategies/indicators 同款 provide 模式)。
44
+ *
45
+ * 2026-09-08 审查实证:桥(client-ui-trading node 半)此前自建第二个 file store,
46
+ * 与工具侧实例各持整表缓存、各自整表回写 → 两侧写互相覆盖(agent `watchlist_add`
47
+ * 抹掉 GUI 刚写入的行与全部分组归属)。服务缺席(老部署)时桥仍回退自建实例。
48
+ */
49
+ declare class WatchlistStoreService extends Service {
50
+ readonly store: WatchlistStore;
51
+ readonly selection: SelectionStore;
52
+ /** 分组注册表单实例(issue #86 / G5):工具与桥共用,杜绝双 store 互踩。 */
53
+ readonly groups: WatchlistGroupsStore;
54
+ constructor(ctx: Context, deps: {
55
+ watchlists: WatchlistStore;
56
+ selection: SelectionStore;
57
+ groups: WatchlistGroupsStore;
58
+ }, serviceName?: string);
59
+ }
31
60
  /** Host plugin body:file store provide + 4 工具注册(host 平面,全会话可见)。 */
32
61
  declare function apply(ctx: Context): void;
33
62
  //#endregion
34
- export { TradingEventsPublisher, WatchlistPluginDeps, WatchlistToolDeps, apply, createFileSelectionStore, createFileWatchlistStore, createMemorySelectionStore, createMemoryWatchlistStore, createWatchlistAddTool, createWatchlistListTool, createWatchlistRemoveTool, createWatchlistSelectTool, defaultSelectionStorePath, defaultWatchlistStorePath, inject, name, registerWatchlistTools };
63
+ export { TRADING_WATCHLIST_KEY, TradingEventsPublisher, WatchlistPluginDeps, WatchlistStoreService, WatchlistToolDeps, apply, createFileSelectionStore, createFileWatchlistGroupsStore, createFileWatchlistStore, createMemorySelectionStore, createMemoryWatchlistGroupsStore, createMemoryWatchlistStore, createWatchlistAddTool, createWatchlistGroupAssignTool, createWatchlistGroupCreateTool, createWatchlistGroupDeleteTool, createWatchlistGroupRenameTool, createWatchlistListTool, createWatchlistRemoveTool, createWatchlistSelectTool, defaultSelectionStorePath, defaultWatchlistGroupsStorePath, defaultWatchlistStorePath, inject, name, registerWatchlistTools };
package/lib/plugin.js CHANGED
@@ -1,20 +1,25 @@
1
1
  import { WATCHLIST_SEEDS, effectiveWatchlistRows, watchlistRowSource } from "./seeds.js";
2
- import { createMemorySelectionStore, createMemoryWatchlistStore } from "./index.js";
3
- import { createFileSelectionStore, createFileWatchlistStore } from "./file-store.js";
2
+ import { createMemorySelectionStore, createMemoryWatchlistGroupsStore, createMemoryWatchlistStore } from "./index.js";
3
+ import { createFileSelectionStore, createFileWatchlistGroupsStore, createFileWatchlistStore } from "./file-store.js";
4
+ import { Service } from "@deepseek-ai/cordis";
4
5
  import { defineTool } from "@deepseek-ai/dsh-tools";
5
- import os from "node:os";
6
6
  import path from "node:path";
7
+ import { dshHomeDir } from "@dshtrading/dsh-home";
7
8
  //#region src/plugin.ts
8
9
  /** Cordis 插件名 = patch 行 id(TEMPLATES §8),市场无关共享行命名空间。 */
9
10
  const name = "dsh-trading-watchlist";
10
11
  /** 本插件不硬依赖任何服务;tools 经 ctx.inject 声明。 */
11
12
  const inject = [];
12
- /** 默认存储路径。 */
13
+ /** 默认存储路径:$DSH_HOME/watchlists.json(缺省 ~/.dsh)。 */
13
14
  function defaultWatchlistStorePath() {
14
- return path.join(os.homedir(), ".dsh", "watchlists.json");
15
+ return path.join(dshHomeDir(), "watchlists.json");
15
16
  }
16
17
  function defaultSelectionStorePath() {
17
- return path.join(os.homedir(), ".dsh", "selection.json");
18
+ return path.join(dshHomeDir(), "selection.json");
19
+ }
20
+ /** 默认分组注册表路径:$DSH_HOME/watchlist-groups.json(issue #82)。 */
21
+ function defaultWatchlistGroupsStorePath() {
22
+ return path.join(dshHomeDir(), "watchlist-groups.json");
18
23
  }
19
24
  function eventsOf(ctx) {
20
25
  return ctx.get?.("tradingEvents", false);
@@ -27,13 +32,36 @@ function parseInstrumentArgs(raw) {
27
32
  return {
28
33
  market,
29
34
  symbol,
30
- name
35
+ ...name !== void 0 ? { name } : {}
31
36
  };
32
37
  }
38
+ /** 分组注册表取用(缺席 = 老部署,写工具必须响亮失败而不是假装成功)。 */
39
+ function requireGroups(deps, tool) {
40
+ if (deps.groups === void 0) throw new Error(`${tool}: watchlist group store is not mounted — upgrade the dsh-trading profile so @dshtrading/watchlist/plugin provides it`);
41
+ return deps.groups;
42
+ }
43
+ /** 分组名校验(与桥 parseGroupNameBody 同口径:trim 非空、≤24 字符)。 */
44
+ function parseGroupName(raw, tool) {
45
+ const name = typeof raw === "string" ? raw.trim() : "";
46
+ if (!name) throw new Error(`${tool}: name is required (non-empty string)`);
47
+ if (name.length > 24) throw new Error(`${tool}: group name too long (24 chars max, got ${name.length})`);
48
+ return name;
49
+ }
50
+ function parseGroupId(raw, tool) {
51
+ const id = typeof raw === "string" ? raw.trim() : "";
52
+ if (!id) throw new Error(`${tool}: id is required (a group id from watchlist_list / watchlist_group_create)`);
53
+ return id;
54
+ }
55
+ /** 全表统计某分组的成员行数(合并视图口径 = 用户实际看到的行)。 */
56
+ function countGroupMembers(map, groupId) {
57
+ let count = 0;
58
+ for (const market of Object.keys(map)) for (const row of effectiveWatchlistRows(map, market)) if (Array.isArray(row.groups) && row.groups.includes(groupId)) count += 1;
59
+ return count;
60
+ }
33
61
  function createWatchlistListTool(deps) {
34
62
  return defineTool({
35
63
  name: "watchlist_list",
36
- description: "List the user's watchlist exactly as displayed in the trading GUI sidebar, across all markets (crypto/us/cn/hk). Rows merge the user's customized entries (source \"custom\") with each market's default seed rows (source \"seed\", shown while that market has no custom edits) — the GUI shows the same rows, so this list IS what the user sees. It also maps display names to symbols (e.g. 苹果 → AAPL / us, 贵州茅台 → 600519 / cn). ALWAYS call this first when the user mentions any instrument by name or symbol; never conclude an instrument is untracked from docs or connector coverage alone. Read-only.",
64
+ description: "List the user's watchlist exactly as displayed in the trading GUI sidebar, across all markets (crypto/us/cn/hk). Rows merge the user's customized entries (source \"custom\") with each market's default seed rows (source \"seed\", shown while that market has no custom edits) — the GUI shows the same rows, so this list IS what the user sees. It also maps display names to symbols (e.g. 苹果 → AAPL / us, 贵州茅台 → 600519 / cn). It also returns \"groups\" (the user's custom groups with id, name and member count) and \"selection\" (the instrument currently focused in the GUI chart, or null). Group membership lives on each row as row.groups (group ids); rows without groups are ungrouped. ALWAYS call this first when the user mentions any instrument by name or symbol, when you need a group id for watchlist_group_assign/rename/delete, or when you need the current selection; never conclude an instrument is untracked from docs or connector coverage alone. Read-only.",
37
65
  parameters: {},
38
66
  output: {
39
67
  schema: { type: "string" },
@@ -54,12 +82,21 @@ function createWatchlistListTool(deps) {
54
82
  sources[market] = watchlistRowSource(map, market);
55
83
  total += rows.length;
56
84
  }
85
+ const groups = (deps.groups === void 0 ? [] : await deps.groups.list()).map((group) => ({
86
+ id: group.id,
87
+ name: group.name,
88
+ createdAt: group.createdAt,
89
+ members: countGroupMembers(watchlists, group.id)
90
+ }));
91
+ const selection = (await deps.selection.get()).instrument;
57
92
  return JSON.stringify({
58
93
  ok: true,
59
94
  total,
60
95
  markets,
61
96
  sources,
62
- watchlists
97
+ watchlists,
98
+ groups,
99
+ selection
63
100
  });
64
101
  }
65
102
  });
@@ -178,6 +215,234 @@ function createWatchlistSelectTool(deps) {
178
215
  }
179
216
  });
180
217
  }
218
+ function createWatchlistGroupCreateTool(deps) {
219
+ return defineTool({
220
+ name: "watchlist_group_create",
221
+ description: "Create a named custom watchlist group (a cross-market bucket for organizing the user watchlist, e.g. \"港股观察\" / \"核心仓\"). Group names are unique (trimmed, 24 chars max); creating an existing name is refused. The returned id is what watchlist_group_assign / watchlist_group_rename / watchlist_group_delete take — call watchlist_list first to see existing groups and their member counts. The open GUI sidebar refreshes live over the SSE invalidation channel.",
222
+ parameters: { name: {
223
+ type: "string",
224
+ required: true,
225
+ description: "Group display name (non-empty, 24 chars max, unique among groups)"
226
+ } },
227
+ output: {
228
+ schema: { type: "string" },
229
+ render: (_args, value) => [{
230
+ type: "text",
231
+ text: value
232
+ }]
233
+ },
234
+ async execute(raw) {
235
+ const groups = requireGroups(deps, "watchlist_group_create");
236
+ const name = parseGroupName((raw ?? {}).name, "watchlist_group_create");
237
+ const result = await groups.create(name);
238
+ if (result.error === "duplicate") return JSON.stringify({
239
+ ok: false,
240
+ code: "duplicate",
241
+ note: `A group named ${JSON.stringify(name)} already exists — call watchlist_list to see the existing groups and reuse that id.`
242
+ });
243
+ if (result.group === void 0) return JSON.stringify({
244
+ ok: false,
245
+ code: "unavailable",
246
+ note: "The watchlist group store returned no group."
247
+ });
248
+ deps.onWatchlistsChanged?.();
249
+ return JSON.stringify({
250
+ ok: true,
251
+ id: result.group.id,
252
+ name: result.group.name,
253
+ note: `Created group ${JSON.stringify(result.group.name)} (id ${result.group.id}, 0 members). Assign instruments with watchlist_group_assign.`
254
+ });
255
+ }
256
+ });
257
+ }
258
+ function createWatchlistGroupRenameTool(deps) {
259
+ return defineTool({
260
+ name: "watchlist_group_rename",
261
+ description: "Rename a custom watchlist group by id (membership is untouched — only the display name changes). Refused when the id is unknown or the new name is already used by another group. Call watchlist_list first to get the id and report the old → new name back to the user.",
262
+ parameters: {
263
+ id: {
264
+ type: "string",
265
+ required: true,
266
+ description: "Group id from watchlist_list / watchlist_group_create"
267
+ },
268
+ name: {
269
+ type: "string",
270
+ required: true,
271
+ description: "New group display name (non-empty, 24 chars max, unique among groups)"
272
+ }
273
+ },
274
+ output: {
275
+ schema: { type: "string" },
276
+ render: (_args, value) => [{
277
+ type: "text",
278
+ text: value
279
+ }]
280
+ },
281
+ async execute(raw) {
282
+ const groups = requireGroups(deps, "watchlist_group_rename");
283
+ const args = raw ?? {};
284
+ const id = parseGroupId(args.id, "watchlist_group_rename");
285
+ const name = parseGroupName(args.name, "watchlist_group_rename");
286
+ const before = (await groups.list()).find((group) => group.id === id);
287
+ const result = await groups.rename(id, name);
288
+ if (result.error === "not-found") return JSON.stringify({
289
+ ok: false,
290
+ code: "not-found",
291
+ note: `No watchlist group with id ${JSON.stringify(id)} — call watchlist_list for the current ids.`
292
+ });
293
+ if (result.error === "duplicate") return JSON.stringify({
294
+ ok: false,
295
+ code: "duplicate",
296
+ note: `Another group is already named ${JSON.stringify(name)} — pick a different name.`
297
+ });
298
+ if (result.group === void 0) return JSON.stringify({
299
+ ok: false,
300
+ code: "unavailable",
301
+ note: "The watchlist group store returned no group."
302
+ });
303
+ deps.onWatchlistsChanged?.();
304
+ return JSON.stringify({
305
+ ok: true,
306
+ id: result.group.id,
307
+ from: before?.name ?? null,
308
+ to: result.group.name,
309
+ note: `Renamed group ${JSON.stringify(before?.name ?? id)} → ${JSON.stringify(result.group.name)} (membership unchanged).`
310
+ });
311
+ }
312
+ });
313
+ }
314
+ function createWatchlistGroupDeleteTool(deps) {
315
+ return defineTool({
316
+ name: "watchlist_group_delete",
317
+ description: "Delete a custom watchlist group by id. Deleting a group clears its membership from every watchlist row but NEVER deletes watchlist rows themselves (the instruments stay in the watchlist, only the grouping is dropped). DISCIPLINE: before calling this, tell the user the group name and how many member rows it has (watchlist_list returns members), and call it only after they confirm; then report the same numbers back after the call. Refused when the id is unknown. The open GUI sidebar refreshes live over the SSE invalidation channel.",
318
+ parameters: { id: {
319
+ type: "string",
320
+ required: true,
321
+ description: "Group id from watchlist_list (unknown id is refused)"
322
+ } },
323
+ output: {
324
+ schema: { type: "string" },
325
+ render: (_args, value) => [{
326
+ type: "text",
327
+ text: value
328
+ }]
329
+ },
330
+ async execute(raw) {
331
+ const groups = requireGroups(deps, "watchlist_group_delete");
332
+ const id = parseGroupId((raw ?? {}).id, "watchlist_group_delete");
333
+ const target = (await groups.list()).find((group) => group.id === id);
334
+ if (target === void 0) return JSON.stringify({
335
+ ok: false,
336
+ code: "not-found",
337
+ note: `No watchlist group with id ${JSON.stringify(id)} — call watchlist_list for the current ids.`
338
+ });
339
+ const membersCleared = await deps.watchlists.stripGroup(id);
340
+ const removed = await groups.remove(id);
341
+ if (removed || membersCleared > 0) deps.onWatchlistsChanged?.();
342
+ return JSON.stringify({
343
+ ok: true,
344
+ id,
345
+ name: target.name,
346
+ removed,
347
+ membersCleared,
348
+ note: removed ? `Deleted group ${JSON.stringify(target.name)} (${membersCleared} row(s) lost this membership). The watchlist rows themselves are untouched.` : `Group ${JSON.stringify(target.name)} was already gone; ${membersCleared} stale membership(s) cleaned.`
349
+ });
350
+ }
351
+ });
352
+ }
353
+ function createWatchlistGroupAssignTool(deps) {
354
+ return defineTool({
355
+ name: "watchlist_group_assign",
356
+ description: "Add or remove one watchlist instrument to/from a custom group (set member=true to add, member=false to remove). The instrument must already be in the watchlist or be a market seed row; when the market has no customized rows yet the target row is materialized from that market seed baseline first (result field materialized=true) — this mirrors the GUI drag behavior, and no other rows are lost. Removing membership never deletes the watchlist row. Unknown group ids are refused (no dangling membership). The open GUI sidebar refreshes live over the SSE invalidation channel.",
357
+ parameters: {
358
+ id: {
359
+ type: "string",
360
+ required: true,
361
+ description: "Group id from watchlist_list / watchlist_group_create"
362
+ },
363
+ market: {
364
+ type: "string",
365
+ required: true,
366
+ description: "Market vocabulary slug: crypto | us | cn | hk"
367
+ },
368
+ symbol: {
369
+ type: "string",
370
+ required: true,
371
+ description: "Market-canonical symbol (docs/symbol-vocabulary.md), e.g. BTCUSDT / AAPL / 600519 / 00700"
372
+ },
373
+ member: {
374
+ type: "boolean",
375
+ required: true,
376
+ description: "true = add the instrument to the group; false = remove it from the group"
377
+ },
378
+ name: {
379
+ type: "string",
380
+ description: "Optional display name used only when the row must be materialized, e.g. 贵州茅台"
381
+ }
382
+ },
383
+ output: {
384
+ schema: { type: "string" },
385
+ render: (_args, value) => [{
386
+ type: "text",
387
+ text: value
388
+ }]
389
+ },
390
+ async execute(raw) {
391
+ const groups = requireGroups(deps, "watchlist_group_assign");
392
+ const args = raw ?? {};
393
+ const id = parseGroupId(args.id, "watchlist_group_assign");
394
+ const { market, symbol } = parseInstrumentArgs(args);
395
+ const displayName = typeof args.name === "string" && args.name.trim() ? args.name.trim() : void 0;
396
+ if (typeof args.member !== "boolean") throw new Error("watchlist_group_assign: member is required (true = add to the group, false = remove from it)");
397
+ const member = args.member;
398
+ const target = (await groups.list()).find((group) => group.id === id);
399
+ if (target === void 0) return JSON.stringify({
400
+ ok: false,
401
+ code: "not-found",
402
+ note: `No watchlist group with id ${JSON.stringify(id)} — call watchlist_list for the current ids.`
403
+ });
404
+ let materialized = false;
405
+ if (member) {
406
+ const map = await deps.watchlists.list();
407
+ const rows = map[market];
408
+ if (!Array.isArray(rows)) {
409
+ const baseline = (WATCHLIST_SEEDS[market] ?? []).map((row) => ({
410
+ market: row.market,
411
+ symbol: row.symbol,
412
+ ...row.name !== void 0 ? { name: row.name } : {}
413
+ }));
414
+ if (!baseline.some((row) => row.symbol === symbol)) baseline.push({
415
+ market,
416
+ symbol,
417
+ ...displayName !== void 0 ? { name: displayName } : {}
418
+ });
419
+ await deps.watchlists.save({
420
+ ...map,
421
+ [market]: baseline
422
+ });
423
+ materialized = true;
424
+ } else if (!rows.some((row) => row.symbol === symbol)) materialized = await deps.watchlists.add(market, {
425
+ market,
426
+ symbol,
427
+ ...displayName !== void 0 ? { name: displayName } : {}
428
+ });
429
+ }
430
+ const changed = await deps.watchlists.assignGroup(market, symbol, id, member);
431
+ if (changed || materialized) deps.onWatchlistsChanged?.();
432
+ return JSON.stringify({
433
+ ok: true,
434
+ id,
435
+ group: target.name,
436
+ market,
437
+ symbol,
438
+ assigned: member ? changed : false,
439
+ removed: member ? false : changed,
440
+ materialized,
441
+ note: member ? changed ? `Added ${symbol} (${market}) to group ${JSON.stringify(target.name)}${materialized ? " (row materialized from the market seed baseline)" : ""}.` : `${symbol} (${market}) is already in group ${JSON.stringify(target.name)} (nothing changed).` : changed ? `Removed ${symbol} (${market}) from group ${JSON.stringify(target.name)}; the watchlist row itself is untouched.` : `${symbol} (${market}) was not in group ${JSON.stringify(target.name)} (nothing changed).`
442
+ });
443
+ }
444
+ });
445
+ }
181
446
  function registerWatchlistTools(ctx, deps) {
182
447
  ctx.inject(["tools"], (toolCtx) => {
183
448
  const tools = toolCtx.tools;
@@ -194,14 +459,48 @@ function registerWatchlistTools(ctx, deps) {
194
459
  shared.register(createWatchlistAddTool(hookDeps));
195
460
  shared.register(createWatchlistRemoveTool(hookDeps));
196
461
  shared.register(createWatchlistSelectTool(hookDeps));
462
+ shared.register(createWatchlistGroupCreateTool(hookDeps));
463
+ shared.register(createWatchlistGroupRenameTool(hookDeps));
464
+ shared.register(createWatchlistGroupDeleteTool(hookDeps));
465
+ shared.register(createWatchlistGroupAssignTool(hookDeps));
197
466
  });
198
467
  }
468
+ /** SDK 服务键:自选/选中 store 单实例(桥与工具共享同一缓存;2026-09-08 审查 H1 收口)。 */
469
+ const TRADING_WATCHLIST_KEY = "tradingWatchlist";
470
+ /**
471
+ * 自选 + 选中 store 服务(单实例共享点,strategies/indicators 同款 provide 模式)。
472
+ *
473
+ * 2026-09-08 审查实证:桥(client-ui-trading node 半)此前自建第二个 file store,
474
+ * 与工具侧实例各持整表缓存、各自整表回写 → 两侧写互相覆盖(agent `watchlist_add`
475
+ * 抹掉 GUI 刚写入的行与全部分组归属)。服务缺席(老部署)时桥仍回退自建实例。
476
+ */
477
+ var WatchlistStoreService = class extends Service {
478
+ store;
479
+ selection;
480
+ /** 分组注册表单实例(issue #86 / G5):工具与桥共用,杜绝双 store 互踩。 */
481
+ groups;
482
+ constructor(ctx, deps, serviceName = TRADING_WATCHLIST_KEY) {
483
+ super(ctx, serviceName);
484
+ this.store = deps.watchlists;
485
+ this.selection = deps.selection;
486
+ this.groups = deps.groups;
487
+ }
488
+ };
199
489
  /** Host plugin body:file store provide + 4 工具注册(host 平面,全会话可见)。 */
200
490
  function apply(ctx) {
491
+ const watchlists = createFileWatchlistStore(defaultWatchlistStorePath());
492
+ const selection = createFileSelectionStore(defaultSelectionStorePath());
493
+ const groups = createFileWatchlistGroupsStore(defaultWatchlistGroupsStorePath());
494
+ new WatchlistStoreService(ctx, {
495
+ watchlists,
496
+ selection,
497
+ groups
498
+ });
201
499
  registerWatchlistTools(ctx, {
202
- watchlists: createFileWatchlistStore(defaultWatchlistStorePath()),
203
- selection: createFileSelectionStore(defaultSelectionStorePath())
500
+ watchlists,
501
+ selection,
502
+ groups
204
503
  });
205
504
  }
206
505
  //#endregion
207
- export { apply, createFileSelectionStore, createFileWatchlistStore, createMemorySelectionStore, createMemoryWatchlistStore, createWatchlistAddTool, createWatchlistListTool, createWatchlistRemoveTool, createWatchlistSelectTool, defaultSelectionStorePath, defaultWatchlistStorePath, inject, name, registerWatchlistTools };
506
+ export { TRADING_WATCHLIST_KEY, WatchlistStoreService, apply, createFileSelectionStore, createFileWatchlistGroupsStore, createFileWatchlistStore, createMemorySelectionStore, createMemoryWatchlistGroupsStore, createMemoryWatchlistStore, createWatchlistAddTool, createWatchlistGroupAssignTool, createWatchlistGroupCreateTool, createWatchlistGroupDeleteTool, createWatchlistGroupRenameTool, createWatchlistListTool, createWatchlistRemoveTool, createWatchlistSelectTool, defaultSelectionStorePath, defaultWatchlistGroupsStorePath, defaultWatchlistStorePath, inject, name, registerWatchlistTools };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dshtrading/watchlist",
3
3
  "description": "Host-side watchlist + selection stores for dsh-trading: multi-list file store (~/.dsh/watchlists.json), selection store (~/.dsh/selection.json), and cross-market watchlist_* tools (watchlist_list/add/remove/select) on the host plane",
4
- "version": "0.1.5",
4
+ "version": "0.2.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "./lib/index.d.ts",
@@ -20,7 +20,9 @@
20
20
  "lib"
21
21
  ],
22
22
  "license": "PolyForm-Noncommercial-1.0.0",
23
- "dependencies": {},
23
+ "dependencies": {
24
+ "@dshtrading/dsh-home": "^0.2.0"
25
+ },
24
26
  "peerDependencies": {
25
27
  "@deepseek-ai/cordis": ">=4.0.0",
26
28
  "@deepseek-ai/dsh-tools": ">=0.1.2-rc.1"