@perrylink/dsh-ticktick 0.1.2

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 (71) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +201 -0
  3. package/README.es.md +41 -0
  4. package/README.hi.md +33 -0
  5. package/README.md +111 -0
  6. package/README.pt.md +41 -0
  7. package/README.zh.md +111 -0
  8. package/cordis.patch.yml +35 -0
  9. package/lib/client.js +6233 -0
  10. package/lib/client.js.map +1 -0
  11. package/lib/index.js +1349 -0
  12. package/lib/typert.host.js +26 -0
  13. package/lib/types/client/TicktickAction.d.ts +29 -0
  14. package/lib/types/client/TicktickAction.d.ts.map +1 -0
  15. package/lib/types/client/TicktickSettingsCard.d.ts +27 -0
  16. package/lib/types/client/TicktickSettingsCard.d.ts.map +1 -0
  17. package/lib/types/client/api.d.ts +34 -0
  18. package/lib/types/client/api.d.ts.map +1 -0
  19. package/lib/types/client/dates.d.ts +26 -0
  20. package/lib/types/client/dates.d.ts.map +1 -0
  21. package/lib/types/client/index.d.ts +34 -0
  22. package/lib/types/client/index.d.ts.map +1 -0
  23. package/lib/types/client/locales.d.ts +61 -0
  24. package/lib/types/client/locales.d.ts.map +1 -0
  25. package/lib/types/client/order.d.ts +25 -0
  26. package/lib/types/client/order.d.ts.map +1 -0
  27. package/lib/types/client/remote.d.ts +260 -0
  28. package/lib/types/client/remote.d.ts.map +1 -0
  29. package/lib/types/client/styles.d.ts +13 -0
  30. package/lib/types/client/styles.d.ts.map +1 -0
  31. package/lib/types/config.d.ts +64 -0
  32. package/lib/types/config.d.ts.map +1 -0
  33. package/lib/types/domain.d.ts +70 -0
  34. package/lib/types/domain.d.ts.map +1 -0
  35. package/lib/types/index.d.ts +56 -0
  36. package/lib/types/index.d.ts.map +1 -0
  37. package/lib/types/mcp.d.ts +94 -0
  38. package/lib/types/mcp.d.ts.map +1 -0
  39. package/lib/types/service.d.ts +160 -0
  40. package/lib/types/service.d.ts.map +1 -0
  41. package/lib/types/tools.d.ts +18 -0
  42. package/lib/types/tools.d.ts.map +1 -0
  43. package/lib/types/typert.host.d.ts +216 -0
  44. package/lib/types/typert.host.d.ts.map +1 -0
  45. package/lib/types/wire.d.ts +561 -0
  46. package/lib/types/wire.d.ts.map +1 -0
  47. package/lib/wire-C-vDxxnC.js +5288 -0
  48. package/package.json +187 -0
  49. package/probes/lib.mjs +65 -0
  50. package/probes/probe-bootstrap.mjs +10 -0
  51. package/probes/probe-crud.mjs +15 -0
  52. package/probes/probe-due.mjs +28 -0
  53. package/probes/probe-queries.mjs +18 -0
  54. package/probes/probe-reorder.mjs +24 -0
  55. package/src/client/TicktickAction.tsx +356 -0
  56. package/src/client/TicktickSettingsCard.tsx +182 -0
  57. package/src/client/api.ts +56 -0
  58. package/src/client/dates.ts +61 -0
  59. package/src/client/index.ts +119 -0
  60. package/src/client/locales.ts +113 -0
  61. package/src/client/order.ts +37 -0
  62. package/src/client/remote.ts +76 -0
  63. package/src/client/styles.ts +48 -0
  64. package/src/config.ts +136 -0
  65. package/src/domain.ts +224 -0
  66. package/src/index.ts +139 -0
  67. package/src/mcp.ts +213 -0
  68. package/src/service.ts +403 -0
  69. package/src/tools.ts +336 -0
  70. package/src/typert.host.ts +25 -0
  71. package/src/wire.ts +401 -0
