c0de-agent 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core/loop.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { chatStream as llmChatStream } from '../llm/provider.js';
2
+ import { createKanbanStore } from '../kanban/index.js';
2
3
  import { isContextOverflowFailure } from '../llm/provider-error.js';
3
4
  import { resolveRoute } from '../llm/registry.js';
4
5
  import { isLLMError } from '../llm/schema/errors.js';
@@ -607,6 +608,11 @@ async function* persistAssistantAndTools(state, deps, collectedText, validCalls)
607
608
  state.todoPhases = phases;
608
609
  },
609
610
  },
611
+ // kanban 工具通过 dependency-reversal 注入:per-project 的 db-backed store。
612
+ // 仅当 session 有 projectId 时启用(子 session 无 project 时不可用)。
613
+ ...(state.session.projectId
614
+ ? { kanbanStore: createKanbanStore(deps.db, state.session.projectId) }
615
+ : {}),
610
616
  }, validCalls, deps.hookRunner);
611
617
  const toolLatency = Date.now() - toolExecStart;
612
618
  const metricsEnabled = deps.config.toolMetrics.enabled;
@@ -950,6 +950,301 @@ export declare const toolMetrics: import("drizzle-orm/pg-core").PgTableWithColum
950
950
  };
951
951
  dialect: "pg";
952
952
  }>;
