@hedgehog-finance/hedgehog-plugin 1.0.11 → 1.0.13

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.
Files changed (45) hide show
  1. package/dist/index.d.ts +10 -0
  2. package/dist/index.js +24 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/src/channel.d.ts +6 -0
  5. package/dist/src/channel.js +620 -0
  6. package/dist/src/channel.js.map +1 -0
  7. package/dist/src/core/database.d.ts +2 -0
  8. package/dist/src/core/database.js +220 -0
  9. package/dist/src/core/database.js.map +1 -0
  10. package/dist/src/core/logger.d.ts +3 -0
  11. package/dist/src/core/logger.js +20 -0
  12. package/dist/src/core/logger.js.map +1 -0
  13. package/dist/src/features/index.d.ts +22 -0
  14. package/dist/src/features/index.js +8 -0
  15. package/dist/src/features/index.js.map +1 -0
  16. package/dist/src/features/watchlist/logic.d.ts +48 -0
  17. package/dist/src/features/watchlist/logic.js +607 -0
  18. package/dist/src/features/watchlist/logic.js.map +1 -0
  19. package/dist/src/features/watchlist/schema.d.ts +85 -0
  20. package/dist/src/features/watchlist/schema.js +29 -0
  21. package/dist/src/features/watchlist/schema.js.map +1 -0
  22. package/dist/src/features/watchlist/store.d.ts +1 -0
  23. package/dist/src/features/watchlist/store.js +2 -0
  24. package/dist/src/features/watchlist/store.js.map +1 -0
  25. package/dist/src/features/watchlist/tools.d.ts +135 -0
  26. package/dist/src/features/watchlist/tools.js +572 -0
  27. package/dist/src/features/watchlist/tools.js.map +1 -0
  28. package/dist/src/runtime.d.ts +5 -0
  29. package/dist/src/runtime.js +40 -0
  30. package/dist/src/runtime.js.map +1 -0
  31. package/dist/src/types.d.ts +99 -0
  32. package/dist/src/types.js +16 -0
  33. package/dist/src/types.js.map +1 -0
  34. package/index.ts +5 -5
  35. package/openclaw.plugin.json +2 -2
  36. package/package.json +24 -7
  37. package/src/channel.ts +35 -13
  38. package/src/core/database.ts +90 -3
  39. package/src/features/index.ts +2 -1
  40. package/src/features/watchlist/logic.ts +503 -128
  41. package/src/features/watchlist/schema.ts +1 -6
  42. package/src/features/watchlist/tools.ts +248 -103
  43. package/src/runtime.ts +3 -3
  44. package/src/types.ts +1 -1
  45. package/tsconfig.json +0 -16
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/runtime.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C,IAAI,OAAO,GAAyB,IAAI,CAAC;AACzC,IAAI,MAAM,GAAW,EAAE,CAAC;AACxB,IAAI,SAAS,GAAW,EAAE,CAAC;AAE3B,MAAM,UAAU,kBAAkB,CAAC,IAAmB;IACrD,OAAO,GAAG,IAAI,CAAC;IAEf,IAAI,CAAC;QACJ,sCAAsC;QACtC,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;QACrC,MAAM,SAAS,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,IAAI,EAAE,CAAyC,CAAC;QACnF,MAAM,aAAa,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,kBAAkB,CAAC,CAAC;QACzE,MAAM,YAAY,GAAG,aAAa,EAAE,SAAS;YAC5C,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,SAAS;YAC/B,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,oBAAoB,CAAC,CAAC;QAE5D,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;QACxD,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC;QAC/C,MAAM,CAAC,IAAI,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,oBAAoB,CAAC,CAAC;IAC7D,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACZ,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,6BAA6B,CAAC,CAAC;IACzD,CAAC;AACF,CAAC;AAED,MAAM,UAAU,SAAS;IACxB,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IACtE,OAAO,MAAM,CAAC;AACf,CAAC;AAED,MAAM,UAAU,YAAY;IAC3B,IAAI,CAAC,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IAC5E,OAAO,SAAS,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,kBAAkB;IACjC,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IACxE,OAAO,OAAO,CAAC;AAChB,CAAC"}
@@ -0,0 +1,99 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Hedgehog Finance Resolved Account
4
+ */
5
+ export interface HedgehogFinanceResolvedAccount {
6
+ accountId: string;
7
+ config: {
8
+ token: string;
9
+ code: string;
10
+ };
11
+ enabled: boolean;
12
+ configured: boolean;
13
+ }
14
+ /**
15
+ * Inbound Message from Relay (Manual Handling)
16
+ */
17
+ export interface RelayInboundMessage {
18
+ type: "req" | "reply" | "item_event" | "usage" | "model" | "reasoning";
19
+ from: string;
20
+ chatId: string;
21
+ id: string;
22
+ text?: string;
23
+ method?: string;
24
+ params?: any;
25
+ replyTo?: string;
26
+ }
27
+ /**
28
+ * Session entry structure in sessions.json
29
+ */
30
+ export interface OpenClawSessionEntry {
31
+ sessionId: string;
32
+ inputTokens?: number;
33
+ outputTokens?: number;
34
+ totalTokens?: number;
35
+ cacheRead?: number;
36
+ estimatedCostUsd?: number;
37
+ model?: string;
38
+ modelProvider?: string;
39
+ updatedAt?: number;
40
+ }
41
+ /**
42
+ * Normalized Usage for UI and Internal logic
43
+ */
44
+ export interface TurnUsage {
45
+ input: number;
46
+ output: number;
47
+ total: number;
48
+ cacheRead: number;
49
+ cost: number;
50
+ model: string;
51
+ provider: string;
52
+ }
53
+ /**
54
+ * Stock Classification Result (AI Schema)
55
+ */
56
+ export declare const StockClassificationSchema: z.ZodObject<{
57
+ industry: z.ZodObject<{
58
+ name: z.ZodString;
59
+ weight: z.ZodDefault<z.ZodNumber>;
60
+ }, "strip", z.ZodTypeAny, {
61
+ name: string;
62
+ weight: number;
63
+ }, {
64
+ name: string;
65
+ weight?: number | undefined;
66
+ }>;
67
+ theme: z.ZodArray<z.ZodObject<{
68
+ name: z.ZodString;
69
+ weight: z.ZodDefault<z.ZodNumber>;
70
+ }, "strip", z.ZodTypeAny, {
71
+ name: string;
72
+ weight: number;
73
+ }, {
74
+ name: string;
75
+ weight?: number | undefined;
76
+ }>, "many">;
77
+ weight: z.ZodDefault<z.ZodNumber>;
78
+ }, "strip", z.ZodTypeAny, {
79
+ weight: number;
80
+ industry: {
81
+ name: string;
82
+ weight: number;
83
+ };
84
+ theme: {
85
+ name: string;
86
+ weight: number;
87
+ }[];
88
+ }, {
89
+ industry: {
90
+ name: string;
91
+ weight?: number | undefined;
92
+ };
93
+ theme: {
94
+ name: string;
95
+ weight?: number | undefined;
96
+ }[];
97
+ weight?: number | undefined;
98
+ }>;
99
+ export type StockClassification = z.infer<typeof StockClassificationSchema>;
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Stock Classification Result (AI Schema)
4
+ */
5
+ export const StockClassificationSchema = z.object({
6
+ industry: z.object({
7
+ name: z.string(),
8
+ weight: z.number().min(0).max(100).default(50)
9
+ }).describe("Required main industry category with weight"),
10
+ theme: z.array(z.object({
11
+ name: z.string(),
12
+ weight: z.number().min(0).max(100).default(50)
13
+ })).describe("Thematic categories with weights"),
14
+ weight: z.number().min(0).max(100).default(50).describe("Overall priority weight")
15
+ });
16
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAyDxB;;GAEG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;QAChB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;KACjD,CAAC,CAAC,QAAQ,CAAC,6CAA6C,CAAC;IAC1D,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;QACpB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;QAChB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;KACjD,CAAC,CAAC,CAAC,QAAQ,CAAC,kCAAkC,CAAC;IAChD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,yBAAyB,CAAC;CACrF,CAAC,CAAC"}
package/index.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
2
- import { hedgehogFinancePlugin } from "./src/channel";
3
- import { setHedgehogRuntime } from "./src/runtime";
4
- import { allFeaturesTools } from "./src/features";
2
+ import { hedgehogFinancePlugin } from "./src/channel.js";
3
+ import { setHedgehogRuntime } from "./src/runtime.js";
4
+ import { allFeaturesTools } from "./src/features/index.js";
5
5
 
