@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.
- package/CHANGELOG.md +24 -0
- package/LICENSE +201 -0
- package/README.es.md +41 -0
- package/README.hi.md +33 -0
- package/README.md +111 -0
- package/README.pt.md +41 -0
- package/README.zh.md +111 -0
- package/cordis.patch.yml +35 -0
- package/lib/client.js +6233 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +1349 -0
- package/lib/typert.host.js +26 -0
- package/lib/types/client/TicktickAction.d.ts +29 -0
- package/lib/types/client/TicktickAction.d.ts.map +1 -0
- package/lib/types/client/TicktickSettingsCard.d.ts +27 -0
- package/lib/types/client/TicktickSettingsCard.d.ts.map +1 -0
- package/lib/types/client/api.d.ts +34 -0
- package/lib/types/client/api.d.ts.map +1 -0
- package/lib/types/client/dates.d.ts +26 -0
- package/lib/types/client/dates.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +34 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +61 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/order.d.ts +25 -0
- package/lib/types/client/order.d.ts.map +1 -0
- package/lib/types/client/remote.d.ts +260 -0
- package/lib/types/client/remote.d.ts.map +1 -0
- package/lib/types/client/styles.d.ts +13 -0
- package/lib/types/client/styles.d.ts.map +1 -0
- package/lib/types/config.d.ts +64 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/domain.d.ts +70 -0
- package/lib/types/domain.d.ts.map +1 -0
- package/lib/types/index.d.ts +56 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/mcp.d.ts +94 -0
- package/lib/types/mcp.d.ts.map +1 -0
- package/lib/types/service.d.ts +160 -0
- package/lib/types/service.d.ts.map +1 -0
- package/lib/types/tools.d.ts +18 -0
- package/lib/types/tools.d.ts.map +1 -0
- package/lib/types/typert.host.d.ts +216 -0
- package/lib/types/typert.host.d.ts.map +1 -0
- package/lib/types/wire.d.ts +561 -0
- package/lib/types/wire.d.ts.map +1 -0
- package/lib/wire-C-vDxxnC.js +5288 -0
- package/package.json +187 -0
- package/probes/lib.mjs +65 -0
- package/probes/probe-bootstrap.mjs +10 -0
- package/probes/probe-crud.mjs +15 -0
- package/probes/probe-due.mjs +28 -0
- package/probes/probe-queries.mjs +18 -0
- package/probes/probe-reorder.mjs +24 -0
- package/src/client/TicktickAction.tsx +356 -0
- package/src/client/TicktickSettingsCard.tsx +182 -0
- package/src/client/api.ts +56 -0
- package/src/client/dates.ts +61 -0
- package/src/client/index.ts +119 -0
- package/src/client/locales.ts +113 -0
- package/src/client/order.ts +37 -0
- package/src/client/remote.ts +76 -0
- package/src/client/styles.ts +48 -0
- package/src/config.ts +136 -0
- package/src/domain.ts +224 -0
- package/src/index.ts +139 -0
- package/src/mcp.ts +213 -0
- package/src/service.ts +403 -0
- package/src/tools.ts +336 -0
- package/src/typert.host.ts +25 -0
- 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
|
+
}
|