953
+ /**
954
+ * Kanban boards — one per project (unique projectId). Stores column/label
955
+ * configuration as JSON; cards live in kanban_cards.
956
+ */
957
+ export declare const kanbanBoards: import("drizzle-orm/pg-core").PgTableWithColumns<{
958
+ name: "kanban_boards";
959
+ schema: undefined;
960
+ columns: {
961
+ id: import("drizzle-orm/pg-core").PgColumn<{
962
+ name: "id";
963
+ tableName: "kanban_boards";
964
+ dataType: "string";
965
+ columnType: "PgUUID";
966
+ data: string;
967
+ driverParam: string;
968
+ notNull: true;
969
+ hasDefault: true;
970
+ isPrimaryKey: true;
971
+ isAutoincrement: false;
972
+ hasRuntimeDefault: false;
973
+ enumValues: undefined;
974
+ baseColumn: never;
975
+ identity: undefined;
976
+ generated: undefined;
977
+ }, {}, {}>;
978
+ projectId: import("drizzle-orm/pg-core").PgColumn<{
979
+ name: "project_id";
980
+ tableName: "kanban_boards";
981
+ dataType: "string";
982
+ columnType: "PgText";
983
+ data: string;
984
+ driverParam: string;
985
+ notNull: true;
986
+ hasDefault: false;
987
+ isPrimaryKey: false;
988
+ isAutoincrement: false;
989
+ hasRuntimeDefault: false;
990
+ enumValues: [string, ...string[]];
991
+ baseColumn: never;
992
+ identity: undefined;
993
+ generated: undefined;
994
+ }, {}, {}>;
995
+ columns: import("drizzle-orm/pg-core").PgColumn<{
996
+ name: "columns";
997
+ tableName: "kanban_boards";
998
+ dataType: "json";
999
+ columnType: "PgJsonb";
1000
+ data: unknown;
1001
+ driverParam: unknown;
1002
+ notNull: true;
1003
+ hasDefault: false;
1004
+ isPrimaryKey: false;
1005
+ isAutoincrement: false;
1006
+ hasRuntimeDefault: false;
1007
+ enumValues: undefined;
1008
+ baseColumn: never;
1009
+ identity: undefined;
1010
+ generated: undefined;
1011
+ }, {}, {}>;
1012
+ labels: import("drizzle-orm/pg-core").PgColumn<{
1013
+ name: "labels";
1014
+ tableName: "kanban_boards";
1015
+ dataType: "json";
1016
+ columnType: "PgJsonb";
1017
+ data: unknown;
1018
+ driverParam: unknown;
1019
+ notNull: true;
1020
+ hasDefault: true;
1021
+ isPrimaryKey: false;
1022
+ isAutoincrement: false;
1023
+ hasRuntimeDefault: false;
1024
+ enumValues: undefined;
1025
+ baseColumn: never;
1026
+ identity: undefined;
1027
+ generated: undefined;
1028
+ }, {}, {}>;
1029
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
1030
+ name: "created_at";
1031
+ tableName: "kanban_boards";
1032
+ dataType: "date";
1033
+ columnType: "PgTimestamp";
1034
+ data: Date;
1035
+ driverParam: string;
1036
+ notNull: true;
1037
+ hasDefault: true;
1038
+ isPrimaryKey: false;
1039
+ isAutoincrement: false;
1040
+ hasRuntimeDefault: false;
1041
+ enumValues: undefined;
1042
+ baseColumn: never;
1043
+ identity: undefined;
1044
+ generated: undefined;
1045
+ }, {}, {}>;
1046
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
1047
+ name: "updated_at";
1048
+ tableName: "kanban_boards";
1049
+ dataType: "date";
1050
+ columnType: "PgTimestamp";
1051
+ data: Date;
1052
+ driverParam: string;
1053
+ notNull: true;
1054
+ hasDefault: true;
1055
+ isPrimaryKey: false;
1056
+ isAutoincrement: false;
1057
+ hasRuntimeDefault: false;
1058
+ enumValues: undefined;
1059
+ baseColumn: never;
1060
+ identity: undefined;
1061
+ generated: undefined;
1062
+ }, {}, {}>;
1063
+ };
1064
+ dialect: "pg";
1065
+ }>;
1066
+ /**
1067
+ * Kanban cards — tasks on a board. position is a real for fractional indexing
1068
+ * (insert between two cards without rewriting all positions). columnId
1069
+ * references a column id in the parent board's columns JSON.
1070
+ */
1071
+ export declare const kanbanCards: import("drizzle-orm/pg-core").PgTableWithColumns<{
1072
+ name: "kanban_cards";
1073
+ schema: undefined;
1074
+ columns: {
1075
+ id: import("drizzle-orm/pg-core").PgColumn<{
1076
+ name: "id";
1077
+ tableName: "kanban_cards";
1078
+ dataType: "string";
1079
+ columnType: "PgUUID";
1080
+ data: string;
1081
+ driverParam: string;
1082
+ notNull: true;
1083
+ hasDefault: true;
1084
+ isPrimaryKey: true;
1085
+ isAutoincrement: false;
1086
+ hasRuntimeDefault: false;
1087
+ enumValues: undefined;
1088
+ baseColumn: never;
1089
+ identity: undefined;
1090
+ generated: undefined;
1091
+ }, {}, {}>;
1092
+ boardId: import("drizzle-orm/pg-core").PgColumn<{
1093
+ name: "board_id";
1094
+ tableName: "kanban_cards";
1095
+ dataType: "string";
1096
+ columnType: "PgUUID";
1097
+ data: string;
1098
+ driverParam: string;
1099
+ notNull: true;
1100
+ hasDefault: false;
1101
+ isPrimaryKey: false;
1102
+ isAutoincrement: false;
1103
+ hasRuntimeDefault: false;
1104
+ enumValues: undefined;
1105
+ baseColumn: never;
1106
+ identity: undefined;
1107
+ generated: undefined;
1108
+ }, {}, {}>;
1109
+ title: import("drizzle-orm/pg-core").PgColumn<{
1110
+ name: "title";
1111
+ tableName: "kanban_cards";
1112
+ dataType: "string";
1113
+ columnType: "PgText";
1114
+ data: string;
1115
+ driverParam: string;
1116
+ notNull: true;
1117
+ hasDefault: false;
1118
+ isPrimaryKey: false;
1119
+ isAutoincrement: false;
1120
+ hasRuntimeDefault: false;
1121
+ enumValues: [string, ...string[]];
1122
+ baseColumn: never;
1123
+ identity: undefined;
1124
+ generated: undefined;
1125
+ }, {}, {}>;
1126
+ description: import("drizzle-orm/pg-core").PgColumn<{
1127
+ name: "description";
1128
+ tableName: "kanban_cards";
1129
+ dataType: "string";
1130
+ columnType: "PgText";
1131
+ data: string;
1132
+ driverParam: string;
1133
+ notNull: false;
1134
+ hasDefault: false;
1135
+ isPrimaryKey: false;
1136
+ isAutoincrement: false;
1137
+ hasRuntimeDefault: false;
1138
+ enumValues: [string, ...string[]];
1139
+ baseColumn: never;
1140
+ identity: undefined;
1141
+ generated: undefined;
1142
+ }, {}, {}>;
1143
+ columnId: import("drizzle-orm/pg-core").PgColumn<{
1144
+ name: "column_id";
1145
+ tableName: "kanban_cards";
1146
+ dataType: "string";
1147
+ columnType: "PgText";
1148
+ data: string;
1149
+ driverParam: string;
1150
+ notNull: true;
1151
+ hasDefault: false;
1152
+ isPrimaryKey: false;
1153
+ isAutoincrement: false;
1154
+ hasRuntimeDefault: false;
1155
+ enumValues: [string, ...string[]];
1156
+ baseColumn: never;
1157
+ identity: undefined;
1158
+ generated: undefined;
1159
+ }, {}, {}>;
1160
+ priority: import("drizzle-orm/pg-core").PgColumn<{
1161
+ name: "priority";
1162
+ tableName: "kanban_cards";
1163
+ dataType: "string";
1164
+ columnType: "PgText";
1165
+ data: string;
1166
+ driverParam: string;
1167
+ notNull: true;
1168
+ hasDefault: true;
1169
+ isPrimaryKey: false;
1170
+ isAutoincrement: false;
1171
+ hasRuntimeDefault: false;
1172
+ enumValues: [string, ...string[]];
1173
+ baseColumn: never;
1174
+ identity: undefined;
1175
+ generated: undefined;
1176
+ }, {}, {}>;
1177
+ position: import("drizzle-orm/pg-core").PgColumn<{
1178
+ name: "position";
1179
+ tableName: "kanban_cards";
1180
+ dataType: "number";
1181
+ columnType: "PgReal";
1182
+ data: number;
1183
+ driverParam: string | number;
1184
+ notNull: true;
1185
+ hasDefault: true;
1186
+ isPrimaryKey: false;
1187
+ isAutoincrement: false;
1188
+ hasRuntimeDefault: false;
1189
+ enumValues: undefined;
1190
+ baseColumn: never;
1191
+ identity: undefined;
1192
+ generated: undefined;
1193
+ }, {}, {}>;
1194
+ labels: import("drizzle-orm/pg-core").PgColumn<{
1195
+ name: "labels";
1196
+ tableName: "kanban_cards";
1197
+ dataType: "json";
1198
+ columnType: "PgJsonb";
1199
+ data: unknown;
1200
+ driverParam: unknown;
1201
+ notNull: true;
1202
+ hasDefault: true;
1203
+ isPrimaryKey: false;
1204
+ isAutoincrement: false;
1205
+ hasRuntimeDefault: false;
1206
+ enumValues: undefined;
1207
+ baseColumn: never;
1208
+ identity: undefined;
1209
+ generated: undefined;
1210
+ }, {}, {}>;
1211
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
1212
+ name: "created_at";
1213
+ tableName: "kanban_cards";
1214
+ dataType: "date";
1215
+ columnType: "PgTimestamp";
1216
+ data: Date;
1217
+ driverParam: string;
1218
+ notNull: true;
1219
+ hasDefault: true;
1220
+ isPrimaryKey: false;
1221
+ isAutoincrement: false;
1222
+ hasRuntimeDefault: false;
1223
+ enumValues: undefined;
1224
+ baseColumn: never;
1225
+ identity: undefined;
1226
+ generated: undefined;
1227
+ }, {}, {}>;
1228
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
1229
+ name: "updated_at";
1230
+ tableName: "kanban_cards";
1231
+ dataType: "date";
1232
+ columnType: "PgTimestamp";
1233
+ data: Date;
1234
+ driverParam: string;
1235
+ notNull: true;
1236
+ hasDefault: true;
1237
+ isPrimaryKey: false;
1238
+ isAutoincrement: false;
1239
+ hasRuntimeDefault: false;
1240
+ enumValues: undefined;
1241
+ baseColumn: never;
1242
+ identity: undefined;
1243
+ generated: undefined;
1244
+ }, {}, {}>;
1245
+ };
1246
+ dialect: "pg";
1247
+ }>;
953
1248
  /** Type exports for insert/select operations. */