6
6
  export default defineChannelPluginEntry({
7
- id: "hedgehog-finance",
7
+ id: "hedgehog_finance",
8
8
  name: "Hedgehog Finance Comprehensive Plugin",
9
9
  description: "WebSocket Channel & Watchlist SQLite Tools for Hedgehog App",
10
10
  plugin: hedgehogFinancePlugin,
@@ -14,6 +14,7 @@ export default defineChannelPluginEntry({
14
14
  registerFull(api) {
15
15
  // 1. 自动化循环注册 Tool
16
16
  Object.entries(allFeaturesTools).forEach(([name, tool]) => {
17
+ if (tool.registerTool === false) return;
17
18
  const registerable = { ...tool, label: tool.description };
18
19
  api.registerTool(registerable as any, { name });
19
20
  });
@@ -21,4 +22,3 @@ export default defineChannelPluginEntry({
21
22
  api.logger.info("[hedgehog-app] Registered tools and runtime context.");
22
23
  },
23
24
  });
24
-
@@ -1,8 +1,8 @@
1
1
  {
2
- "id": "hedgehog-finance",
2
+ "id": "hedgehog_finance",
3
3
  "kind": "channel",
4
4
  "channels": [
5
- "hedgehog-finance"
5
+ "hedgehog_finance"
6
6
  ],
7
7
  "name": "Hedgehog Finance Comprehensive Plugin",
8
8
  "description": "WebSocket Channel & Watchlist SQLite Tools for Hedgehog App",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hedgehog-finance/hedgehog-plugin",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Hedgehog App WebSocket channel and Watchlist Tools for OpenClaw",
5
5
  "keywords": [
6
6
  "bot",
@@ -11,11 +11,25 @@
11
11
  "hedgehog-app"
12
12
  ],
13
13
  "repository": "github:hedgehog-finance/hedgehog-plugin",
14
- "main": "index.ts",
14
+ "main": "dist/index.js",
15
15
  "type": "module",
16
16
  "author": "Hedgehog Finance",
17
17
  "license": "MIT",
18
+ "files": [
19
+ "dist/**/*.js",
20
+ "dist/**/*.js.map",
21
+ "dist/**/*.d.ts",
22
+ "index.ts",
23
+ "src/**/*.ts",
24
+ "openclaw.plugin.json",
25
+ "LICENSE"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.json",
29
+ "prepack": "npm run build"
30
+ },
18
31
  "dependencies": {
32
+ "@mariozechner/pi-ai": "^0.66.1",
19
33
  "pino": "^9.0.0",
20
34
  "pino-pretty": "^11.1.0",
21
35
  "ws": "^8.20.0",
@@ -31,17 +45,20 @@
31
45
  "extensions": [
32
46
  "./index.ts"
33
47
  ],
48
+ "runtimeExtensions": [
49
+ "./dist/index.js"
50
+ ],
34
51
  "install": {
35
52
  "npmSpec": "@hedgehog-finance/hedgehog-plugin",
36
53
  "defaultChoice": "npm"
37
54
  },
38
55
  "compat": {
39
- "pluginApi": ">=2026.4.15",
40
- "minGatewayVersion": "2026.4.15"
56
+ "pluginApi": ">=2026.5.4",
57
+ "minGatewayVersion": "2026.5.4"
41
58
  },
42
59
  "build": {
43
- "openclawVersion": "2026.4.15",
44
- "pluginSdkVersion": "2026.4.15"
60
+ "openclawVersion": "2026.5.4",
61
+ "pluginSdkVersion": "2026.5.4"
45
62
  }
46
63
  }
47
- }
64
+ }
package/src/channel.ts CHANGED
@@ -11,13 +11,13 @@ import type {
11
11
  ChannelAccountSnapshot,
12
12
  ChannelStatusIssue
13
13
  } from "openclaw/plugin-sdk/channel-contract";
14
- import { getHedgehogRuntime } from "./runtime";
15
- import { logger } from "./core/logger";
14
+ import { getHedgehogRuntime } from "./runtime.js";
15
+ import { logger } from "./core/logger.js";
16
16
  import type {
17
17
  HedgehogFinanceResolvedAccount,
18
18
  RelayInboundMessage
19
- } from "./types";
20
- import { allFeaturesTools } from "./features";
19
+ } from "./types.js";
20
+ import { allFeaturesTools } from "./features/index.js";
21
21
 
22
22
 
23
23
 
@@ -192,10 +192,10 @@ async function getCurrentTurnUsageAsync(
192
192
  * Hedgehog Finance Channel Plugin
193
193
  */
194
194
  export const hedgehogFinancePlugin: ChannelPlugin<HedgehogFinanceResolvedAccount> = {
195
- id: "hedgehog-finance",
195
+ id: "hedgehog_finance",
196
196
 
197
197
  meta: {
198
- id: "hedgehog-finance",
198
+ id: "hedgehog_finance",
199
199
  label: "Hedgehog Finance",
200
200
  selectionLabel: "Hedgehog Finance",
201
201
  blurb: "Custom WebSocket relay channel for Hedgehog App",
@@ -215,7 +215,7 @@ export const hedgehogFinancePlugin: ChannelPlugin<HedgehogFinanceResolvedAccount
215
215
 
216
216
  config: {
217
217
  listAccountIds: (cfg: OpenClawConfig): string[] => {
218
- const channelConfig = (cfg.channels?.['hedgehog-finance'] || {}) as any;
218
+ const channelConfig = (cfg.channels?.['hedgehog_finance'] || {}) as any;
219
219
 
220
220
  if (channelConfig.accounts) {
221
221
  if (Array.isArray(channelConfig.accounts)) {
@@ -232,7 +232,7 @@ export const hedgehogFinancePlugin: ChannelPlugin<HedgehogFinanceResolvedAccount
232
232
  },
233
233
 
234
234
  resolveAccount: (cfg: OpenClawConfig, accountId?: string | null): HedgehogFinanceResolvedAccount => {
235
- const channelConfig = (cfg.channels?.['hedgehog-finance'] || {}) as any;
235
+ const channelConfig = (cfg.channels?.['hedgehog_finance'] || {}) as any;
236
236
  const id = accountId || channelConfig.accountId || "default";
237
237
 
238
238
  let accountInfo: any;
@@ -267,7 +267,7 @@ export const hedgehogFinancePlugin: ChannelPlugin<HedgehogFinanceResolvedAccount
267
267
  },
268
268
 
269
269
  defaultAccountId: (cfg: OpenClawConfig): string => {
270
- const channelConfig = (cfg as any)?.channels?.['hedgehog-finance'];
270
+ const channelConfig = (cfg as any)?.channels?.['hedgehog_finance'];
271
271
  return channelConfig?.accountId || "default";
272
272
  },
273
273
  },
@@ -346,6 +346,18 @@ export const hedgehogFinancePlugin: ChannelPlugin<HedgehogFinanceResolvedAccount
346
346
  if (!method) return;
347
347
 
348
348
  // 从中央工具注册表中查找方法
349
+ if (method === "ping") {
350
+ if (ws?.readyState === WebSocket.OPEN) {
351
+ ws.send(JSON.stringify({
352
+ type: "res",
353
+ id: id,
354
+ ok: true,
355
+ payload: { success: true }
356
+ }));
357
+ }
358
+ return;
359
+ }
360
+
349
361
  const tool = allFeaturesTools[method];
350
362
 
351
363
  if (tool && typeof tool.execute === 'function') {
@@ -386,6 +398,16 @@ export const hedgehogFinancePlugin: ChannelPlugin<HedgehogFinanceResolvedAccount
386
398
  }
387
399
  return;
388
400
  }
401
+
402
+ if (ws?.readyState === WebSocket.OPEN) {
403
+ ws.send(JSON.stringify({
404
+ type: "res",
405
+ id: id,
406
+ ok: false,
407
+ error: { message: `Unknown RPC method: ${method}` }
408
+ }));
409
+ }
410
+ return;
389
411
  }
390
412
 
391
413
  const { from, text, chatId, id } = appPayload;
@@ -394,7 +416,7 @@ export const hedgehogFinancePlugin: ChannelPlugin<HedgehogFinanceResolvedAccount
394
416
 
395
417
  const route = rt.channel.routing.resolveAgentRoute({
396
418
  cfg,
397
- channel: "hedgehog-finance",
419
+ channel: "hedgehog_finance",
398
420
  accountId: String(accountId),
399
421
  peer: { kind: "direct", id: chatId },
400
422
  });
@@ -417,7 +439,7 @@ export const hedgehogFinancePlugin: ChannelPlugin<HedgehogFinanceResolvedAccount
417
439
  AccountId: route.accountId,
418
440
  AgentId: agentId,
419
441
  AgentWorkspace: (route as any).agentWorkspace,
420
- Provider: "hedgehog-finance",
442
+ Provider: "hedgehog_finance",
421
443
  MessageSid: id,
422
444
  });
423
445
 
@@ -427,7 +449,7 @@ export const hedgehogFinancePlugin: ChannelPlugin<HedgehogFinanceResolvedAccount
427
449
  ctx: context,
428
450
  updateLastRoute: {
429
451
  sessionKey: route.mainSessionKey,
430
- channel: "hedgehog-finance",
452
+ channel: "hedgehog_finance",
431
453
  to: chatId,
432
454
  accountId: String(accountId),
433
455
  },
@@ -657,7 +679,7 @@ export const hedgehogFinancePlugin: ChannelPlugin<HedgehogFinanceResolvedAccount
657
679
  if (!account.configured) {
658
680
  return [
659
681
  {
660
- channel: "hedgehog-finance",
682
+ channel: "hedgehog_finance",
661
683
  accountId: account.accountId,
662
684
  kind: "config" as const,
663
685
  message: "Account not configured (missing relay token)",
@@ -3,8 +3,8 @@
3
3
  import { DatabaseSync } from 'node:sqlite';
4
4
  import { mkdirSync, existsSync, copyFileSync, readdirSync, statSync, unlinkSync } from "node:fs";
5
5
  import path from "node:path";
6
- import { getDbPath, getBackupDir } from "../runtime";
7
- import { logger } from "./logger";
6
+ import { getDbPath, getBackupDir } from "../runtime.js";
7
+ import { logger } from "./logger.js";
8
8
 
9
9
  let _db: any = null;
10
10
  let _backupJobStarted = false;
@@ -59,6 +59,92 @@ function startDailyBackupJob() {
59
59
  scheduleNextBackup();
60
60
  }
61
61
 
62
+ function normalizeMetadataStockCode(stockCode: string, exchange: string): string {
63
+ const code = String(stockCode || "").trim().toUpperCase();
64
+ if (/\.(SH|SS|SZ|HK|US)$/i.test(code)) {
65
+ return code.replace(/\.SS$/i, ".SH");
66
+ }
67
+ switch (exchange) {
68
+ case "SSE":
69
+ return `${code}.SH`;
70
+ case "SZSE":
71
+ return `${code}.SZ`;
72
+ case "HKEX":
73
+ return `${code}.HK`;
74
+ default:
75
+ return code;
76
+ }
77
+ }
78
+
79
+ function runWatchlistDedupMigrations(db: DatabaseSync) {
80
+ db.exec("BEGIN TRANSACTION");
81
+ try {
82
+ const metadataRows = db.prepare(`
83
+ SELECT stockCode, exchange FROM global_stock_metadata
84
+ `).all() as { stockCode: string; exchange: string }[];
85
+ const metadataDeleteStmt = db.prepare(`
86
+ DELETE FROM global_stock_metadata WHERE stockCode = ? AND exchange = ?
87
+ `);
88
+ const metadataUpdateStmt = db.prepare(`
89
+ UPDATE global_stock_metadata SET stockCode = ? WHERE stockCode = ? AND exchange = ?
90
+ `);
91
+ const metadataExistsStmt = db.prepare(`
92
+ SELECT 1 FROM global_stock_metadata WHERE stockCode = ? AND exchange = ?
93
+ `);
94
+ for (const row of metadataRows) {
95
+ const normalizedCode = normalizeMetadataStockCode(row.stockCode, row.exchange);
96
+ if (!normalizedCode || normalizedCode === row.stockCode) continue;
97
+ const existing = metadataExistsStmt.get(normalizedCode, row.exchange);
98
+ if (existing) {
99
+ metadataDeleteStmt.run(row.stockCode, row.exchange);
100
+ } else {
101
+ metadataUpdateStmt.run(normalizedCode, row.stockCode, row.exchange);
102
+ }
103
+ }
104
+
105
+ const duplicateCategories = db.prepare(`
106
+ SELECT userId, name, type, MIN(id) AS keepId, GROUP_CONCAT(id) AS ids
107
+ FROM watchlist_categories
108
+ GROUP BY userId, name, type
109
+ HAVING COUNT(*) > 1
110
+ `).all() as { userId: string; name: string; type: 'industry' | 'theme'; keepId: string; ids: string }[];
111
+ for (const dup of duplicateCategories) {
112
+ const table = dup.type === 'industry' ? 'watchlist_industry_items' : 'watchlist_theme_items';
113
+ const ids = dup.ids.split(",").filter(id => id && id !== dup.keepId);
114
+ for (const oldId of ids) {
115
+ db.prepare(`UPDATE ${table} SET categoryId = ? WHERE userId = ? AND categoryId = ?`).run(dup.keepId, dup.userId, oldId);
116
+ db.prepare("DELETE FROM watchlist_categories WHERE id = ?").run(oldId);
117
+ }
118
+ }
119
+
120
+ db.exec(`
121
+ DELETE FROM watchlist_industry_items
122
+ WHERE rowid NOT IN (
123
+ SELECT MIN(rowid) FROM watchlist_industry_items GROUP BY watchlistId, categoryId
124
+ );
125
+
126
+ DELETE FROM watchlist_theme_items
127
+ WHERE rowid NOT IN (
128
+ SELECT MIN(rowid) FROM watchlist_theme_items GROUP BY watchlistId, categoryId
129
+ );
130
+ `);
131
+
132
+ db.exec("COMMIT");
133
+ } catch (e) {
134
+ if (db.inTransaction) db.exec("ROLLBACK");
135
+ throw e;
136
+ }
137
+
138
+ db.exec(`
139
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_watchlist_categories_user_name_type
140
+ ON watchlist_categories(userId, name, type);
141
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_watchlist_industry_items_watchlist_category
142
+ ON watchlist_industry_items(watchlistId, categoryId);
143
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_watchlist_theme_items_watchlist_category
144
+ ON watchlist_theme_items(watchlistId, categoryId);
145
+ `);
146
+ }
147
+
62
148
  export function getDB(): DatabaseSync {
63
149
  if (!_db) {
64
150
  const dbPath = getDbPath();
@@ -141,6 +227,7 @@ export function getDB(): DatabaseSync {
141
227
  CREATE INDEX IF NOT EXISTS idx_watchlist_theme_user ON watchlist_theme_items(userId);
142
228
  `);
143
229
 
230
+ runWatchlistDedupMigrations(_db);
144
231
  }
145
232
  return _db;
146
- }
233
+ }
@@ -1,4 +1,4 @@
1
- import { watchlistTools } from "./watchlist/tools";
1
+ import { watchlistTools } from "./watchlist/tools.js";
2
2
 
3
3
  /**
4
4
  * Runtime tool shape used for dynamic RPC dispatch and registerTool.
@@ -13,6 +13,7 @@ export interface RuntimeTool {
13
13
  label?: string;
14
14
  description: string;
15
15
  parameters: unknown;
16
+ registerTool?: boolean;
16
17
  // bivariant method signature — allows specific param types
17
18
  execute(params: unknown, ctx: { userId: string }): Promise<string>;
18
19
  }