@@ -0,0 +1,119 @@
1
+ /**
2
+ * `dsh-ticktick`, browser half: mounts the `ticktick` Remote contribution,
3
+ * then registers the Session-header task panel action
4
+ * (`conversation.session.header.actions`, id `ticktick`) and the plugin
5
+ * settings card (`settings.plugin.item`, key `ticktick`). All data arrives
6
+ * through the `remote.ticktick` namespace and the bound `settingsScope`;
7
+ * the panel holds no state beyond its popup, forms, and drag session.
8
+ *
9
+ * @module dsh-ticktick/client
10
+ */
11
+
12
+ import type { Context as ClientContext } from '@deepseek-ai/cordis'
13
+ // Type-only: the api-remotes client declares the 'remote' service on the
14
+ // client Context (the shell graph owns the runtime value).
15
+ import type {} from '@deepseek-ai/dsh-api-remotes/client'
16
+ // Type-only: pulls the 'settings.plugin.item' SlotMap declaration into this
17
+ // program so the card registration typechecks against the real declaration.
18
+ import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
19
+ // Type-only: the header-actions SlotMap merge (conversation slot family).
20
+ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
21
+ // Type-only: the settingsScope Context merge.
22
+ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
23
+ import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client'
24
+ // Type-only: the locale service Context merge (ctx.locale).
25
+ import type {} from '@deepseek-ai/dsh-client-locale/client'
26
+ import { createTicktickApi, type TicktickApi } from './api.ts'
27
+ import { TicktickAction } from './TicktickAction.tsx'
28
+ import { TicktickSettingsCard, type TicktickSettingsCardInjected } from './TicktickSettingsCard.tsx'
29
+ import { en, zh, type TicktickLocaleKey } from './locales.ts'
30
+ import { installPanelStyles } from './styles.ts'
31
+ import { TICKTICK_REMOTE } from './remote.ts'
32
+ import { TICKTICK_SETTINGS_NS, type TicktickSettings } from '../wire.ts'
33
+
34
+ export type { TicktickApi } from './api.ts'
35
+ export type { TicktickLocaleKey } from './locales.ts'
36
+
37
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
38
+ interface LocaleNamespaceMap {
39
+ /** TickTick panel and settings-card copy. */
40
+ ticktick: TicktickLocaleKey
41
+ }
42
+ }
43
+
44
+ /** Dictionary namespace owned by this plugin. */
45
+ export const NS = 'ticktick'
46
+
47
+ /** Plugin name: matches the package name, the graph row `name`, and the bundle id. */
48
+ export const name = '@perrylink/dsh-ticktick'
49
+
50
+ /** Services the client reads; `remote.ticktick` appears once this plugin mounts its contribution. */
51
+ export const inject = ['slots', 'locale', 'remote', 'settingsScope']
52
+
53
+ /**
54
+ * Minimal structural contract of the client slots registry this client
55
+ * registers into, for the two slots it occupies. Declared locally because
56
+ * the service's owner package moved across harness lines; the runtime
57
+ * contract is structural.
58
+ */
59
+ interface TicktickSlots {
60
+ inject(slot: string, callback: () => unknown): void
61
+ register(options: {
62
+ name: 'conversation.session.header.actions'
63
+ id: 'ticktick'
64
+ order: number
65
+ locale: string
66
+ inject: () => { api: TicktickApi }
67
+ }, component: unknown): () => void
68
+ register(options: {
69
+ name: 'settings.plugin.item'
70
+ key: 'ticktick'
71
+ locale: string
72
+ inject: () => TicktickSettingsCardInjected
73
+ }, component: unknown): () => void
74
+ }
75
+
76
+ /**
77
+ * Browser plugin body: dictionaries, the scoped stylesheet, the Remote
78
+ * contribution mount, the header action, and the settings card.
79
+ *
80
+ * @param ctx - client root context.
81
+ */
82
+ export async function apply(ctx: ClientContext): Promise<void> {
83
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-ticktick: dictionaries')
84
+ ctx.effect(() => installPanelStyles(), 'dsh-ticktick: stylesheet')
85
+
86
+ // $mount registers the 'remote.ticktick' namespace service and owns its
87
+ // removal for this fiber's lifetime.
88
+ await ctx.remote.$mount(TICKTICK_REMOTE)
89
+
90
+ ctx.inject(['remote.ticktick'], (scope) => {
91
+ const slots = scope.get('slots') as unknown as TicktickSlots
92
+ const settingsScope = scope.settingsScope.bind({ namespace: TICKTICK_SETTINGS_NS }) as unknown as SettingsScope<TicktickSettings>
93
+
94
+ slots.inject('conversation.session.header.actions', () => slots.register({
95
+ name: 'conversation.session.header.actions',
96
+ id: 'ticktick',
97
+ order: 11,
98
+ locale: NS,
99
+ inject: (): { api: TicktickApi, setToken: (token: string) => Promise<void> } => ({
100
+ api: createTicktickApi(scope),
101
+ setToken: async (token) => { await settingsScope.set('token', token.trim()) },
102
+ }),
103
+ }, TicktickAction))
104
+
105
+ slots.inject('settings.plugin.item', () => slots.register({
106
+ name: 'settings.plugin.item',
107
+ key: 'ticktick',
108
+ locale: NS,
109
+ inject: (): TicktickSettingsCardInjected => ({
110
+ scope: settingsScope,
111
+ probe: async () => {
112
+ const result = await scope.remote.ticktick.probe()
113
+ if (!result.ok) throw new Error(`ticktick.probe failed: ${result.error.code}: ${result.error.message}`)
114
+ return result.value
115
+ },
116
+ }),
117
+ }, TicktickSettingsCard))
118
+ })
119
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Locale dictionaries for the TickTick panel and settings card. The harness
3
+ * locale registry accepts the 'en' | 'zh' UI language codes, so this client
4
+ * ships those two dictionaries and follows the app's UI language.
5
+ *
6
+ * @module dsh-ticktick/client/locales
7
+ */
8
+
9
+ export type TicktickLocaleKey = keyof typeof en
10
+
11
+ export const en = {
12
+ title: 'TickTick',
13
+ open: 'Open TickTick tasks',
14
+ allLists: 'All lists',
15
+ viewUndone: 'Undone',
16
+ viewCompleted: 'Completed',
17
+ searchPlaceholder: 'Search tasks…',
18
+ searchEmpty: 'No matches',
19
+ refresh: 'Refresh',
20
+ addPlaceholder: 'Add a task…',
21
+ add: 'Add',
22
+ empty: 'No tasks',
23
+ confirmDelete: 'Delete this task?',
24
+ delete: 'Delete',
25
+ complete: 'Complete',
26
+ setDue: 'Set due date',
27
+ clearDue: 'Clear due date',
28
+ overdue: 'overdue',
29
+ today: 'today',
30
+ tomorrow: 'tomorrow',
31
+ warning: 'Warning',
32
+ unconfigured: 'No TickTick token: set it in Settings → Plugins → TickTick.',
33
+ loadError: 'Failed to load: ',
34
+ settingsName: 'TickTick',
35
+ settingsToken: 'API token (dp_…)',
36
+ settingsTokenHint: 'A secret saved locally; wins over the token file.',
37
+ settingsTokenFile: 'Token file path',
38
+ settingsTokenFileHint: 'Empty = $DSH_HOME/.ticktick-token, re-read per request.',
39
+ settingsMcpUrl: 'MCP endpoint',
40
+ settingsMcpUrlHint: 'Restart dsh after changing the endpoint.',
41
+ settingsProtected: 'Protected task ids',
42
+ settingsProtectedHint: 'Comma-separated; mutating operations refuse these (restart applies).',
43
+ settingsSave: 'Save',
44
+ settingsSaved: 'Saved',
45
+ settingsTest: 'Test connection',
46
+ settingsTestOk: 'Connected: N tools',
47
+ settingsTestFail: 'Connection failed: ',
48
+ settingsClear: 'Clear credentials',
49
+ settingsCleared: 'Credentials cleared',
50
+ panelTokenPlaceholder: 'Paste API token (dp_…)',
51
+ panelTokenSave: 'Save token',
52
+ panelTokenHint: 'No token yet — paste it here, or open Settings → Plugins → TickTick.',
53
+ statusConnected: 'connected',
54
+ statusNotConnected: 'configured, not connected',
55
+ statusUnconfigured: 'no token',
56
+ endpointPreset: 'Endpoint preset',
57
+ endpointCn: 'CN (mcp.dida365.com)',
58
+ endpointIntl: 'International (unverified)',
59
+ endpointCustom: 'Custom',
60
+ } as const
61
+
62
+ export type TicktickLocale = Record<TicktickLocaleKey, string>
63
+
64
+ export const zh: TicktickLocale = {
65
+ title: '滴答清单',
66
+ open: '打开滴答任务',
67
+ allLists: '全部清单',
68
+ viewUndone: '未完成',
69
+ viewCompleted: '已完成',
70
+ searchPlaceholder: '搜索任务…',
71
+ searchEmpty: '无匹配结果',
72
+ refresh: '刷新',
73
+ addPlaceholder: '添加任务…',
74
+ add: '添加',
75
+ empty: '暂无任务',
76
+ confirmDelete: '删除这个任务?',
77
+ delete: '删除',
78
+ complete: '完成',
79
+ setDue: '设置截止日期',
80
+ clearDue: '清除截止日期',
81
+ overdue: '已过期',
82
+ today: '今天',
83
+ tomorrow: '明天',
84
+ warning: '警告',
85
+ unconfigured: '未配置滴答令牌:请在 设置 → 插件 → TickTick 中设置。',
86
+ loadError: '加载失败:',
87
+ settingsName: 'TickTick',
88
+ settingsToken: 'API 口令(dp_ 开头)',
89
+ settingsTokenHint: '仅保存在本地,优先于令牌文件。',
90
+ settingsTokenFile: '令牌文件路径',
91
+ settingsTokenFileHint: '留空 = $DSH_HOME/.ticktick-token,每次请求重读。',
92
+ settingsMcpUrl: 'MCP 端点',
93
+ settingsMcpUrlHint: '修改端点后需重启 dsh。',
94
+ settingsProtected: '受保护任务 id',
95
+ settingsProtectedHint: '逗号分隔;变更操作拒绝这些任务(重启生效)。',
96
+ settingsSave: '保存',
97
+ settingsSaved: '已保存',
98
+ settingsTest: '测试连接',
99
+ settingsTestOk: '连接成功:N 个工具',
100
+ settingsTestFail: '连接失败:',
101
+ settingsClear: '清除凭据',
102
+ settingsCleared: '凭据已清除',
103
+ panelTokenPlaceholder: '粘贴 API 口令(dp_ 开头)',
104
+ panelTokenSave: '保存令牌',
105
+ panelTokenHint: '尚未配置令牌——在此粘贴,或前往 设置 → 插件 → TickTick。',
106
+ statusConnected: '已连接',
107
+ statusNotConnected: '已配置未连接',
108
+ statusUnconfigured: '未配置令牌',
109
+ endpointPreset: '端点预设',
110
+ endpointCn: '国内(mcp.dida365.com)',
111
+ endpointIntl: '国际版(未实测)',
112
+ endpointCustom: '自定义',
113
+ } as const
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Drag-reorder math. Measured TickTick semantics: lists order by DESCENDING
3
+ * sortOrder. "Place before target" = target.sortOrder + 1; "place at end" =
4
+ * current last.sortOrder − 1.
5
+ *
6
+ * @module dsh-ticktick/client/order
7
+ */
8
+
9
+ /** A task row with the fields the math reads. */
10
+ export interface SortableRow {
11
+ readonly id: string
12
+ readonly sortOrder: number | null
13
+ }
14
+
15
+ /**
16
+ * The sortOrder that places `dragged` immediately before `target`.
17
+ * @param target - the row the dragged task is dropped onto.
18
+ * @returns the new sortOrder, or `null` when the target carries none.
19
+ */
20
+ export function sortOrderBefore(target: SortableRow): number | null {
21
+ if (target.sortOrder === null) return null
22
+ return target.sortOrder + 1
23
+ }
24
+
25
+ /**
26
+ * The sortOrder that places a task at the list end (below the current last).
27
+ * @param rows - the visible list, ordered top to bottom.
28
+ * @returns the new sortOrder, or `null` when the list is empty or lacks orders.
29
+ */
30
+ export function sortOrderAtEnd(rows: readonly SortableRow[]): number | null {
31
+ let last: SortableRow | undefined
32
+ for (const row of rows) {
33
+ if (row.sortOrder !== null) last = row
34
+ }
35
+ if (last === undefined || last.sortOrder === null) return null
36
+ return last.sortOrder - 1
37
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The client-side Remote face of the `ticktick` namespace: the hand-written
3
+ * `TypertRemoteContribution` mounted through `ctx.remote.$mount`, plus the
4
+ * declaration merging that types `ctx.remote.ticktick`. The descriptor list
5
+ * is shared with the host `./typert` manifest (`../wire.ts`), so the two
6
+ * faces can never drift.
7
+ *
8
+ * @module dsh-ticktick/client/remote
9
+ */
10
+
11
+ import type { RemoteResult, TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol'
12
+ import { TICKTICK_INVOCATIONS } from '../wire.ts'
13
+ import type {
14
+ TicktickAddResult,
15
+ TicktickOkResult,
16
+ TicktickProbeResult,
17
+ TicktickStatus,
18
+ TicktickTasksResult,
19
+ } from '../wire.ts'
20
+
21
+ /** Result of `ticktick/projects`. */
22
+ export interface TicktickProjectsResult {
23
+ projects: readonly { id: string, name: string }[]
24
+ }
25
+
26
+ /** Result of `ticktick/batchAdd`. */
27
+ export interface TicktickBatchAddResult {
28
+ created: number
29
+ }
30
+
31
+ /** One batch-add row. */
32
+ export interface TicktickBatchAddRow {
33
+ title: string
34
+ projectId?: string
35
+ dueDate?: string
36
+ }
37
+
38
+ declare module '@deepseek-ai/dsh-typert-protocol' {
39
+ interface TypertRemoteNamespace$ticktick {
40
+ status: () => Promise<RemoteResult<TicktickStatus>>
41
+ projects: () => Promise<RemoteResult<TicktickProjectsResult>>
42
+ tasks: (projectId?: string) => Promise<RemoteResult<TicktickTasksResult>>
43
+ add: (title: string, projectId?: string, dueDate?: string) => Promise<RemoteResult<TicktickAddResult>>
44
+ complete: (id: string, projectId: string) => Promise<RemoteResult<TicktickOkResult>>
45
+ remove: (id: string, projectId: string) => Promise<RemoteResult<TicktickOkResult>>
46
+ setDue: (id: string, projectId: string | undefined, dueDate?: string) => Promise<RemoteResult<TicktickOkResult>>
47
+ reorder: (id: string, projectId: string | undefined, sortOrder: number) => Promise<RemoteResult<TicktickOkResult>>
48
+ completed: (projectId?: string, days?: number) => Promise<RemoteResult<TicktickTasksResult>>
49
+ search: (query: string) => Promise<RemoteResult<TicktickTasksResult>>
50
+ batchAdd: (tasks: TicktickBatchAddRow[]) => Promise<RemoteResult<TicktickBatchAddResult>>
51
+ probe: () => Promise<RemoteResult<TicktickProbeResult>>
52
+ }
53
+ interface TypertRemoteMap {
54
+ 'ticktick/status': () => Promise<RemoteResult<TicktickStatus>>
55
+ 'ticktick/projects': () => Promise<RemoteResult<TicktickProjectsResult>>
56
+ 'ticktick/tasks': (projectId?: string) => Promise<RemoteResult<TicktickTasksResult>>
57
+ 'ticktick/add': (title: string, projectId?: string, dueDate?: string) => Promise<RemoteResult<TicktickAddResult>>
58
+ 'ticktick/complete': (id: string, projectId: string) => Promise<RemoteResult<TicktickOkResult>>
59
+ 'ticktick/remove': (id: string, projectId: string) => Promise<RemoteResult<TicktickOkResult>>
60
+ 'ticktick/setDue': (id: string, projectId: string | undefined, dueDate?: string) => Promise<RemoteResult<TicktickOkResult>>
61
+ 'ticktick/reorder': (id: string, projectId: string | undefined, sortOrder: number) => Promise<RemoteResult<TicktickOkResult>>
62
+ 'ticktick/completed': (projectId?: string, days?: number) => Promise<RemoteResult<TicktickTasksResult>>
63
+ 'ticktick/search': (query: string) => Promise<RemoteResult<TicktickTasksResult>>
64
+ 'ticktick/batchAdd': (tasks: TicktickBatchAddRow[]) => Promise<RemoteResult<TicktickBatchAddResult>>
65
+ 'ticktick/probe': () => Promise<RemoteResult<TicktickProbeResult>>
66
+ }
67
+ interface TypertRemoteNamespaceMap {
68
+ ticktick: TypertRemoteNamespace$ticktick
69
+ }
70
+ }
71
+
72
+ /** The client Remote contribution for the `ticktick` namespace. */
73
+ export const TICKTICK_REMOTE = Object.freeze({
74
+ package: '@perrylink/dsh-ticktick',
75
+ descriptors: TICKTICK_INVOCATIONS,
76
+ }) satisfies TypertRemoteContribution
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Scoped stylesheet for the TickTick panel. Standalone bundles cannot use
3
+ * the in-repo CSS-module pipeline, so one inline <style> element carries the
4
+ * panel's scoped classes (tkt-* prefix).
5
+ *
6
+ * @module dsh-ticktick/client/styles
7
+ */
8
+
9
+ const TICKTICK_CSS = `
10
+ .tkt-anchor { position: relative; }
11
+ .tkt-button { display: inline-flex; align-items: center; gap: 4px; cursor: pointer; }
12
+ .tkt-panel {
13
+ position: absolute; top: calc(100% + 6px); right: 0; z-index: 1000;
14
+ width: 320px; max-height: 70vh; overflow: auto;
15
+ background: var(--ds-color-bg-elevated, #fff); color: var(--ds-color-text, #222);
16
+ border: 1px solid var(--ds-color-border, #d0d0d0); border-radius: 8px;
17
+ box-shadow: 0 8px 24px rgba(0,0,0,.15); padding: 10px; font-size: 13px;
18
+ }
19
+ .tkt-row { display: flex; align-items: center; gap: 8px; padding: 6px 2px; border-bottom: 1px solid var(--ds-color-border-subtle, #eee); }
20
+ .tkt-row.dragging { opacity: .45; }
21
+ .tkt-title { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
22
+ .tkt-title.done { text-decoration: line-through; opacity: .6; }
23
+ .tkt-chip { font-size: 11px; padding: 1px 6px; border-radius: 8px; white-space: nowrap; }
24
+ .tkt-chip.today { background: #e3f2fd; color: #1565c0; }
25
+ .tkt-chip.tomorrow { background: #e8f5e9; color: #2e7d32; }
26
+ .tkt-chip.overdue { background: #ffebee; color: #c62828; }
27
+ .tkt-chip.later { background: #f5f5f5; color: #616161; }
28
+ .tkt-list { background: var(--ds-color-bg-elevated, #fff); }
29
+ .tkt-input { width: 100%; box-sizing: border-box; padding: 6px 8px; border: 1px solid var(--ds-color-border, #d0d0d0); border-radius: 6px; }
30
+ .tkt-select { padding: 4px 6px; border: 1px solid var(--ds-color-border, #d0d0d0); border-radius: 6px; }
31
+ .tkt-warning { margin: 6px 0; padding: 6px 8px; background: #fff8e1; color: #8d6e63; border-radius: 6px; font-size: 12px; }
32
+ .tkt-error { margin: 6px 0; padding: 6px 8px; background: #ffebee; color: #c62828; border-radius: 6px; font-size: 12px; }
33
+ .tkt-iconbtn { border: none; background: transparent; cursor: pointer; padding: 2px 4px; font-size: 13px; color: var(--ds-color-text-subtle, #666); }
34
+ .tkt-iconbtn:hover { color: var(--ds-color-text, #222); }
35
+ .tkt-date { border: 1px solid var(--ds-color-border, #d0d0d0); border-radius: 6px; font-size: 12px; padding: 2px 4px; }
36
+ `
37
+
38
+ /**
39
+ * Install the scoped stylesheet once per plugin fiber.
40
+ * @returns the effect disposer that removes the style element.
41
+ */
42
+ export function installPanelStyles(): () => void {
43
+ const style = document.createElement('style')
44
+ style.setAttribute('data-dsh-ticktick', '')
45
+ style.textContent = TICKTICK_CSS
46
+ document.head.appendChild(style)
47
+ return () => { style.remove() }
48
+ }
package/src/config.ts ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Plugin configuration and its explicit resolve step. `resolveConfig`
3
+ * re-judges every default and bound so programmatic construction that
4
+ * bypasses Schemastery normalization still fails loud instead of running
5
+ * with hidden defaults (the explicit-resolve contract).
6
+ *
7
+ * @module dsh-ticktick/config
8
+ */
9
+
10
+ import z from '@deepseek-ai/schemastery'
11
+
12
+ /** Default TickTick MCP endpoint (CN region). */
13
+ export const DEFAULT_MCP_URL = 'https://mcp.dida365.com'
14
+
15
+ /** Default per-tools/call deadline in milliseconds. */
16
+ export const DEFAULT_TOOL_CALL_TIMEOUT_MS = 30_000
17
+
18
+ /** Ceiling for one tool call: the endpoint may block on a slow server round. */
19
+ export const MAX_TOOL_CALL_TIMEOUT_MS = 300_000
20
+
21
+ /** Raw MCP tool-name pins; every `''` entry is discovered by pattern. */
22
+ export interface ToolPins {
23
+ projects: string
24
+ tasks: string
25
+ create: string
26
+ complete: string
27
+ remove: string
28
+ update: string
29
+ move: string
30
+ completed: string
31
+ search: string
32
+ getTask: string
33
+ batchAdd: string
34
+ }
35
+
36
+ /** Configuration for the TickTick bridge. */
37
+ export interface Config {
38
+ /** Path to a text file holding the Bearer token; `''` resolves to `<DSH_HOME>/.ticktick-token`. */
39
+ tokenFile?: string
40
+ /** TickTick MCP endpoint. */
41
+ mcpUrl?: string
42
+ /** Per-tools/call deadline in milliseconds. */
43
+ toolCallTimeoutMs?: number
44
+ /** Task ids every mutating operation refuses. */
45
+ protectedTaskIds?: string[]
46
+ /** Optional raw tool-name pins, discovered by pattern when omitted. */
47
+ tools?: Partial<ToolPins>
48
+ }
49
+
50
+ /** Fully resolved configuration captured at plugin load. */
51
+ export interface ResolvedConfig {
52
+ /** Path to a text file holding the Bearer token (empty = default path). */
53
+ tokenFile: string
54
+ /** TickTick MCP endpoint. */
55
+ mcpUrl: string
56
+ /** Per-tools/call deadline in milliseconds. */
57
+ toolCallTimeoutMs: number
58
+ /** Task ids every mutating operation refuses. */
59
+ protectedTaskIds: readonly string[]
60
+ /** Raw tool-name pins (`''` = discover). */
61
+ tools: ToolPins
62
+ }
63
+
64
+ /** Schemastery schema for loader-validated configuration. */
65
+ export const Config: z<Config> = z.object({
66
+ tokenFile: z.string().default(''),
67
+ mcpUrl: z.string().default(DEFAULT_MCP_URL),
68
+ toolCallTimeoutMs: z.number().min(1_000).max(MAX_TOOL_CALL_TIMEOUT_MS).default(DEFAULT_TOOL_CALL_TIMEOUT_MS),
69
+ protectedTaskIds: z.array(z.string()).default([]),
70
+ tools: z.object({
71
+ projects: z.string().default(''),
72
+ tasks: z.string().default(''),
73
+ create: z.string().default(''),
74
+ complete: z.string().default(''),
75
+ remove: z.string().default(''),
76
+ update: z.string().default(''),
77
+ move: z.string().default(''),
78
+ completed: z.string().default(''),
79
+ search: z.string().default(''),
80
+ getTask: z.string().default(''),
81
+ batchAdd: z.string().default(''),
82
+ }).default({
83
+ projects: '',
84
+ tasks: '',
85
+ create: '',
86
+ complete: '',
87
+ remove: '',
88
+ update: '',
89
+ move: '',
90
+ completed: '',
91
+ search: '',
92
+ getTask: '',
93
+ batchAdd: '',
94
+ }),
95
+ })
96
+
97
+ /**
98
+ * Resolve raw config to the runtime policy, re-validating defaults and bounds.
99
+ * @param config - raw loader config; `undefined` for a bare row.
100
+ * @returns the frozen resolved config.
101
+ */
102
+ export function resolveConfig(config: Config | undefined): ResolvedConfig {
103
+ const tokenFile = config?.tokenFile ?? ''
104
+ if (typeof tokenFile !== 'string') throw new TypeError('dsh-ticktick: config.tokenFile must be a string')
105
+ const mcpUrl = config?.mcpUrl ?? DEFAULT_MCP_URL
106
+ if (typeof mcpUrl !== 'string' || mcpUrl === '') throw new Error('dsh-ticktick: config.mcpUrl must be a non-empty string')
107
+ const toolCallTimeoutMs = config?.toolCallTimeoutMs ?? DEFAULT_TOOL_CALL_TIMEOUT_MS
108
+ if (!Number.isFinite(toolCallTimeoutMs) || toolCallTimeoutMs < 1_000 || toolCallTimeoutMs > MAX_TOOL_CALL_TIMEOUT_MS) {
109
+ throw new Error(`dsh-ticktick: config.toolCallTimeoutMs must be a finite number between 1000 and ${MAX_TOOL_CALL_TIMEOUT_MS}`)
110
+ }
111
+ const protectedTaskIds = (config?.protectedTaskIds ?? []).filter((id): id is string => typeof id === 'string' && id !== '')
112
+ const rawTools = config?.tools
113
+ const pin = (key: keyof ToolPins): string => {
114
+ const value = rawTools?.[key]
115
+ return typeof value === 'string' ? value : ''
116
+ }
117
+ return Object.freeze({
118
+ tokenFile,
119
+ mcpUrl,
120
+ toolCallTimeoutMs,
121
+ protectedTaskIds: Object.freeze(protectedTaskIds),
122
+ tools: Object.freeze({
123
+ projects: pin('projects'),
124
+ tasks: pin('tasks'),
125
+ create: pin('create'),
126
+ complete: pin('complete'),
127
+ remove: pin('remove'),
128
+ update: pin('update'),
129
+ move: pin('move'),
130
+ completed: pin('completed'),
131
+ search: pin('search'),
132
+ getTask: pin('getTask'),
133
+ batchAdd: pin('batchAdd'),
134
+ }),
135
+ })
136
+ }