954
1249
  export type ProjectRow = typeof projects.$inferSelect;
955
1250
  export type ProjectInsert = typeof projects.$inferInsert;
@@ -963,3 +1258,7 @@ export type FileSnapshotRow = typeof fileSnapshots.$inferSelect;
963
1258
  export type FileSnapshotInsert = typeof fileSnapshots.$inferInsert;
964
1259
  export type ToolMetricRow = typeof toolMetrics.$inferSelect;
965
1260
  export type ToolMetricInsert = typeof toolMetrics.$inferInsert;
1261
+ export type KanbanBoardRow = typeof kanbanBoards.$inferSelect;
1262
+ export type KanbanBoardInsert = typeof kanbanBoards.$inferInsert;
1263
+ export type KanbanCardRow = typeof kanbanCards.$inferSelect;
1264
+ export type KanbanCardInsert = typeof kanbanCards.$inferInsert;
package/dist/db/schema.js CHANGED
@@ -110,3 +110,39 @@ export const toolMetrics = pgTable('tool_metrics', {
110
110
  }, (table) => [
111
111
  uniqueIndex('uq_tool_metrics_model_tool_mode').on(table.model, table.tool, table.mode),
112
112
  ]);
113
+ /**
114
+ * Kanban boards — one per project (unique projectId). Stores column/label
115
+ * configuration as JSON; cards live in kanban_cards.
116
+ */
117
+ export const kanbanBoards = pgTable('kanban_boards', {
118
+ id: uuid('id').primaryKey().defaultRandom(),
119
+ projectId: text('project_id')
120
+ .notNull()
121
+ .references(() => projects.id, { onDelete: 'cascade' }),
122
+ /** Column definitions: [{ id, name }] */
123
+ columns: jsonb('columns').notNull(),
124
+ /** Label definitions: [{ id, name, color }] */
125
+ labels: jsonb('labels').notNull().default([]),
126
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
127
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
128
+ }, (table) => [uniqueIndex('uq_kanban_boards_project').on(table.projectId)]);
129
+ /**
130
+ * Kanban cards — tasks on a board. position is a real for fractional indexing
131
+ * (insert between two cards without rewriting all positions). columnId
132
+ * references a column id in the parent board's columns JSON.
133
+ */
134
+ export const kanbanCards = pgTable('kanban_cards', {
135
+ id: uuid('id').primaryKey().defaultRandom(),
136
+ boardId: uuid('board_id')
137
+ .notNull()
138
+ .references(() => kanbanBoards.id, { onDelete: 'cascade' }),
139
+ title: text('title').notNull(),
140
+ description: text('description'),
141
+ columnId: text('column_id').notNull(),
142
+ priority: text('priority').notNull().default('medium'),
143
+ position: real('position').notNull().default(0),
144
+ /** Label ids referencing board.labels[].id */
145
+ labels: jsonb('labels').notNull().default([]),
146
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
147
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
148
+ }, (table) => [index('idx_kanban_cards_board').on(table.boardId, table.columnId, table.position)]);
@@ -0,0 +1 @@
1
+ export { createKanbanStore } from './store.js';
@@ -0,0 +1 @@
1
+ export { createKanbanStore } from './store.js';
@@ -0,0 +1,8 @@
1
+ import type { DB } from '../db/client.js';
2
+ import type { KanbanStore } from '../shared/types/kanban.js';
3
+ /**
4
+ * Create a project-scoped KanbanStore backed by the given db handle.
5
+ * The board is created on first access (lazy) with the default 5 columns.
6
+ */
7
+ declare function createKanbanStore(handle: DB, projectId: string): KanbanStore;
8
+ export { createKanbanStore };
@@ -0,0 +1,158 @@
1
+ import { and, asc, eq, max } from 'drizzle-orm';
2
+ import { kanbanBoards, kanbanCards } from '../db/schema.js';
3
+ import { DEFAULT_KANBAN_COLUMNS } from '../shared/types/kanban.js';
4
+ /** Default column when none is specified. */
5
+ const DEFAULT_COLUMN_ID = 'todo';
6
+ /** Position increment — large gap avoids frequent re-indexing on reorder. */
7
+ const POSITION_GAP = 1000;
8
+ // ── Row → API mappers ──────────────────────────────────────
9
+ function rowToBoard(row) {
10
+ return {
11
+ id: row.id,
12
+ projectId: row.projectId,
13
+ columns: row.columns,
14
+ labels: row.labels,
15
+ createdAt: row.createdAt.toISOString(),
16
+ updatedAt: row.updatedAt.toISOString(),
17
+ };
18
+ }
19
+ function rowToCard(row) {
20
+ return {
21
+ id: row.id,
22
+ boardId: row.boardId,
23
+ title: row.title,
24
+ description: row.description,
25
+ columnId: row.columnId,
26
+ priority: row.priority,
27
+ position: row.position,
28
+ labels: row.labels,
29
+ createdAt: row.createdAt.toISOString(),
30
+ updatedAt: row.updatedAt.toISOString(),
31
+ };
32
+ }
33
+ // ── Factory ────────────────────────────────────────────────
34
+ /**
35
+ * Create a project-scoped KanbanStore backed by the given db handle.
36
+ * The board is created on first access (lazy) with the default 5 columns.
37
+ */
38
+ function createKanbanStore(handle, projectId) {
39
+ const db = handle.db;
40
+ /** Insert a default board if none exists (idempotent via unique projectId). */
41
+ async function getOrCreateBoardId() {
42
+ await db
43
+ .insert(kanbanBoards)
44
+ .values({
45
+ projectId,
46
+ columns: [...DEFAULT_KANBAN_COLUMNS],
47
+ labels: [],
48
+ })
49
+ .onConflictDoNothing({ target: kanbanBoards.projectId });
50
+ const [row] = await db
51
+ .select({ id: kanbanBoards.id })
52
+ .from(kanbanBoards)
53
+ .where(eq(kanbanBoards.projectId, projectId))
54
+ .limit(1);
55
+ // 行一定存在:上面 insert + onConflictDoNothing 保证了 projectId 对应的行已创建
56
+ return row.id;
57
+ }
58
+ /** Max position in a column (0 if empty). */
59
+ async function maxPos(boardId, columnId) {
60
+ const [row] = await db
61
+ .select({ m: max(kanbanCards.position) })
62
+ .from(kanbanCards)
63
+ .where(and(eq(kanbanCards.boardId, boardId), eq(kanbanCards.columnId, columnId)));
64
+ return row?.m ?? 0;
65
+ }
66
+ return {
67
+ async getBoard() {
68
+ const boardId = await getOrCreateBoardId();
69
+ const [boardRow] = await db
70
+ .select()
71
+ .from(kanbanBoards)
72
+ .where(eq(kanbanBoards.id, boardId))
73
+ .limit(1);
74
+ const board = boardRow;
75
+ const cards = await db
76
+ .select()
77
+ .from(kanbanCards)
78
+ .where(eq(kanbanCards.boardId, boardId))
79
+ .orderBy(asc(kanbanCards.columnId), asc(kanbanCards.position));
80
+ return { ...rowToBoard(board), cards: cards.map(rowToCard) };
81
+ },
82
+ async addCard(input) {
83
+ const boardId = await getOrCreateBoardId();
84
+ const columnId = input.columnId ?? DEFAULT_COLUMN_ID;
85
+ const position = (await maxPos(boardId, columnId)) + POSITION_GAP;
86
+ const [cardRow] = await db
87
+ .insert(kanbanCards)
88
+ .values({
89
+ boardId,
90
+ title: input.title,
91
+ description: input.description ?? null,
92
+ columnId,
93
+ priority: input.priority ?? 'medium',
94
+ position,
95
+ labels: input.labels ?? [],
96
+ })
97
+ .returning();
98
+ const row = cardRow;
99
+ return rowToCard(row);
100
+ },
101
+ async updateCard(id, patch) {
102
+ const [row] = await db
103
+ .update(kanbanCards)
104
+ .set({
105
+ ...(patch.title !== undefined && { title: patch.title }),
106
+ ...(patch.description !== undefined && { description: patch.description }),
107
+ ...(patch.priority !== undefined && { priority: patch.priority }),
108
+ ...(patch.labels !== undefined && { labels: patch.labels }),
109
+ updatedAt: new Date(),
110
+ })
111
+ .where(eq(kanbanCards.id, id))
112
+ .returning();
113
+ if (!row)
114
+ throw new Error(`Kanban card not found: ${id}`);
115
+ return rowToCard(row);
116
+ },
117
+ async moveCard(id, columnId, position) {
118
+ // If no explicit position, append to end of target column.
119
+ let newPos = position;
120
+ if (newPos === undefined) {
121
+ const [card] = await db
122
+ .select({ boardId: kanbanCards.boardId })
123
+ .from(kanbanCards)
124
+ .where(eq(kanbanCards.id, id))
125
+ .limit(1);
126
+ if (!card)
127
+ throw new Error(`Kanban card not found: ${id}`);
128
+ newPos = (await maxPos(card.boardId, columnId)) + POSITION_GAP;
129
+ }
130
+ const [row] = await db
131
+ .update(kanbanCards)
132
+ .set({ columnId, position: newPos, updatedAt: new Date() })
133
+ .where(eq(kanbanCards.id, id))
134
+ .returning();
135
+ if (!row)
136
+ throw new Error(`Kanban card not found: ${id}`);
137
+ return rowToCard(row);
138
+ },
139
+ async deleteCard(id) {
140
+ await db.delete(kanbanCards).where(eq(kanbanCards.id, id));
141
+ },
142
+ async updateBoard(patch) {
143
+ await getOrCreateBoardId();
144
+ const [boardRow] = await db
145
+ .update(kanbanBoards)
146
+ .set({
147
+ ...(patch.columns !== undefined && { columns: patch.columns }),
148
+ ...(patch.labels !== undefined && { labels: patch.labels }),
149
+ updatedAt: new Date(),
150
+ })
151
+ .where(eq(kanbanBoards.projectId, projectId))
152
+ .returning();
153
+ const row = boardRow;
154
+ return rowToBoard(row);
155
+ },
156
+ };
157
+ }
158
+ export { createKanbanStore };
@@ -14,6 +14,7 @@ import { createConfigRoute } from './routes/config.js';
14
14
  import { createFilesRoute } from './routes/files.js';
