@alpacachen/dsh-kanban 1.3.0 → 1.3.1

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 (4) hide show
  1. package/README.md +53 -119
  2. package/README.zh.md +48 -114
  3. package/index.js +136 -92
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -2,171 +2,105 @@
2
2
 
3
3
  # dsh-kanban
4
4
 
5
- **Plan with your AI. See the work move.**
5
+ ### Plan with AI. Move work forward visually.
6
6
 
7
- A shared kanban board for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness), built for both people and agents.
7
+ A collaborative kanban board where you and your AI agent plan, organize, and ship work together—without leaving DeepSeek Harness.
8
8
 
9
- [简体中文](README.zh.md) · English
9
+ [![npm version](https://img.shields.io/npm/v/@alpacachen/dsh-kanban?color=5b8def&label=npm)](https://www.npmjs.com/package/@alpacachen/dsh-kanban)
10
+ ![DeepSeek Harness Plugin](https://img.shields.io/badge/DeepSeek%20Harness-Plugin-7c5cff)
11
+ ![License](https://img.shields.io/badge/license-MIT-22c55e)
10
12
 
11
- </div>
13
+ [简体中文](README.zh.md) · **English**
12
14
 
13
- Turn a conversation into an actionable plan without copying tasks between your AI assistant and a project tracker. Ask the agent to break down a feature, prioritize a bug, or move finished work—the same changes appear instantly in the **Board** tab, where you can continue organizing everything by hand.
15
+ </div>
14
16
 
15
17
  <p align="center">
16
18
  <img src="./image.png" alt="dsh-kanban board inside DeepSeek Harness" width="100%">
17
19
  </p>
18
20
 
19
- ## Why dsh-kanban?
21
+ ## From conversation to action
22
+
23
+ Stop copying AI-generated plans into a separate project tracker. Tell your agent what you want to build, and watch it turn the plan into cards, priorities, labels, and workflow updates on the **Board** tab.
20
24
 
21
- - **Plan in conversation**14 `kanban_*` tools let the agent create and manage cards, lists, and labels.
22
- - **Stay in control** — edit cards, drag work between lists, reorder your workflow, and filter by priority from the UI.
23
- - **Keep projects separate** — every DSH workspace gets its own board and persisted data.
24
- - **Feel at home in DSH** — the board follows the active language and theme, including dark mode.
25
- - **Install and go** — the browser bundle is prebuilt; no frontend toolchain or native build step is required.
25
+ You can take over at any timedrag a card, refine a note, reorder a list—and the agent immediately works from the same board.
26
26
 
27
- ## Quick start
27
+ | 💬 Plan naturally | 🧭 Stay in control | ⚡ Start instantly |
28
+ | --- | --- | --- |
29
+ | Manage the board with 14 built-in `kanban_*` tools. | Edit, drag, prioritize, label, and filter from the UI. | Install the prebuilt plugin—no frontend setup required. |
30
+ | **🗂️ Keep work separate** | **🌓 Feel at home** | **🤝 Share one source of truth** |
31
+ | Every DSH workspace gets its own board. | Follows your DSH language, theme, and dark mode. | You and the agent always see the same tasks and status. |
28
32
 
29
- ### 1. Install the plugin
33
+ ## 🚀 Get started
30
34
 
31
- Install the published npm package:
35
+ ### 1. Install
32
36
 
33
37
  ```sh
34
38
  dsh plugin --profile web add @alpacachen/dsh-kanban
35
39
  ```
36
40
 
37
- GitHub source checkouts are not a supported installation method because the generated browser bundle is created only by CI for the npm package.
38
-
39
- Restart `dsh web` after installation so the plugin bundle is loaded. To confirm that it was added successfully:
41
+ Restart `dsh web` so the plugin bundle is loaded. You can confirm the installation with:
40
42
 
41
43
  ```sh
42
44
  dsh --profile web --dump-config
43
45
  ```
44
46
 
45
- The output should include a `dsh-kanban` layer.
47
+ Look for a `dsh-kanban` layer in the output.
48
+
49
+ ### 2. Open your board
46
50
 
47
- ### 2. Open the board
51
+ Enter any workspace and choose the **Board** tab beside the conversation views. A fresh board starts with four familiar stages:
48
52
 
49
- Open a workspace and select the **Board** tab next to the conversation views. A new board starts with **Todo**, **In Progress**, **Review**, and **Done**.
53
+ `Todo` `In Progress` → `Review` `Done`
50
54
 
51
- ### 3. Plan with the agent
55
+ ### 3. Ask your agent to plan
52
56
 
53
- Try a prompt like:
57
+ Try this:
54
58
 
55
- > Break the authentication feature into implementation tasks, add one card per step, and mark the security review as P0.
59
+ > Break the authentication feature into implementation tasks, create one card per step, and mark the security review as P0.
56
60
 
57
- The agent updates the board directly. You can then refine the plan in the UI, or keep working through conversation.
61
+ The cards appear directly on your board. Continue in conversation, or switch to the Board tab and shape the plan by hand.
58
62
 
59
- ## What you can do
63
+ ## 🧩 Everything you need to keep work moving
60
64
 
61
- ### From the Board tab
65
+ ### On the Board
62
66
 
63
67
  - Create, edit, delete, and drag cards between lists
64
68
  - Add notes, labels, and P0 / P1 / P2 priorities
65
- - View a card's activity history: when it was created and by whom, plus label / priority / status changes
66
- - Create, rename, reorder, and remove lists
67
- - Create labels and customize their colors
68
- - Filter the board by priority
69
-
70
- ### From a conversation
71
-
72
- The plugin makes these tools available automatically in every workspace conversation:
73
-
74
- | Area | Tools |
75
- | --- | --- |
76
- | Board | `kanban_get` |
77
- | Cards | `kanban_get_card`, `kanban_add_card`, `kanban_update_card`, `kanban_delete_card`, `kanban_move_card` |
78
- | Lists | `kanban_add_column`, `kanban_rename_column`, `kanban_delete_column`, `kanban_move_column` |
79
- | Labels | `kanban_get_label`, `kanban_add_label`, `kanban_update_label`, `kanban_delete_label` |
69
+ - Open a card's activity timeline to see when it changed, what changed, and whether you or the agent made the change
70
+ - Track card creation, moves, title and note edits, label changes, and priority changes
71
+ - Send any card to the agent in your current conversation or a new one
72
+ - Create, rename, reorder, and remove workflow lists
73
+ - Create color-coded labels for fast visual scanning
74
+ - Filter the entire board by priority, or manually refresh it whenever needed
80
75
 
81
- A few more things you can ask:
76
+ ### Continue any card with your agent
82
77
 
83
- - “Summarize what is currently in progress.”
84
- - “Move ‘Fix login redirect’ to In Progress and set it to P0.”
85
- - “Add a Blocked list after In Progress.”
86
- - “Create a red Urgent label and apply it to the release card.”
87
-
88
- ## Data and persistence
89
-
90
- Boards are isolated by workspace and stored through the DSH filesystem service as `kanban-board-<workspaceId>.json`. They survive browser refreshes and DSH restarts, and uninstalling the plugin does not delete existing board data.
91
-
92
- ### Schema versioning and safe migration
93
-
94
- Persisted files carry a `schemaVersion` field (currently `1`). Files written by older versions (without a version field) are treated as `v0` and are **automatically upgraded the first time the workspace board is opened** — the pre-upgrade file is copied to `kanban-board-<workspaceId>.json.bak-v0` first, then the upgraded file is written back. Future format changes simply bump `SCHEMA_VERSION` and register a step-by-step migration function in `MIGRATIONS` (`index.js`).
95
-
96
- #### Adding a future format version (e.g. v2)
97
-
98
- Migration functions live in `MIGRATIONS` in `index.js` and are pure functions — they transform data only, never touch the filesystem or runtime state. Each step upgrades by exactly one version; `migrateBoard` chains them automatically, so a `v0` file walks `v0 → v1 → v2`. Backups and write-back are handled by the load pipeline.
99
-
100
- ```js
101
- export const SCHEMA_VERSION = 2 // 1) bump the version
102
-
103
- export const MIGRATIONS = {
104
- 0: (data) => ({ /* existing v0 → v1, unchanged */ }),
105
- 1: (data) => ({
106
- schemaVersion: 2, // 2) output must declare the new version
107
- columns: data.columns,
108
- labels: data.labels,
109
- cards: data.cards.map((c) => ({ ...c, labelId: c.label ?? null })), // e.g. label → labelId
110
- }),
111
- }
112
- ```
78
+ Open a card and choose **Chat with agent**. dsh-kanban carries its ID, title, label, and notes into either the current conversation or a fresh one, so the agent has the context it needs immediately. The content is inserted as an editable draft—it is never sent until you choose to send it.
113
79
 
114
- If the on-disk shape changes, update `validateBoard` in `index.js` and the client types in `src/client/lib/types.ts` accordingly, and extend the self-checks in `scripts/check-schema.mjs` / `scripts/check-pipeline.mjs`.
80
+ ### In conversation
115
81
 
116
- Abnormal data never makes the board unusable:
82
+ The plugin automatically gives the agent these tools in every workspace:
117
83
 
118
- - **Corrupt/unparseable files** are backed up to `kanban-board-<workspaceId>.json.corrupt-<timestamp>` and the board opens empty, with a clear notice.
119
- - **Files from a newer plugin version** (higher `schemaVersion`) are backed up to `kanban-board-<workspaceId>.json.unsupported-v<version>` and left untouched, so no data is destroyed by a downgraded plugin.
120
- - At plugin startup a **validation scan** checks every board file (parse + version + structure), backs up any corrupt files, and logs a summary.
121
-
122
- Notices from migrations, corruption, or unsupported versions are surfaced both in the agent tool results (`warnings`) and as a banner in the Board tab.
123
-
124
- ## How it works
125
-
126
- `dsh-kanban` uses the standard DSH bundle format:
127
-
128
- - `index.js` registers the agent tools and the `/api/kanban` browser endpoint.
129
- - Both interfaces share one data layer, so the agent and the Board tab always operate on the same board.
130
- - `cordis.patch.yml` adds the plugin to the selected DSH profile.
131
- - `lib/client.js` is the prebuilt web client loaded by DSH.
132
-
133
- The client is built with React, TypeScript, Tailwind CSS, shadcn/ui, Radix UI, and dnd-kit. React itself is provided by DSH, keeping the distributed bundle smaller and avoiding duplicate runtimes.
134
-
135
- ## Development
136
-
137
- Requirements: Node.js and pnpm.
138
-
139
- ```sh
140
- pnpm install
141
- pnpm build
142
- pnpm typecheck
143
- ```
84
+ | Area | Tools |
85
+ | --- | --- |
86
+ | **Board** | `kanban_get` |
87
+ | **Cards** | `kanban_get_card`, `kanban_add_card`, `kanban_update_card`, `kanban_delete_card`, `kanban_move_card` |
88
+ | **Lists** | `kanban_add_column`, `kanban_rename_column`, `kanban_delete_column`, `kanban_move_column` |
89
+ | **Labels** | `kanban_get_label`, `kanban_add_label`, `kanban_update_label`, `kanban_delete_label` |
144
90
 
145
- For an unminified client bundle while debugging:
91
+ ## 💡 Things to ask your agent
146
92
 
147
- ```sh
148
- node build.mjs --no-minify
149
- ```
93
+ > “Summarize what is currently in progress.”
150
94
 
151
- Project layout:
95
+ > “Move ‘Fix login redirect’ to In Progress and set it to P0.”
152
96
 
153
- ```text
154
- index.js # Host plugin, tools, persistence, and HTTP endpoint
155
- cordis.patch.yml # DSH bundle patch
156
- build.mjs # Client build script
157
- src/client/ # React and TypeScript source
158
- lib/client.js # Generated locally or by CI; included only in the npm package
159
- ```
160
-
161
- ## Contributing
97
+ > “Add a Blocked list after In Progress.”
162
98
 
163
- Issues and pull requests are welcome. If you are planning a larger change, opening an issue first is a good way to align on the approach before implementation.
99
+ > “Create a red Urgent label and apply it to the release card.”
164
100
 
165
- ## Uninstall
101
+ > “I'm a lawyer. Set up a board for my day-to-day casework, create the workflow stages and labels I’ll need, and keep them up to date as my work evolves.”
166
102
 
167
- ```sh
168
- dsh plugin --profile web remove @alpacachen/dsh-kanban
169
- ```
103
+ You do not need to design the workflow first. Tell the agent your role, situation, or goal, and it can create suitable stages and labels—then keep refining them as you work.
170
104
 
171
105
  ## License
172
106
 
package/README.zh.md CHANGED
@@ -2,171 +2,105 @@
2
2
 
3
3
  # dsh-kanban
4
4
 
5
- **和 AI 一起规划,在看板上看到进展。**
5
+ ### AI 一起规划,让每一步进展清晰可见。
6
6
 
7
- 一个为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 打造、由你和 Agent 共同维护的项目看板。
7
+ 一块真正由你和 AI Agent 共同维护的项目看板——从想法、任务拆解到推进交付,全程无需离开 DeepSeek Harness。
8
8
 
9
- 简体中文 · [English](README.md)
9
+ [![npm version](https://img.shields.io/npm/v/@alpacachen/dsh-kanban?color=5b8def&label=npm)](https://www.npmjs.com/package/@alpacachen/dsh-kanban)
10
+ ![DeepSeek Harness Plugin](https://img.shields.io/badge/DeepSeek%20Harness-Plugin-7c5cff)
11
+ ![License](https://img.shields.io/badge/license-MIT-22c55e)
10
12
 
11
- </div>
13
+ **简体中文** · [English](README.md)
12
14
 
13
- 不用再把 AI 给出的计划手动搬进项目管理工具。让 Agent 拆解功能、调整优先级或推进任务,变更会立即出现在 **「看板」**标签页;你也可以随时接手,通过拖拽和编辑继续整理工作。
15
+ </div>
14
16
 
15
17
  <p align="center">
16
18
  <img src="./image.png" alt="DeepSeek Harness 中的 dsh-kanban 看板界面" width="100%">
17
19
  </p>
18
20
 
19
- ## 为什么是 dsh-kanban?
21
+ ## 从一句话,到一块能执行的看板
22
+
23
+ 不必再把 AI 生成的计划手动搬进项目管理工具。告诉 Agent 你想完成什么,它就能在 **「看板」** 标签页中拆分任务、创建卡片、设置优先级和更新进度。
20
24
 
21
- - **在对话里完成规划**:14 个 `kanban_*` 工具让 Agent 能直接管理卡片、列表和标签。
22
- - **一切仍由你掌控**:编辑卡片、跨列拖拽、调整工作流,或按优先级筛选任务。
23
- - **项目之间互不干扰**:每个 DSH 工作区都有独立看板,并自动持久化数据。
24
- - **自然融入 DSH**:界面跟随当前语言和主题,也支持暗色模式。
25
- - **安装后即可使用**:浏览器端已预构建,不需要额外配置前端工具链,也没有原生构建步骤。
25
+ 你也可以随时接手:拖动卡片、补充备注、调整列表。你和 Agent 始终面对同一块看板、同一份进度。
26
26
 
27
- ## 快速开始
27
+ | 💬 自然对话 | 🧭 随时掌控 | ⚡ 开箱即用 |
28
+ | --- | --- | --- |
29
+ | 通过 14 个 `kanban_*` 工具直接管理任务。 | 在界面中编辑、拖拽、标记和筛选卡片。 | 浏览器端已预构建,无需配置前端环境。 |
30
+ | **🗂️ 项目隔离** | **🌓 原生体验** | **🤝 实时协作** |
31
+ | 每个 DSH 工作区都有自己的独立看板。 | 自动跟随 DSH 的语言、主题与暗色模式。 | 你做的调整,Agent 立即可见;反之亦然。 |
28
32
 
29
- ### 1. 安装插件
33
+ ## 🚀 三步开始使用
30
34
 
31
- npm 安装已发布的包:
35
+ ### 1. 安装
32
36
 
33
37
  ```sh
34
38
  dsh plugin --profile web add @alpacachen/dsh-kanban
35
39
  ```
36
40
 
37
- 项目不支持从 GitHub 源码安装,因为生成的浏览器 bundle 只会由 CI 构建并放入 npm 包。
38
-
39
- 安装后重启 `dsh web`,让插件 bundle 在启动时加载。可以通过下面的命令确认配置:
41
+ 安装后重启 `dsh web`,让插件 bundle 完成加载。你可以用下面的命令确认安装结果:
40
42
 
41
43
  ```sh
42
44
  dsh --profile web --dump-config
43
45
  ```
44
46
 
45
- 如果安装成功,输出中会包含 `dsh-kanban` 配置层。
47
+ 如果输出中包含 `dsh-kanban` 配置层,就说明安装成功。
46
48
 
47
49
  ### 2. 打开看板
48
50
 
49
- 进入任意工作区,在会话视图旁打开**「看板」**标签页。新看板默认包含 **Todo**、**In Progress**、**Review** 和 **Done** 四个列表。
51
+ 进入任意工作区,点击会话视图旁的 **「看板」** 标签页。新看板已经准备好四个常用阶段:
52
+
53
+ `Todo` → `In Progress` → `Review` → `Done`
50
54
 
51
55
  ### 3. 让 Agent 开始规划
52
56
 
53
57
  试试这样说:
54
58
 
55
- > 把登录功能拆成具体的实现任务,每一步建一张卡片,并把安全审查标为 P0。
59
+ > 把登录功能拆成具体的实现任务,每一步创建一张卡片,并把安全审查标为 P0。
56
60
 
57
- Agent 会直接更新当前看板。之后你可以在界面中继续调整,也可以留在对话里推进任务。
61
+ 任务会直接出现在看板中。你可以继续通过对话推进,也可以切换到看板,用拖拽和编辑完善计划。
58
62
 
59
- ## 你可以做什么
63
+ ## 🧩 推进工作需要的能力,都在这里
60
64
 
61
65
  ### 在「看板」标签页中
62
66
 
63
67
  - 创建、编辑、删除卡片,并将卡片拖到其他列表
64
68
  - 为卡片补充备注、标签和 P0 / P1 / P2 优先级
65
- - 查看卡片的活动记录:创建时间与来源,以及标签、优先级、状态的变更历史
66
- - 创建、重命名、排序和删除列表
67
- - 创建标签并自定义颜色
68
- - 按优先级筛选整个看板
69
+ - 打开卡片活动时间线,查看变更时间、具体内容,以及操作来自你还是 Agent
70
+ - 追踪卡片创建、移动、标题与备注编辑、标签变化和优先级变化
71
+ - 从任意卡片发起对话,在当前会话或新会话中让 Agent 接着处理
72
+ - 创建、重命名、排序和删除工作流列表
73
+ - 创建彩色标签,让不同任务一眼可辨
74
+ - 按优先级筛选整个看板,也可以随时手动刷新
75
+
76
+ ### 从任意卡片继续与 Agent 协作
77
+
78
+ 打开卡片并选择 **「与 Agent 聊一聊」**,dsh-kanban 会把卡片 ID、标题、标签和备注带入当前会话或一个新会话,让 Agent 立即获得完整上下文。内容只会填入输入框作为可编辑草稿,由你确认后发送,不会自动提交。
69
79
 
70
80
  ### 在对话中
71
81
 
72
- 插件会自动为工作区内的对话提供以下工具:
82
+ 插件会自动为每个工作区中的 Agent 提供以下工具:
73
83
 
74
84
  | 范围 | 工具 |
75
85
  | --- | --- |
76
- | 看板 | `kanban_get` |
77
- | 卡片 | `kanban_get_card`、`kanban_add_card`、`kanban_update_card`、`kanban_delete_card`、`kanban_move_card` |
78
- | 列表 | `kanban_add_column`、`kanban_rename_column`、`kanban_delete_column`、`kanban_move_column` |
79
- | 标签 | `kanban_get_label`、`kanban_add_label`、`kanban_update_label`、`kanban_delete_label` |
80
-
81
- 你还可以这样使用:
82
-
83
- - 「总结一下目前正在进行的任务。」
84
- - 「把『修复登录跳转』移到进行中,并设为 P0。」
85
- - 「在进行中后面新增一个阻塞列表。」
86
- - 「创建一个红色的『紧急』标签,并加到发布卡片上。」
87
-
88
- ## 数据与持久化
89
-
90
- 看板按工作区隔离,并通过 DSH 文件系统服务保存为 `kanban-board-<workspaceId>.json`。刷新页面或重启 DSH 都不会丢失数据;卸载插件时,已有的看板数据也会保留。
91
-
92
- ### 版本化与安全迁移
93
-
94
- 持久化文件带 `schemaVersion` 字段(当前为 `1`)。旧版本插件写入的文件(无版本字段)按 `v0` 处理,在**该工作区看板首次打开时自动升级**——先复制出 `kanban-board-<workspaceId>.json.bak-v0` 备份,再写回升级后的文件。后续格式演进只需递增 `SCHEMA_VERSION` 并在 `MIGRATIONS`(`index.js`)里注册逐版本迁移函数。
95
-
96
- #### 新增一个格式版本(以 v2 为例)
97
-
98
- 迁移函数写在 `index.js` 的 `MIGRATIONS` 注册表里,必须是**纯函数**——只做数据变换,不触碰文件系统或运行时状态。每步只升级一个版本,`migrateBoard` 会自动串联,v0 文件会依次走 `v0 → v1 → v2`;备份与写回由加载管线处理,无需手写脚本。
99
-
100
- ```js
101
- export const SCHEMA_VERSION = 2 // 1) 递增版本号
102
-
103
- export const MIGRATIONS = {
104
- 0: (data) => ({ /* 现有 v0 → v1,保持不变 */ }),
105
- 1: (data) => ({
106
- schemaVersion: 2, // 2) 输出必须声明新版本号
107
- columns: data.columns,
108
- labels: data.labels,
109
- cards: data.cards.map((c) => ({ ...c, labelId: c.label ?? null })), // 例如 label → labelId
110
- }),
111
- }
112
- ```
113
-
114
- 如果磁盘形状发生变化,同步更新 `index.js` 里的 `validateBoard` 与 `src/client/lib/types.ts` 的客户端类型,并在 `scripts/check-schema.mjs` / `scripts/check-pipeline.mjs` 中补充自检用例。
86
+ | **看板** | `kanban_get` |
87
+ | **卡片** | `kanban_get_card`、`kanban_add_card`、`kanban_update_card`、`kanban_delete_card`、`kanban_move_card` |
88
+ | **列表** | `kanban_add_column`、`kanban_rename_column`、`kanban_delete_column`、`kanban_move_column` |
89
+ | **标签** | `kanban_get_label`、`kanban_add_label`、`kanban_update_label`、`kanban_delete_label` |
115
90
 
116
- 异常数据不会让看板不可用:
91
+ ## 💡 你可以直接这样说
117
92
 
118
- - **损坏/无法解析的文件**:备份为 `kanban-board-<workspaceId>.json.corrupt-<时间戳>`,看板以空板打开,并给出清晰提示。
119
- - **来自更新版本插件的文件**(`schemaVersion` 更高):备份为 `kanban-board-<workspaceId>.json.unsupported-v<版本>` 且不改动原文件,避免降级插件破坏新数据。
120
- - 插件启动时会执行**全量校验扫描**:逐一检查看板文件(解析 + 版本 + 结构),损坏文件立即备份并汇总记录。
93
+ > 「总结一下目前正在进行的任务。」
121
94
 
122
- 迁移、损坏或版本超前的提示会同时出现在 Agent 工具结果(`warnings`)和「看板」标签页的提示横幅中。
95
+ > 「把『修复登录跳转』移到进行中,并设为 P0。」
123
96
 
124
- ## 工作原理
97
+ > 「在进行中后面新增一个阻塞列表。」
125
98
 
126
- `dsh-kanban` 使用标准的 DSH bundle 格式:
99
+ > 「创建一个红色的『紧急』标签,并加到发布卡片上。」
127
100
 
128
- - `index.js` 注册 Agent 工具和浏览器端使用的 `/api/kanban` 接口。
129
- - Agent 与「看板」标签页共享同一套数据层,双方看到的始终是同一块看板。
130
- - `cordis.patch.yml` 将插件加入指定的 DSH profile。
131
- - `lib/client.js` 是由 DSH 直接加载的预构建 Web 客户端。
101
+ > 「我是一名律师,请帮我初始化一套适合日常办案的看板,根据工作流程创建阶段列表和常用标签,并随着工作变化持续帮我维护。」
132
102
 
133
- 客户端使用 React、TypeScript、Tailwind CSS、shadcn/ui、Radix UI 和 dnd-kit 构建。React 由 DSH 提供,以减少发布体积并避免重复运行时。
134
-
135
- ## 本地开发
136
-
137
- 需要准备 Node.js 和 pnpm。
138
-
139
- ```sh
140
- pnpm install
141
- pnpm build
142
- pnpm typecheck
143
- ```
144
-
145
- 调试时可以生成未压缩的客户端 bundle:
146
-
147
- ```sh
148
- node build.mjs --no-minify
149
- ```
150
-
151
- 项目结构:
152
-
153
- ```text
154
- index.js # 宿主插件、Agent 工具、持久化与 HTTP 接口
155
- cordis.patch.yml # DSH bundle 补丁
156
- build.mjs # 客户端构建脚本
157
- src/client/ # React 与 TypeScript 源码
158
- lib/client.js # 本地或 CI 生成,仅随 npm 包发布
159
- ```
160
-
161
- ## 参与贡献
162
-
163
- 欢迎提交 Issue 和 Pull Request。如果准备进行较大的改动,建议先开一个 Issue 讨论目标和实现方式,避免重复工作。
164
-
165
- ## 卸载
166
-
167
- ```sh
168
- dsh plugin --profile web remove @alpacachen/dsh-kanban
169
- ```
103
+ 不必提前设计好工作流。只需告诉 Agent 你的职业、场景或目标,它就能为你创建合适的阶段和标签,并在后续协作中持续调整。
170
104
 
171
105
  ## 开源许可
172
106
 
package/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * 职责:
8
8
  * - 按工作区(项目)隔离:boards 以 workspaceId 为键,每个工作区一块独立看板
9
- * - 磁盘持久化:经 ctx.fs 写入 <workspaceRoot>/kanban-board-<workspaceId>.json
9
+ * - 磁盘持久化:经 ctx.fs 写入 <workspace.path>/.dsh-kanban.json
10
10
  * - 模型工具:经 ctx.tools.register 注册 14 个 kanban_* 工具
11
11
  * - 浏览器数据层:经 ctx.get('webServer') 注册 /api/kanban 前缀路由
12
12
  *
@@ -275,41 +275,53 @@ export function apply(ctx) {
275
275
  ]
276
276
 
277
277
  // ---- 持久化定位 ----
278
- const root = () => {
279
- const p = getPolicy()
280
- return p && typeof p.workspaceRoot === 'string' ? p.workspaceRoot : undefined
278
+ const BOARD_FILE = '.dsh-kanban.json'
279
+ const workspaceKey = (workspace) => String(workspace.id || workspace.path)
280
+ const writePolicyFor = (workspace, session) => {
281
+ const policy = getPolicy()
282
+ const resolved =
283
+ policy && typeof policy.resolve === 'function'
284
+ ? policy.resolve(session ? { session } : {})
285
+ : { mode: (policy && policy.defaultMode) || 'workspace-write' }
286
+ return { ...resolved, workspaceRoot: workspace.path }
281
287
  }
282
- const fileName = (wsid) => 'kanban-board-' + wsid + '.json'
283
- const resolveFile = async (wsid) => {
288
+ const resolveFile = async (workspace) => {
284
289
  const fs = getFs()
285
290
  if (!fs) return null
286
291
  try {
287
- return await fs.resolve(fileName(wsid), root() ? { cwd: root() } : {})
292
+ return await fs.resolve(BOARD_FILE, { cwd: workspace.path })
288
293
  } catch (err) {
289
- console.log('dsh-kanban: 解析看板文件失败,退回内存模式:' + ((err && err.message) || err))
294
+ console.log(
295
+ 'dsh-kanban: 解析工作区看板文件失败 ' + workspaceKey(workspace) + ':' + ((err && err.message) || err),
296
+ )
290
297
  return null
291
298
  }
292
299
  }
293
- const targetOf = async (wsid) => {
294
- if (!fileTargets.has(wsid)) fileTargets.set(wsid, await resolveFile(wsid))
295
- return fileTargets.get(wsid)
300
+ const targetOf = async (workspace) => {
301
+ const key = workspaceKey(workspace)
302
+ if (!fileTargets.has(key)) fileTargets.set(key, await resolveFile(workspace))
303
+ return fileTargets.get(key)
304
+ }
305
+ const persistedFlag = (workspace) => {
306
+ const key = workspaceKey(workspace)
307
+ return fileTargets.has(key) && fileTargets.get(key) !== null
296
308
  }
297
- const persistedFlag = (wsid) => fileTargets.has(wsid) && fileTargets.get(wsid) !== null
298
309
 
299
310
  // ---- 看板读写 ----
300
311
 
301
- // 备份原文件为 <文件名>.<suffix>(复制而非移动,保证原文件在写回前始终存在)
302
- const backupFile = async (wsid, suffix) => {
312
+ // 备份原文件为 .dsh-kanban.json.<suffix>(复制而非移动,保证原文件在写回前始终存在)
313
+ const backupFile = async (workspace, suffix, session) => {
303
314
  const fs = getFs()
304
- const target = await targetOf(wsid)
315
+ const target = await targetOf(workspace)
305
316
  if (!fs || !target) return null
317
+ const key = workspaceKey(workspace)
306
318
  try {
307
319
  const text = await fs.readText(target)
308
- const backupTarget = await fs.resolve(fileName(wsid) + '.' + suffix, root() ? { cwd: root() } : {})
309
- await fs.writeText(backupTarget, text)
320
+ const backupTarget = await fs.resolve(BOARD_FILE + '.' + suffix, { cwd: workspace.path })
321
+ await fs.writeText(backupTarget, text, undefined, undefined, writePolicyFor(workspace, session))
310
322
  return backupTarget
311
323
  } catch (err) {
312
- console.log('dsh-kanban: 备份失败 ' + wsid + ' (' + suffix + '):' + ((err && err.message) || err))
324
+ console.log('dsh-kanban: 备份失败 ' + key + ' (' + suffix + '):' + ((err && err.message) || err))
313
325
  return null
314
326
  }
315
327
  }
@@ -326,14 +338,15 @@ export function apply(ctx) {
326
338
  }
327
339
  const timestamp = () => new Date().toISOString().replace(/[:.]/g, '-')
328
340
 
329
- // 首次访问某工作区时从磁盘加载;异常数据一律不阻塞看板可用性
330
- const boardOf = async (wsid) => {
331
- let board = boards.get(wsid)
341
+ // 首次访问某工作区时从该工作区根目录加载;异常数据一律不阻塞看板可用性
342
+ const boardOf = async (workspace, session) => {
343
+ const key = workspaceKey(workspace)
344
+ let board = boards.get(key)
332
345
  if (board) return board
333
346
  board = { schemaVersion: SCHEMA_VERSION, columns: [], labels: [], cards: [], activities: [], warnings: [] }
334
- boards.set(wsid, board)
347
+ boards.set(key, board)
335
348
  const fs = getFs()
336
- const target = await targetOf(wsid)
349
+ const target = await targetOf(workspace)
337
350
  let migrated = false
338
351
  if (fs && target) {
339
352
  try {
@@ -346,19 +359,19 @@ export function apply(ctx) {
346
359
  board.cards = parsed.data.cards
347
360
  board.activities = Array.isArray(parsed.data.activities) ? parsed.data.activities : []
348
361
  migrated = parsed.migrated
349
- // 迁移场景:写回前先把迁移前的原文件备份下来,保证可回滚
362
+ // schema 迁移场景:写回前先把迁移前的原文件备份下来,保证可回滚
350
363
  if (parsed.migrated) {
351
- await backupFile(wsid, 'bak-v' + parsed.fromVersion)
364
+ await backupFile(workspace, 'bak-v' + parsed.fromVersion, session)
352
365
  }
353
366
  } else {
354
367
  // 损坏 / 结构无效 / 版本超前:原文件已无法安全读取,先备份再以空板继续
355
368
  const suffix =
356
369
  parsed.kind === 'unsupported' ? 'unsupported-v' + parsed.version : 'corrupt-' + timestamp()
357
- await backupFile(wsid, suffix)
370
+ await backupFile(workspace, suffix, session)
358
371
  }
359
372
  } catch (err) {
360
373
  // 尚无看板文件(首次使用)或 fs 读取异常,保留默认空板
361
- console.log('dsh-kanban: 读取看板失败 ' + wsid + ':' + ((err && err.message) || err))
374
+ console.log('dsh-kanban: 读取看板失败 ' + key + ':' + ((err && err.message) || err))
362
375
  }
363
376
  }
364
377
  for (const col of board.columns) bumpSeq(col.id)
@@ -370,14 +383,15 @@ export function apply(ctx) {
370
383
  if (board.labels.length === 0) {
371
384
  board.labels = DEFAULT_LABELS.map((l) => ({ ...l }))
372
385
  }
373
- // 升级写回:迁移成功即落盘新版本,保证每个文件只迁移一次
374
- if (migrated && fs && target) await save(wsid)
386
+ // schema 升级写回:迁移成功即落盘新版本,保证每个文件只迁移一次
387
+ if (migrated && fs && target) await save(workspace, session)
375
388
  return board
376
389
  }
377
- const save = async (wsid) => {
390
+ const save = async (workspace, session) => {
378
391
  const fs = getFs()
379
- const target = await targetOf(wsid)
380
- const board = boards.get(wsid)
392
+ const target = await targetOf(workspace)
393
+ const key = workspaceKey(workspace)
394
+ const board = boards.get(key)
381
395
  if (!fs || !target || !board) return
382
396
  try {
383
397
  await fs.writeText(
@@ -389,9 +403,12 @@ export function apply(ctx) {
389
403
  cards: board.cards,
390
404
  activities: Array.isArray(board.activities) ? board.activities : [],
391
405
  }),
406
+ undefined,
407
+ undefined,
408
+ writePolicyFor(workspace, session),
392
409
  )
393
410
  } catch (err) {
394
- console.log('dsh-kanban: 保存失败 ' + wsid + ':' + ((err && err.message) || err))
411
+ console.log('dsh-kanban: 保存失败 ' + key + ':' + ((err && err.message) || err))
395
412
  }
396
413
  }
397
414
 
@@ -449,11 +466,11 @@ export function apply(ctx) {
449
466
  }
450
467
 
451
468
  // ---- 核心数据操作:工具与浏览器 HTTP 共用同一份逻辑 ----
452
- const dispatch = async (wsid, method, args, source) => {
453
- const board = await boardOf(wsid)
469
+ const dispatch = async (workspace, method, args, source, session) => {
470
+ const board = await boardOf(workspace, session)
454
471
  const a = args || {}
455
472
  const actor = source === 'agent' ? 'agent' : 'human'
456
- const persisted = () => persistedFlag(wsid)
473
+ const persisted = () => persistedFlag(workspace)
457
474
  const result = (extra) => ({ board: cloneBoard(board), persisted: persisted(), warnings: takeWarnings(board), ...extra })
458
475
 
459
476
  switch (method) {
@@ -491,7 +508,7 @@ export function apply(ctx) {
491
508
  priority: card.priority ?? null,
492
509
  },
493
510
  })
494
- await save(wsid)
511
+ await save(workspace, session)
495
512
  return result({ message: 'Card added to "' + col.title + '"' })
496
513
  }
497
514
 
@@ -526,7 +543,7 @@ export function apply(ctx) {
526
543
  if (after.priority !== before.priority) {
527
544
  record(board, { cardId: card.id, type: 'card_priority_changed', source: actor, field: 'priority', from: before.priority, to: after.priority })
528
545
  }
529
- await save(wsid)
546
+ await save(workspace, session)
530
547
  }
531
548
  return card
532
549
  ? result({ message: 'Card updated' })
@@ -540,7 +557,7 @@ export function apply(ctx) {
540
557
  if (card) {
541
558
  record(board, { cardId: id, type: 'card_deleted', source: actor, meta: { title: card.title } })
542
559
  }
543
- await save(wsid)
560
+ await save(workspace, session)
544
561
  return result({ message: 'Card deleted' })
545
562
  }
546
563
 
@@ -562,7 +579,7 @@ export function apply(ctx) {
562
579
  if (fromTitle !== target.title) {
563
580
  record(board, { cardId: card.id, type: 'card_moved', source: actor, field: 'columnId', from: fromTitle, to: target.title, meta: { title: card.title } })
564
581
  }
565
- await save(wsid)
582
+ await save(workspace, session)
566
583
  return result({ message: 'Moved to "' + target.title + '"' })
567
584
  }
568
585
 
@@ -570,7 +587,7 @@ export function apply(ctx) {
570
587
  const title = str(a.title, '').slice(0, 40) || 'New list'
571
588
  board.columns.push({ id: nextId('c'), title })
572
589
  record(board, { cardId: null, type: 'column_added', source: actor, meta: { column: title } })
573
- await save(wsid)
590
+ await save(workspace, session)
574
591
  return result({ message: 'List added: "' + title + '"' })
575
592
  }
576
593
 
@@ -582,7 +599,7 @@ export function apply(ctx) {
582
599
  if (col.title !== before) {
583
600
  record(board, { cardId: null, type: 'column_renamed', source: actor, field: 'title', from: before, to: col.title, meta: { column: col.title } })
584
601
  }
585
- await save(wsid)
602
+ await save(workspace, session)
586
603
  }
587
604
  return col ? result({ message: 'List renamed' }) : result({ error: 'List not found' })
588
605
  }
@@ -602,7 +619,7 @@ export function apply(ctx) {
602
619
  }
603
620
  }
604
621
  record(board, { cardId: null, type: 'column_deleted', source: actor, meta: { column: deleted.title } })
605
- await save(wsid)
622
+ await save(workspace, session)
606
623
  return result({ message: 'List deleted, cards moved to "' + board.columns[0].title + '"' })
607
624
  }
608
625
 
@@ -615,7 +632,7 @@ export function apply(ctx) {
615
632
  ? Math.max(0, Math.min(Math.floor(a.toIndex), board.columns.length))
616
633
  : board.columns.length
617
634
  board.columns.splice(toIndex, 0, col)
618
- await save(wsid)
635
+ await save(workspace, session)
619
636
  return result({ message: 'List order updated' })
620
637
  }
621
638
 
@@ -625,7 +642,7 @@ export function apply(ctx) {
625
642
  if (findLabel(board, name)) return result({ error: 'Label already exists' })
626
643
  board.labels.push({ name, color: normColor(a.color) || '#94a3b8' })
627
644
  record(board, { cardId: null, type: 'label_added', source: actor, meta: { label: name } })
628
- await save(wsid)
645
+ await save(workspace, session)
629
646
  return result({ message: 'Label added: "' + name + '"' })
630
647
  }
631
648
 
@@ -652,7 +669,7 @@ export function apply(ctx) {
652
669
  record(board, { cardId: null, type: 'label_color_changed', source: actor, field: 'color', from: beforeColor, to: label.color, meta: { label: label.name } })
653
670
  }
654
671
  }
655
- await save(wsid)
672
+ await save(workspace, session)
656
673
  return result({ message: 'Label updated' })
657
674
  }
658
675
 
@@ -668,7 +685,7 @@ export function apply(ctx) {
668
685
  }
669
686
  }
670
687
  record(board, { cardId: null, type: 'label_deleted', source: actor, meta: { label: name } })
671
- await save(wsid)
688
+ await save(workspace, session)
672
689
  return result({ message: 'Label deleted' })
673
690
  }
674
691
 
@@ -677,27 +694,42 @@ export function apply(ctx) {
677
694
  }
678
695
  }
679
696
 
680
- // ---- 工具执行上下文 -> 工作区 id ----
681
- const wsidOfExec = async (exec) => {
697
+ // ---- 工具执行上下文 / 浏览器 workspaceId -> 工作区 ----
698
+ const workspaceOfExec = async (exec) => {
682
699
  const agent = exec && exec.agent
683
- const cwd = agent && agent.session && agent.session.header && agent.session.header.cwd
684
- if (typeof cwd === 'string' && cwd) {
685
- const registry = getWorkspaceRegistry()
686
- if (registry) {
687
- try {
688
- const ws = await registry.resolveByPath(cwd)
689
- if (ws) return ws.id
690
- } catch (err) {
691
- console.log('dsh-kanban: 解析工作区失败:' + ((err && err.message) || err))
692
- }
700
+ const session = agent && agent.session
701
+ const cwd = session && session.header && session.header.cwd
702
+ if (typeof cwd !== 'string' || !cwd) return null
703
+ const registry = getWorkspaceRegistry()
704
+ if (registry) {
705
+ try {
706
+ const workspace = await registry.resolveByPath(cwd)
707
+ if (workspace) return workspace
708
+ } catch (err) {
709
+ console.log('dsh-kanban: 解析工作区失败:' + ((err && err.message) || err))
693
710
  }
694
711
  }
695
- return 'default'
712
+ // 未登记到 WorkspaceRegistry 的会话仍以自身 cwd 作为工作区根目录。
713
+ return { id: 'cwd:' + cwd, path: cwd, title: cwd }
714
+ }
715
+ const workspaceOfId = (id) => {
716
+ const registry = getWorkspaceRegistry()
717
+ return registry && typeof registry.get === 'function' ? registry.get(id) : undefined
696
718
  }
719
+ const emptyBoardSummary = { columns: [], labels: [], cards: [] }
697
720
 
698
721
  const runTool = async (method, args, exec) => {
699
- const wsid = await wsidOfExec(exec)
700
- const r = await dispatch(wsid, method, args, 'agent')
722
+ const workspace = await workspaceOfExec(exec)
723
+ if (!workspace) {
724
+ return {
725
+ ok: false,
726
+ message: 'Cannot determine the current workspace from the tool execution context',
727
+ board: emptyBoardSummary,
728
+ warnings: [],
729
+ }
730
+ }
731
+ const session = exec && exec.agent && exec.agent.session
732
+ const r = await dispatch(workspace, method, args, 'agent', session)
701
733
  return {
702
734
  ok: !r.error,
703
735
  message: r.error || r.message || 'Done',
@@ -715,8 +747,14 @@ export function apply(ctx) {
715
747
  const body = raw ? JSON.parse(raw) : {}
716
748
  const method = typeof body.method === 'string' ? body.method : 'get'
717
749
  const args = body.args || {}
718
- const wsid = typeof args.workspaceId === 'string' && args.workspaceId ? args.workspaceId : 'default'
719
- const result = await dispatch(wsid, method, args, 'human')
750
+ const workspaceId = typeof args.workspaceId === 'string' ? args.workspaceId : ''
751
+ const workspace = workspaceId ? workspaceOfId(workspaceId) : undefined
752
+ if (!workspace) {
753
+ res.writeHead(400, { 'content-type': 'application/json' })
754
+ res.end(JSON.stringify({ error: 'Unknown workspace: ' + (workspaceId || '(missing)') }))
755
+ return
756
+ }
757
+ const result = await dispatch(workspace, method, args, 'human')
720
758
  res.writeHead(200, { 'content-type': 'application/json' })
721
759
  res.end(JSON.stringify(result))
722
760
  } catch (err) {
@@ -753,61 +791,63 @@ export function apply(ctx) {
753
791
  }
754
792
  }
755
793
 
756
- // ---- 启动校验:全量体检看板文件(只读 + 备份损坏文件,不迁移、不加载内存)----
794
+ // ---- 启动校验:逐个工作区体检 .dsh-kanban.json(只读 + 备份损坏文件)----
757
795
  const startupState = { done: false }
758
796
  const maybeStartupCheck = () => {
759
797
  if (startupState.done) return
760
- if (!getFs()) return
798
+ const registry = getWorkspaceRegistry()
799
+ if (!getFs() || !registry || typeof registry.list !== 'function') return
761
800
  startupState.done = true
762
801
  runStartupCheck()
763
802
  }
764
803
  const runStartupCheck = async () => {
765
804
  const fs = getFs()
766
- const wsRoot = root()
767
- if (!fs || !wsRoot) return
805
+ const registry = getWorkspaceRegistry()
806
+ if (!fs || !registry || typeof registry.list !== 'function') return
768
807
  try {
769
- const dir = await fs.resolve('.', { cwd: wsRoot })
770
- const entries = await fs.listDir(dir)
771
- const boardFiles = entries.filter(
772
- (e) => typeof e.name === 'string' && /^kanban-board-.+\.json$/.test(e.name),
773
- )
774
- if (boardFiles.length === 0) {
775
- console.log('dsh-kanban: 启动校验完成,未发现看板数据文件')
776
- return
777
- }
808
+ const workspaces = registry.list()
809
+ let found = 0
778
810
  let corrupt = 0
779
811
  let pendingUpgrade = 0
780
812
  let unsupported = 0
781
813
  let ok = 0
782
- for (const entry of boardFiles) {
783
- const wsid = entry.name.slice('kanban-board-'.length, -'.json'.length)
814
+ for (const workspace of workspaces) {
815
+ const key = workspaceKey(workspace)
784
816
  try {
785
- const text = await fs.readText(entry.target)
817
+ const target = await fs.resolve(BOARD_FILE, { cwd: workspace.path })
818
+ const text = await fs.readText(target)
819
+ found++
786
820
  const parsed = parseBoardText(text)
787
821
  if (parsed.ok) {
788
822
  if (parsed.migrated) {
789
823
  pendingUpgrade++
790
- console.log('dsh-kanban: 启动校验 ' + wsid + ':schemaVersion ' + parsed.fromVersion + ',首次打开时将自动升级')
824
+ console.log('dsh-kanban: 启动校验 ' + key + ':schemaVersion ' + parsed.fromVersion + ',首次打开时将自动升级')
791
825
  } else {
792
826
  ok++
793
827
  }
794
828
  } else if (parsed.kind === 'unsupported') {
795
829
  unsupported++
796
- console.log('dsh-kanban: 启动校验 ' + wsid + ':文件由更新版本插件写入(schemaVersion ' + parsed.version + ')')
830
+ console.log('dsh-kanban: 启动校验 ' + key + ':文件由更新版本插件写入(schemaVersion ' + parsed.version + ')')
797
831
  } else {
798
832
  corrupt++
799
833
  const suffix = 'corrupt-' + timestamp()
800
- const backupTarget = await fs.resolve(entry.name + '.' + suffix, { cwd: wsRoot })
801
- await fs.writeText(backupTarget, text)
802
- console.log('dsh-kanban: 启动校验 ' + wsid + ':数据文件损坏(' + parsed.kind + '),已备份为 ' + entry.name + '.' + suffix)
834
+ const backupTarget = await fs.resolve(BOARD_FILE + '.' + suffix, { cwd: workspace.path })
835
+ await fs.writeText(backupTarget, text, undefined, undefined, writePolicyFor(workspace))
836
+ console.log('dsh-kanban: 启动校验 ' + key + ':数据文件损坏(' + parsed.kind + '),已备份为 ' + BOARD_FILE + '.' + suffix)
803
837
  }
804
838
  } catch (err) {
805
- console.log('dsh-kanban: 启动校验 ' + wsid + ':检查失败 ' + ((err && err.message) || err))
839
+ if (!err || (err.code !== 'ENOENT' && err.message !== 'ENOENT')) {
840
+ console.log('dsh-kanban: 启动校验 ' + key + ':检查失败 ' + ((err && err.message) || err))
841
+ }
806
842
  }
807
843
  }
844
+ if (found === 0) {
845
+ console.log('dsh-kanban: 启动校验完成,未发现看板数据文件')
846
+ return
847
+ }
808
848
  console.log(
809
849
  'dsh-kanban: 启动校验完成:共 ' +
810
- boardFiles.length +
850
+ found +
811
851
  ' 个看板文件,正常 ' +
812
852
  ok +
813
853
  ',损坏并已备份 ' +
@@ -914,8 +954,10 @@ export function apply(ctx) {
914
954
  },
915
955
  },
916
956
  async execute(args, exec) {
917
- const wsid = await wsidOfExec(exec)
918
- const r = await dispatch(wsid, 'getCard', args, 'agent')
957
+ const workspace = await workspaceOfExec(exec)
958
+ if (!workspace) return { ok: false, message: 'Cannot determine the current workspace from the tool execution context', warnings: [] }
959
+ const session = exec && exec.agent && exec.agent.session
960
+ const r = await dispatch(workspace, 'getCard', args, 'agent', session)
919
961
  if (r.error) return { ok: false, message: r.error, warnings: Array.isArray(r.warnings) ? r.warnings : [] }
920
962
  return { ok: true, message: 'Card ' + String(args.id || '') + ' details', card: r.card, warnings: Array.isArray(r.warnings) ? r.warnings : [] }
921
963
  },
@@ -1122,12 +1164,14 @@ export function apply(ctx) {
1122
1164
  },
1123
1165
  },
1124
1166
  async execute(args, exec) {
1125
- const wsid = await wsidOfExec(exec)
1126
- const r = await dispatch(wsid, 'get', args, 'agent')
1167
+ const workspace = await workspaceOfExec(exec)
1168
+ if (!workspace) return { ok: false, message: 'Cannot determine the current workspace from the tool execution context', labels: [], warnings: [] }
1169
+ const session = exec && exec.agent && exec.agent.session
1170
+ const r = await dispatch(workspace, 'get', args, 'agent', session)
1127
1171
  const labels = (r.board && r.board.labels) || []
1128
1172
  return {
1129
1173
  ok: true,
1130
- message: 'Labels (workspace ' + wsid + ')',
1174
+ message: 'Labels (workspace ' + workspaceKey(workspace) + ')',
1131
1175
  labels,
1132
1176
  warnings: Array.isArray(r.warnings) ? r.warnings : [],
1133
1177
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alpacachen/dsh-kanban",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "A kanban board plugin for DeepSeek Harness: a 'Board' tab in conversations plus 14 kanban_* AI tools, per-workspace isolation and disk persistence. Built with React, TypeScript, dnd-kit, shadcn/ui and Tailwind CSS.",
5
5
  "type": "module",
6
6
  "main": "index.js",