15
15
  import { createFilesystemRoute } from './routes/filesystem.js';
16
16
  import { createHealthRoute } from './routes/health.js';
17
+ import { createKanbanRoute } from './routes/kanban.js';
17
18
  import { createPermissionsRoute } from './routes/permissions.js';
18
19
  import { createProjectRoute } from './routes/project.js';
19
20
  import { createProviderRoute } from './routes/provider.js';
@@ -44,6 +45,7 @@ function createApp(ctx) {
44
45
  app.route('/api/commands', createCommandsRoute(ctx));
45
46
  app.route('/api/tools', createToolRoute(ctx));
46
47
  app.route('/api/todo', createTodoRoute(ctx));
48
+ app.route('/api/kanban', createKanbanRoute(ctx));
47
49
  app.route('/api/update', createUpdateRoute(ctx));
48
50
  app.route('/api/config', createConfigRoute(ctx));
49
51
  app.route('/api/permissions', createPermissionsRoute(ctx));
@@ -66,6 +68,7 @@ function createApp(ctx) {
66
68
  '/api/commands',
67
69
  '/api/tools',
68
70
  '/api/todo',
71
+ '/api/kanban',
69
72
  '/api/update',
70
73
  '/api/config',
71
74
  '/api/permissions',
@@ -0,0 +1,4 @@
1
+ import { Hono } from 'hono';
2
+ import type { ServerContext } from '../types.js';
3
+ declare function createKanbanRoute(ctx: ServerContext): Hono;
4
+ export { createKanbanRoute };
@@ -0,0 +1,74 @@
1
+ // REST routes for the kanban board — frontend UI uses these for drag-and-drop
2
+ // card operations, board config, and initial load.
3
+ import { Hono } from 'hono';
4
+ import { createKanbanStore } from '../../kanban/index.js';
5
+ import { apiError } from '../middleware/error.js';
6
+ function createKanbanRoute(ctx) {
7
+ const app = new Hono();
8
+ // GET /:projectId — full board with cards
9
+ app.get('/:projectId', async (c) => {
10
+ const projectId = c.req.param('projectId');
11
+ const store = createKanbanStore(ctx.db, projectId);
12
+ const board = await store.getBoard();
13
+ return c.json(board);
14
+ });
15
+ // PATCH /:projectId — update board columns/labels config
16
+ app.patch('/:projectId', async (c) => {
17
+ const projectId = c.req.param('projectId');
18
+ const body = await c.req.json().catch(() => ({}));
19
+ const store = createKanbanStore(ctx.db, projectId);
20
+ const board = await store.updateBoard({
21
+ ...(body.columns !== undefined && { columns: body.columns }),
22
+ ...(body.labels !== undefined && { labels: body.labels }),
23
+ });
24
+ return c.json(board);
25
+ });
26
+ // POST /:projectId/cards — add a card
27
+ app.post('/:projectId/cards', async (c) => {
28
+ const projectId = c.req.param('projectId');
29
+ const body = await c.req.json().catch(() => ({}));
30
+ const title = body.title?.trim();
31
+ if (!title)
32
+ return apiError(c, 400, 'INVALID_INPUT', 'title is required');
33
+ const store = createKanbanStore(ctx.db, projectId);
34
+ const card = await store.addCard({
35
+ title,
36
+ description: body.description ?? null,
37
+ columnId: body.columnId,
38
+ priority: body.priority,
39
+ labels: body.labels,
40
+ });
41
+ return c.json(card, 201);
42
+ });
43
+ // PATCH /:projectId/cards/:cardId — update card fields or move
44
+ app.patch('/:projectId/cards/:cardId', async (c) => {
45
+ const projectId = c.req.param('projectId');
46
+ const cardId = c.req.param('cardId');
47
+ const body = await c.req.json().catch(() => ({}));
48
+ const store = createKanbanStore(ctx.db, projectId);
49
+ // columnId + position → moveCard; otherwise field update.
50
+ if (body.columnId !== undefined) {
51
+ const card = await store.moveCard(cardId, body.columnId, body.position);
52
+ return c.json(card);
53
+ }
54
+ const card = await store.updateCard(cardId, {
55
+ ...(body.title !== undefined && { title: body.title }),
56
+ ...(body.description !== undefined && {
57
+ description: body.description,
58
+ }),
59
+ ...(body.priority !== undefined && { priority: body.priority }),
60
+ ...(body.labels !== undefined && { labels: body.labels }),
61
+ });
62
+ return c.json(card);
63
+ });
64
+ // DELETE /:projectId/cards/:cardId — delete a card
65
+ app.delete('/:projectId/cards/:cardId', async (c) => {
66
+ const projectId = c.req.param('projectId');
67
+ const cardId = c.req.param('cardId');
68
+ const store = createKanbanStore(ctx.db, projectId);
69
+ await store.deleteCard(cardId);
70
+ return c.json({ ok: true });
71
+ });
72
+ return app;
73
+ }
74
+ export { createKanbanRoute };
@@ -1,6 +1,7 @@
1
1
  export type * from './agent.js';
2
2
  export type * from './base.js';
3
3
  export type * from './config.js';
4
+ export type * from './kanban.js';
4
5
  export type * from './llm.js';
5
6
  export type * from './message.js';
6
7
  export type * from './tool.js';