kanbo-cli 0.2.1 → 0.4.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/README.md +17 -46
  3. package/dist/{better-sqlite3-fU3G4xnX.cjs → better-sqlite3-DPdSZ6sH.cjs} +244 -247
  4. package/dist/cli-main.cjs +51690 -0
  5. package/dist/cli.cjs +32 -45216
  6. package/dist/dist-IHRTF-b5.cjs +2552 -0
  7. package/dist/lib/{tool-names-WfH4IURz.d.cts → approval-BsBk2xVr.d.cts} +72 -3
  8. package/dist/lib/{tool-names-3q_GEZej.d.mts → approval-LRZ6ontC.d.mts} +72 -3
  9. package/dist/lib/{board-store-BvWn9xTB.d.mts → board-store-D_Mpfb84.d.mts} +75 -2
  10. package/dist/lib/{board-store-BfszzAJB.d.cts → board-store-Dx-mw2I6.d.cts} +75 -2
  11. package/dist/lib/index.cjs +8 -2
  12. package/dist/lib/index.d.cts +202 -91
  13. package/dist/lib/index.d.mts +202 -91
  14. package/dist/lib/index.mjs +3 -3
  15. package/dist/lib/{manifest-D9nCRtXM.cjs → manifest-DcIMYvWk.cjs} +7363 -1556
  16. package/dist/lib/manifest-Dmo6IlWk.mjs +14654 -0
  17. package/dist/lib/mcp/index.cjs +166 -22
  18. package/dist/lib/mcp/index.d.cts +144 -5
  19. package/dist/lib/mcp/index.d.mts +144 -5
  20. package/dist/lib/mcp/index.mjs +162 -23
  21. package/dist/lib/{migrate-DnMjTCvu.cjs → migrate-Dca5WdYd.cjs} +144 -14
  22. package/dist/lib/{migrate-BhYi9cXw.mjs → migrate-DtBeW3Fs.mjs} +134 -16
  23. package/dist/lib/{migrations-path-Ciwan537.mjs → migrations-path-ClaYRNwk.mjs} +5 -7
  24. package/dist/lib/{migrations-path-CZxVOgeD.cjs → migrations-path-DBCE17dT.cjs} +5 -7
  25. package/dist/lib/postgres/index.cjs +2 -4
  26. package/dist/lib/postgres/index.d.cts +2 -3
  27. package/dist/lib/postgres/index.d.mts +2 -3
  28. package/dist/lib/postgres/index.mjs +2 -4
  29. package/dist/lib/{roles.sql-CvyCGt38.mjs → roles.sql-BoOZSkBp.mjs} +96 -5
  30. package/dist/lib/{roles.sql-B9rt-x4o.cjs → roles.sql-E58x7mLS.cjs} +95 -4
  31. package/dist/lib/schema-D0MyXcej.d.cts +1 -2
  32. package/dist/lib/schema-D0MyXcej.d.mts +1 -2
  33. package/dist/lib/schema-epoch-ZyumVL_A.d.cts +1 -2
  34. package/dist/lib/schema-epoch-ZyumVL_A.d.mts +1 -2
  35. package/dist/lib/sqlite/index.cjs +2 -2
  36. package/dist/lib/sqlite/index.d.cts +3 -4
  37. package/dist/lib/sqlite/index.d.mts +3 -4
  38. package/dist/lib/sqlite/index.mjs +2 -2
  39. package/dist/lib/sqlite/schema.cjs +0 -2
  40. package/dist/lib/sqlite/schema.mjs +0 -2
  41. package/dist/page/board.css +1622 -327
  42. package/dist/page/board.js +2 -2
  43. package/package.json +13 -10
  44. package/dist/better-sqlite3-fU3G4xnX.cjs.map +0 -1
  45. package/dist/cli.cjs.map +0 -1
  46. package/dist/dist-BsCwQFEl.cjs +0 -989
  47. package/dist/dist-BsCwQFEl.cjs.map +0 -1
  48. package/dist/lib/manifest-D9nCRtXM.cjs.map +0 -1
  49. package/dist/lib/manifest-DHV2fRdO.mjs +0 -8933
  50. package/dist/lib/manifest-DHV2fRdO.mjs.map +0 -1
  51. package/dist/lib/mcp/index.cjs.map +0 -1
  52. package/dist/lib/mcp/index.mjs.map +0 -1
  53. package/dist/lib/migrate-BhYi9cXw.mjs.map +0 -1
  54. package/dist/lib/migrate-DnMjTCvu.cjs.map +0 -1
  55. package/dist/lib/migrations-path-CZxVOgeD.cjs.map +0 -1
  56. package/dist/lib/migrations-path-Ciwan537.mjs.map +0 -1
  57. package/dist/lib/postgres/index.cjs.map +0 -1
  58. package/dist/lib/postgres/index.mjs.map +0 -1
  59. package/dist/lib/roles.sql-B9rt-x4o.cjs.map +0 -1
  60. package/dist/lib/roles.sql-CvyCGt38.mjs.map +0 -1
  61. package/dist/lib/sqlite/schema.cjs.map +0 -1
  62. package/dist/lib/sqlite/schema.mjs.map +0 -1
  63. package/drizzle-postgres/0000_board.sql +0 -135
  64. package/drizzle-postgres/0001_history_order.sql +0 -2
  65. package/drizzle-postgres/0002_issue_pull_requests.sql +0 -16
  66. package/drizzle-postgres/0003_milestone_start_date.sql +0 -1
  67. package/drizzle-postgres/0004_status_entry_rules.sql +0 -1
  68. package/drizzle-postgres/0005_run_external_session_ref.sql +0 -1
  69. package/drizzle-postgres/README.md +0 -61
  70. package/drizzle-postgres/meta/0000_snapshot.json +0 -1078
  71. package/drizzle-postgres/meta/0001_snapshot.json +0 -1090
  72. package/drizzle-postgres/meta/0002_snapshot.json +0 -1239
  73. package/drizzle-postgres/meta/0003_snapshot.json +0 -1245
  74. package/drizzle-postgres/meta/0004_snapshot.json +0 -1251
  75. package/drizzle-postgres/meta/0005_snapshot.json +0 -1257
  76. package/drizzle-postgres/meta/_journal.json +0 -48
  77. package/drizzle-sqlite/0000_board.sql +0 -133
  78. package/drizzle-sqlite/0001_issue_pull_requests.sql +0 -15
  79. package/drizzle-sqlite/0002_milestone_start_date.sql +0 -1
  80. package/drizzle-sqlite/0003_status_entry_rules.sql +0 -1
  81. package/drizzle-sqlite/0004_run_external_session_ref.sql +0 -1
  82. package/drizzle-sqlite/README.md +0 -67
  83. package/drizzle-sqlite/meta/0000_snapshot.json +0 -946
  84. package/drizzle-sqlite/meta/0001_snapshot.json +0 -1054
  85. package/drizzle-sqlite/meta/0002_snapshot.json +0 -1061
  86. package/drizzle-sqlite/meta/0003_snapshot.json +0 -1068
  87. package/drizzle-sqlite/meta/0004_snapshot.json +0 -1075
  88. package/drizzle-sqlite/meta/_journal.json +0 -41
@@ -1,5 +1,5 @@
1
- import { G as normalizeStatusName, _ as projectKanboPullRequest, b as projectKanboSubCard, c as registerKanboTools, d as KANBO_MCP_INSTRUCTIONS, f as KANBO_CAPABILITIES_MARKDOWN_RESOURCE_URI, g as projectKanboColumn, h as projectKanboCard, i as KANBO_TOOL_NAMES, l as KANBO_TOOLS, m as registerKanboCapabilitiesResources, o as KANBO_MCP_SERVER_NAME, p as KANBO_CAPABILITIES_RESOURCE_URI, r as KANBO_TOOL_CLI_EQUIVALENTS, s as createKanboMcpServer, u as buildKanboTools, v as projectKanboRun, y as projectKanboSprint } from "../manifest-DHV2fRdO.mjs";
2
- import { a as BoardError } from "../migrations-path-Ciwan537.mjs";
1
+ import { A as projectKanboSprint, C as pageOf, D as projectKanboFieldChange, E as projectKanboColumn, O as projectKanboPullRequest, S as matchesKanboCardQuery, T as projectKanboCardComment, b as DEFAULT_KANBO_COMMENT_LIMIT, c as registerKanboTools, d as KANBO_MCP_INSTRUCTIONS, f as KANBO_CAPABILITIES_MARKDOWN_RESOURCE_URI, i as KANBO_TOOL_NAMES, it as normalizeStatusName, j as projectKanboSubCard, k as projectKanboRun, l as KANBO_TOOLS, m as registerKanboCapabilitiesResources, o as KANBO_MCP_SERVER_NAME, p as KANBO_CAPABILITIES_RESOURCE_URI, r as KANBO_TOOL_CLI_EQUIVALENTS, s as createKanboMcpServer, st as previewComment, u as buildKanboTools, w as projectKanboCard, x as KANBO_CARD_INCLUDES, y as DEFAULT_KANBO_CARD_INCLUDES } from "../manifest-Dmo6IlWk.mjs";
2
+ import { a as BoardError } from "../migrations-path-ClaYRNwk.mjs";
3
3
  import { z } from "zod";
4
4
  //#region src/mcp/http-transport.ts
5
5
  const IssueSchema = z.object({
@@ -87,6 +87,32 @@ const SprintSchema = z.object({
87
87
  })
88
88
  });
89
89
  const PrimeSchema = z.object({ text: z.string() });
90
+ const ACTOR_KINDS = [
91
+ "user",
92
+ "agent",
93
+ "provider-target",
94
+ "system"
95
+ ];
96
+ /** A comment as the server lists it; who wrote it by the stored kind, `system.*` markers included. */
97
+ const CardCommentSchema = z.object({
98
+ id: z.string(),
99
+ authorKind: z.string(),
100
+ authorId: z.string().nullish(),
101
+ sourceChatSessionId: z.string().nullish(),
102
+ content: z.string(),
103
+ createdAt: z.number()
104
+ }).transform((row) => ({
105
+ ...row,
106
+ authorId: row.authorId ?? null,
107
+ sourceChatSessionId: row.sourceChatSessionId ?? null
108
+ }));
109
+ const FieldChangeSchema = z.object({
110
+ field: z.string(),
111
+ fromValue: z.string().nullable(),
112
+ toValue: z.string().nullable(),
113
+ actorKind: z.enum(ACTOR_KINDS),
114
+ createdAt: z.number()
115
+ });
90
116
  function createHttpTransport(input) {
91
117
  const { request } = input;
92
118
  async function readColumns() {
@@ -110,36 +136,135 @@ function createHttpTransport(input) {
110
136
  });
111
137
  return column;
112
138
  }
139
+ async function listCards(filters) {
140
+ const query = new URLSearchParams({
141
+ workspaceId: input.workspaceId(),
142
+ ...filters
143
+ });
144
+ return z.array(IssueSchema).parse(await request("GET", `/issues/?${query.toString()}`));
145
+ }
146
+ /**
147
+ * The cards a query picks. A server of the `/issues` routes filters on one
148
+ * column and on a parent, and no more; the rest of the query is applied here,
149
+ * to the cards it answered with, so the answer is the one the board file gives.
150
+ *
151
+ * The server filters on a parent's id only, and an agent may name the parent
152
+ * the way it names any card — `WOR-001`, `WOR-1`, `1`. The id is asked first;
153
+ * when no card answers it, the board is read whole to learn which card the
154
+ * reference names, and a reference that names none is refused as `issue_not_found`.
155
+ */
156
+ async function cardPage(cardQuery) {
157
+ const columns = await readColumns();
158
+ const statusIds = cardQuery.columns === void 0 ? null : new Set(cardQuery.columns.map((column) => requireColumn(columns, column).id));
159
+ const filters = statusIds?.size === 1 ? { statusId: [...statusIds][0] } : {};
160
+ let parent = cardQuery.parent;
161
+ let rows = await listCards(parent === void 0 ? filters : {
162
+ ...filters,
163
+ parentIssueId: parent
164
+ });
165
+ if (parent !== void 0 && rows.length === 0) {
166
+ const board = await listCards({});
167
+ const named = findCardRow(board, parent);
168
+ if (!named) throw new BoardError("issue_not_found", { issueId: parent });
169
+ parent = named.id;
170
+ rows = board;
171
+ }
172
+ const matched = rows.filter((row) => matchesKanboCardQuery(row, {
173
+ ...cardQuery,
174
+ parent,
175
+ returned: void 0
176
+ }, statusIds));
177
+ const picked = cardQuery.returned === void 0 ? matched : (await withAttention(matched)).filter((row) => matchesKanboCardQuery(row, cardQuery, null));
178
+ const offset = cardQuery.offset ?? 0;
179
+ return {
180
+ cards: (await withAttention(picked.slice(offset, cardQuery.limit === void 0 ? void 0 : offset + cardQuery.limit))).map((row) => projectKanboCard(row, columns)),
181
+ total: picked.length,
182
+ offset
183
+ };
184
+ }
185
+ /**
186
+ * The cards with what a list says beside the ones a person has to look at or
187
+ * sent back — their comments read one card at a time, since the route lists
188
+ * none, and only for cards that are waiting, or neither waiting nor running.
189
+ */
190
+ async function withAttention(rows) {
191
+ return await Promise.all(rows.map(async (row) => {
192
+ if (row.attention || row.waitingFor !== "human" && row.activeRun !== null) return row;
193
+ const comments = z.array(CardCommentSchema).parse(await request("GET", `/issues/${encodeURIComponent(row.id)}/comments`));
194
+ const decision = comments.filter((comment) => comment.authorKind === "system.approved" || comment.authorKind === "system.returned").at(-1);
195
+ const returned = row.waitingFor !== "human" && decision?.authorKind === "system.returned";
196
+ const latest = comments.filter((comment) => comment.authorKind !== "system.run").at(-1);
197
+ const lastComment = (row.waitingFor === "human" || returned) && latest ? previewComment({
198
+ ...latest,
199
+ authorKind: latest.authorKind
200
+ }) : null;
201
+ return {
202
+ ...row,
203
+ attention: {
204
+ returned,
205
+ lastComment
206
+ }
207
+ };
208
+ }));
209
+ }
210
+ /** The ready cards, a page of them; the server answers the whole queue. */
211
+ async function readyPage({ returnedLimit, ...page }) {
212
+ const query = new URLSearchParams({ workspaceId: input.workspaceId() });
213
+ const cards = z.array(IssueSchema).parse(await request("GET", `/issues/ready?${query.toString()}`));
214
+ const columns = await readColumns();
215
+ const offset = page.offset ?? 0;
216
+ const ready = {
217
+ cards: cards.slice(offset, page.limit === void 0 ? void 0 : offset + page.limit).map((row) => projectKanboCard(row, columns)),
218
+ total: cards.length,
219
+ offset
220
+ };
221
+ return returnedLimit === void 0 ? ready : {
222
+ ...ready,
223
+ returned: await cardPage({
224
+ returned: true,
225
+ limit: returnedLimit
226
+ })
227
+ };
228
+ }
113
229
  return {
114
230
  prime: async () => PrimeSchema.parse(await request("GET", `/issues/prime?workspaceId=${encodeURIComponent(input.workspaceId())}`)),
115
- ready: async ({ limit }) => {
116
- const query = new URLSearchParams({ workspaceId: input.workspaceId() });
117
- if (limit !== void 0) query.set("limit", String(limit));
118
- const cards = z.array(IssueSchema).parse(await request("GET", `/issues/ready?${query.toString()}`));
119
- const columns = await readColumns();
120
- return cards.map((row) => projectKanboCard(row, columns));
121
- },
231
+ ready: async ({ limit }) => (await readyPage({ limit })).cards,
232
+ readyPage,
122
233
  columns: readColumns,
123
234
  sprints: async () => z.array(SprintSchema).parse(await request("GET", `/issues/sprints?workspaceId=${encodeURIComponent(input.workspaceId())}`)).map(projectKanboSprint),
124
- cardGet: async ({ card }) => {
235
+ cardGet: async ({ card, include = DEFAULT_KANBO_CARD_INCLUDES, commentLimit = 10 }) => {
125
236
  const found = IssueSchema.parse(await request("GET", `/issues/${encodeURIComponent(card)}`));
126
- const columns = await readColumns();
237
+ const wants = new Set(include);
238
+ const path = `/issues/${encodeURIComponent(found.id)}`;
127
239
  const query = new URLSearchParams({
128
240
  workspaceId: input.workspaceId(),
129
241
  parentIssueId: found.id
130
242
  });
131
- const subCards = z.array(IssueSchema).parse(await request("GET", `/issues/?${query.toString()}`));
243
+ const [columns, subCards, comments, runs, history, pullRequests] = await Promise.all([
244
+ readColumns(),
245
+ wants.has("subCards") ? request("GET", `/issues/?${query.toString()}`).then((rows) => z.array(IssueSchema).parse(rows)) : null,
246
+ wants.has("comments") ? request("GET", `${path}/comments`).then((rows) => z.array(CardCommentSchema).parse(rows)) : null,
247
+ wants.has("runs") ? request("GET", `${path}/runs`).then((rows) => z.array(RunSchema).parse(rows)) : null,
248
+ wants.has("history") ? request("GET", `${path}/field-changes`).then((rows) => z.array(FieldChangeSchema).parse(rows)) : null,
249
+ wants.has("prs") ? request("GET", `${path}/pull-requests`).then((rows) => z.array(PullRequestSchema).parse(rows)) : null
250
+ ]);
132
251
  return {
133
252
  ...projectKanboCard(found, columns),
134
- subCards: subCards.map((row) => projectKanboSubCard(row, columns))
253
+ ...subCards ? { subCards: subCards.map((row) => projectKanboSubCard(row, columns)) } : {},
254
+ ...comments ? {
255
+ comments: comments.slice(-commentLimit).map(projectKanboCardComment),
256
+ commentCount: comments.length
257
+ } : {},
258
+ ...runs ? { runs: runs.map(projectKanboRun) } : {},
259
+ ...history ? { history: history.map(projectKanboFieldChange) } : {},
260
+ ...pullRequests ? { pullRequests: pullRequests.map(projectKanboPullRequest) } : {}
135
261
  };
136
262
  },
137
- cardList: async ({ column, limit }) => {
138
- const columns = await readColumns();
139
- const query = new URLSearchParams({ workspaceId: input.workspaceId() });
140
- if (column) query.set("statusId", requireColumn(columns, column).id);
141
- return z.array(IssueSchema).parse(await request("GET", `/issues/?${query.toString()}`)).slice(0, limit ?? Number.POSITIVE_INFINITY).map((row) => projectKanboCard(row, columns));
142
- },
263
+ cardList: async ({ column, limit }) => (await cardPage({
264
+ columns: column ? [column] : void 0,
265
+ limit
266
+ })).cards,
267
+ cardPage,
143
268
  cardCreate: async ({ title, description, column, parent, executionMode }) => await projectOne(await request("POST", "/issues/", {
144
269
  workspaceId: input.workspaceId(),
145
270
  ...title !== void 0 ? { title } : {},
@@ -148,7 +273,10 @@ function createHttpTransport(input) {
148
273
  ...parent !== void 0 ? { parentIssueId: parent } : {},
149
274
  ...executionMode !== void 0 ? { executionMode } : {}
150
275
  })),
151
- cardUpdate: async ({ card, ...patch }) => await projectOne(await request("PATCH", `/issues/${encodeURIComponent(card)}`, patch)),
276
+ cardUpdate: async ({ card, parent, ...patch }) => await projectOne(await request("PATCH", `/issues/${encodeURIComponent(card)}`, {
277
+ ...patch,
278
+ ...parent !== void 0 ? { parentIssueId: parent } : {}
279
+ })),
152
280
  cardMove: async ({ card, column }) => await projectOne(await request("PATCH", `/issues/${encodeURIComponent(card)}/status/${encodeURIComponent(column)}`)),
153
281
  cardComment: async ({ card, content }) => CommentSchema.parse(await request("POST", `/issues/${encodeURIComponent(card)}/comments`, { content })),
154
282
  cardLinkPullRequest: async ({ card, url }) => projectKanboPullRequest(PullRequestSchema.parse(await request("POST", `/issues/${encodeURIComponent(card)}/pull-requests`, { url }))),
@@ -167,7 +295,18 @@ function createHttpTransport(input) {
167
295
  })))
168
296
  };
169
297
  }
298
+ /**
299
+ * The card a reference names among cards already read, the way the board file
300
+ * resolves one: its id, its number, or its key without the padding (`WOR-1`).
301
+ */
302
+ function findCardRow(rows, reference) {
303
+ const trimmed = reference.trim();
304
+ const byId = rows.find((row) => row.id === trimmed);
305
+ if (byId) return byId;
306
+ const key = /^(?:(.+)-)?(\d+)$/.exec(trimmed);
307
+ if (!key) return;
308
+ const [, prefix, digits] = key;
309
+ return rows.find((row) => row.number === Number(digits) && (prefix === void 0 || row.id.slice(0, row.id.lastIndexOf("-")).toUpperCase() === prefix.toUpperCase()));
310
+ }
170
311
  //#endregion
171
- export { KANBO_CAPABILITIES_MARKDOWN_RESOURCE_URI, KANBO_CAPABILITIES_RESOURCE_URI, KANBO_MCP_INSTRUCTIONS, KANBO_MCP_SERVER_NAME, KANBO_TOOLS, KANBO_TOOL_CLI_EQUIVALENTS, KANBO_TOOL_NAMES, buildKanboTools, createHttpTransport, createKanboMcpServer, registerKanboCapabilitiesResources, registerKanboTools };
172
-
173
- //# sourceMappingURL=index.mjs.map
312
+ export { DEFAULT_KANBO_CARD_INCLUDES, DEFAULT_KANBO_COMMENT_LIMIT, KANBO_CAPABILITIES_MARKDOWN_RESOURCE_URI, KANBO_CAPABILITIES_RESOURCE_URI, KANBO_CARD_INCLUDES, KANBO_MCP_INSTRUCTIONS, KANBO_MCP_SERVER_NAME, KANBO_TOOLS, KANBO_TOOL_CLI_EQUIVALENTS, KANBO_TOOL_NAMES, buildKanboTools, createHttpTransport, createKanboMcpServer, matchesKanboCardQuery, pageOf, registerKanboCapabilitiesResources, registerKanboTools };
@@ -1,4 +1,4 @@
1
- const require_migrations_path = require("./migrations-path-CZxVOgeD.cjs");
1
+ const require_migrations_path = require("./migrations-path-DBCE17dT.cjs");
2
2
  const require_sqlite_schema = require("./sqlite/schema.cjs");
3
3
  let drizzle_orm = require("drizzle-orm");
4
4
  let drizzle_orm_sqlite_core = require("drizzle-orm/sqlite-core");
@@ -6,6 +6,17 @@ let node_module = require("node:module");
6
6
  let node_path = require("node:path");
7
7
  let node_url = require("node:url");
8
8
  let drizzle_orm_better_sqlite3_migrator = require("drizzle-orm/better-sqlite3/migrator");
9
+ //#region src/domain/fold-case.ts
10
+ /**
11
+ * Text as a search compares it: every letter lowercased by Unicode's rules, not
12
+ * only A–Z — `Über` and `über` are one word. What Postgres `ilike` does on
13
+ * either side of a board search; SQLite's own `like` folds ASCII alone, so a
14
+ * board file compares through this function instead (`kanbo_fold`).
15
+ */
16
+ function foldCase(text) {
17
+ return text.toLowerCase();
18
+ }
19
+ //#endregion
9
20
  //#region src/sqlite/transaction.ts
10
21
  /**
11
22
  * The better-sqlite3 handle Drizzle keeps beside the query builder. `drizzle()`
@@ -162,6 +173,82 @@ function selectRunProjection(database, scope) {
162
173
  }));
163
174
  }
164
175
  /**
176
+ * One page of the cards a query picks, and how many it picks in all, in one
177
+ * statement: `count(*) over ()` is taken over every picked row before `limit`
178
+ * and `offset` cut the page out of them. Only a page past the last card — no
179
+ * row to carry the count — asks a second time.
180
+ */
181
+ function selectCardPage(database, query) {
182
+ if (query.statusIds?.length === 0 || query.priorities?.length === 0) return {
183
+ cards: [],
184
+ total: 0
185
+ };
186
+ if (query.text !== void 0) registerFoldFunction(database);
187
+ const where = (0, drizzle_orm.and)(...cardQueryPredicates(query));
188
+ const rows = database.select({
189
+ ...(0, drizzle_orm.getTableColumns)(require_sqlite_schema.issues),
190
+ pickedTotal: drizzle_orm.sql`count(*) over ()`
191
+ }).from(require_sqlite_schema.issues).where(where).orderBy(require_sqlite_schema.issues.order, (0, drizzle_orm.desc)(require_sqlite_schema.issues.createdAt), require_sqlite_schema.issues.id).limit(query.limit ?? Number.MAX_SAFE_INTEGER).offset(query.offset ?? 0).all();
192
+ if (rows.length === 0) return {
193
+ cards: [],
194
+ total: (query.offset ?? 0) > 0 ? database.select({ count: drizzle_orm.sql`count(*)` }).from(require_sqlite_schema.issues).where(where).get()?.count ?? 0 : 0
195
+ };
196
+ return {
197
+ cards: rows.map(({ pickedTotal: _total, ...card }) => card),
198
+ total: rows[0].pickedTotal
199
+ };
200
+ }
201
+ /** The connections `kanbo_fold` is registered on already. */
202
+ const foldingConnections = /* @__PURE__ */ new WeakSet();
203
+ /**
204
+ * Teach the connection `kanbo_fold(text)`, the search's `foldCase`, once.
205
+ *
206
+ * SQLite's `like` and `lower()` fold A–Z and nothing else, so `über` would not
207
+ * find `Über` on a board file while Postgres `ilike` and the HTTP transport find
208
+ * it. The function is registered on the connection the statement runs on —
209
+ * the host may have opened it, not this package — and stays one statement.
210
+ */
211
+ function registerFoldFunction(database) {
212
+ const connection = readSqliteConnection(database);
213
+ if (foldingConnections.has(connection)) return;
214
+ connection.function("kanbo_fold", { deterministic: true }, (value) => typeof value === "string" ? foldCase(value) : value);
215
+ foldingConnections.add(connection);
216
+ }
217
+ function cardQueryPredicates(query) {
218
+ const running = drizzle_orm.sql`${require_sqlite_schema.issues.id} in (select ${require_sqlite_schema.issueRuns.issueId} from ${require_sqlite_schema.issueRuns} where ${require_sqlite_schema.issueRuns.state} = 'running')`;
219
+ const text = foldCase(query.text ?? "");
220
+ return [
221
+ (0, drizzle_orm.eq)(require_sqlite_schema.issues.workspaceId, query.workspaceId),
222
+ query.statusIds === void 0 ? void 0 : (0, drizzle_orm.inArray)(require_sqlite_schema.issues.statusId, [...query.statusIds]),
223
+ query.waitingForPerson === void 0 ? void 0 : query.waitingForPerson ? (0, drizzle_orm.eq)(require_sqlite_schema.issues.waitingFor, "human") : (0, drizzle_orm.isNull)(require_sqlite_schema.issues.waitingFor),
224
+ query.parentIssueId === void 0 ? void 0 : (0, drizzle_orm.eq)(require_sqlite_schema.issues.parentIssueId, query.parentIssueId),
225
+ query.hasActiveRun === void 0 ? void 0 : query.hasActiveRun ? running : drizzle_orm.sql`not ${running}`,
226
+ query.returned === void 0 ? void 0 : query.returned ? returnedCard(running) : drizzle_orm.sql`not ${returnedCard(running)}`,
227
+ query.text === void 0 ? void 0 : drizzle_orm.sql`(instr(kanbo_fold(${require_sqlite_schema.issues.id}), ${text}) > 0 or instr(kanbo_fold(${require_sqlite_schema.issues.title}), ${text}) > 0 or instr(kanbo_fold(coalesce(${require_sqlite_schema.issues.description}, '')), ${text}) > 0)`,
228
+ query.updatedSince === void 0 ? void 0 : (0, drizzle_orm.gte)(require_sqlite_schema.issues.updatedAt, query.updatedSince),
229
+ ...(query.labels ?? []).map((label) => drizzle_orm.sql`exists (select 1 from json_each(${require_sqlite_schema.issues.labels}) where value = ${label})`),
230
+ query.priorities === void 0 ? void 0 : (0, drizzle_orm.inArray)(require_sqlite_schema.issues.priority, [...query.priorities])
231
+ ];
232
+ }
233
+ /**
234
+ * A card a person sent back that nobody picked up again (`BoardCardQuery.returned`):
235
+ * not waiting, not running, and its last decision a return — no approval or
236
+ * return written after it, `rowid` breaking a tie inside one second the way
237
+ * `readApproval`'s comment order does.
238
+ */
239
+ function returnedCard(running) {
240
+ return drizzle_orm.sql`(${require_sqlite_schema.issues.waitingFor} is null and not ${running} and exists (
241
+ select 1 from ${require_sqlite_schema.issueComments} as decision
242
+ where decision.issue_id = ${require_sqlite_schema.issues.id} and decision.author_kind = 'system.returned'
243
+ and not exists (
244
+ select 1 from ${require_sqlite_schema.issueComments} as later
245
+ where later.issue_id = decision.issue_id
246
+ and later.author_kind in ('system.approved', 'system.returned')
247
+ and (later.created_at > decision.created_at or (later.created_at = decision.created_at and later.rowid > decision.rowid))
248
+ )
249
+ ))`;
250
+ }
251
+ /**
165
252
  * Bind the store's statements to one SQLite handle. `resolveDatabase` is called
166
253
  * per statement rather than captured, so the store follows a host that swaps its
167
254
  * database out (a server that resets its infrastructure between tests, say).
@@ -173,6 +260,7 @@ function createSqliteBoardStoreCore(resolveDatabase) {
173
260
  listAllInBoardOrder: () => resolveDatabase().select().from(require_sqlite_schema.issueStatuses).orderBy(require_sqlite_schema.issueStatuses.workspaceId, require_sqlite_schema.issueStatuses.order).all(),
174
261
  listAll: () => resolveDatabase().select().from(require_sqlite_schema.issueStatuses).all(),
175
262
  findById: (statusId) => resolveDatabase().select().from(require_sqlite_schema.issueStatuses).where((0, drizzle_orm.eq)(require_sqlite_schema.issueStatuses.id, statusId)).get() ?? null,
263
+ lockById: (statusId) => resolveDatabase().select().from(require_sqlite_schema.issueStatuses).where((0, drizzle_orm.eq)(require_sqlite_schema.issueStatuses.id, statusId)).get() ?? null,
176
264
  findInWorkspace: (workspaceId, statusId) => resolveDatabase().select().from(require_sqlite_schema.issueStatuses).where(drizzle_orm.sql`${require_sqlite_schema.issueStatuses.workspaceId} = ${workspaceId} AND ${require_sqlite_schema.issueStatuses.id} = ${statusId}`).get() ?? null,
177
265
  countByWorkspace: (workspaceId) => {
178
266
  return resolveDatabase().select({ count: drizzle_orm.sql`count(*)` }).from(require_sqlite_schema.issueStatuses).where((0, drizzle_orm.eq)(require_sqlite_schema.issueStatuses.workspaceId, workspaceId)).get()?.count ?? 0;
@@ -205,11 +293,17 @@ function createSqliteBoardStoreCore(resolveDatabase) {
205
293
  issues: {
206
294
  listInBoardOrder: (workspaceId) => {
207
295
  const predicates = [workspaceId ? (0, drizzle_orm.eq)(require_sqlite_schema.issues.workspaceId, workspaceId) : void 0].filter((predicate) => predicate !== void 0);
208
- return resolveDatabase().select().from(require_sqlite_schema.issues).where(predicates.length > 0 ? (0, drizzle_orm.and)(...predicates) : void 0).orderBy(require_sqlite_schema.issues.order, (0, drizzle_orm.desc)(require_sqlite_schema.issues.createdAt)).all();
296
+ return resolveDatabase().select().from(require_sqlite_schema.issues).where(predicates.length > 0 ? (0, drizzle_orm.and)(...predicates) : void 0).orderBy(require_sqlite_schema.issues.order, (0, drizzle_orm.desc)(require_sqlite_schema.issues.createdAt), require_sqlite_schema.issues.id).all();
209
297
  },
210
298
  listNewestFirst: () => resolveDatabase().select().from(require_sqlite_schema.issues).orderBy((0, drizzle_orm.desc)(require_sqlite_schema.issues.createdAt)).all(),
211
299
  listAll: () => resolveDatabase().select().from(require_sqlite_schema.issues).all(),
212
300
  listByWorkspace: (workspaceId) => resolveDatabase().select().from(require_sqlite_schema.issues).where((0, drizzle_orm.eq)(require_sqlite_schema.issues.workspaceId, workspaceId)).all(),
301
+ listPage: (query) => selectCardPage(resolveDatabase(), query),
302
+ countByMilestone: (workspaceId) => resolveDatabase().select({
303
+ milestoneId: drizzle_orm.sql`${require_sqlite_schema.issues.milestoneId}`,
304
+ statusId: require_sqlite_schema.issues.statusId,
305
+ count: drizzle_orm.sql`count(*)`
306
+ }).from(require_sqlite_schema.issues).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(require_sqlite_schema.issues.workspaceId, workspaceId), (0, drizzle_orm.isNotNull)(require_sqlite_schema.issues.milestoneId))).groupBy(require_sqlite_schema.issues.milestoneId, require_sqlite_schema.issues.statusId).all(),
213
307
  listByIds: (issueIds) => resolveDatabase().select().from(require_sqlite_schema.issues).where((0, drizzle_orm.inArray)(require_sqlite_schema.issues.id, issueIds)).all(),
214
308
  findById: (issueId) => resolveDatabase().select().from(require_sqlite_schema.issues).where((0, drizzle_orm.eq)(require_sqlite_schema.issues.id, issueId)).get() ?? null,
215
309
  findInWorkspace: (workspaceId, issueId) => resolveDatabase().select().from(require_sqlite_schema.issues).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(require_sqlite_schema.issues.id, issueId), (0, drizzle_orm.eq)(require_sqlite_schema.issues.workspaceId, workspaceId))).get() ?? null,
@@ -248,6 +342,15 @@ function createSqliteBoardStoreCore(resolveDatabase) {
248
342
  },
249
343
  comments: {
250
344
  listByIssue: (issueId) => resolveDatabase().select().from(require_sqlite_schema.issueComments).where((0, drizzle_orm.eq)(require_sqlite_schema.issueComments.issueId, issueId)).orderBy((0, drizzle_orm.asc)(require_sqlite_schema.issueComments.createdAt), COMMENT_INSERTION_ORDER).all(),
345
+ listLatestByIssues: (issueIds, authorKinds) => {
346
+ if (issueIds.length === 0 || authorKinds?.length === 0) return [];
347
+ const database = resolveDatabase();
348
+ const ranked = database.select({
349
+ id: require_sqlite_schema.issueComments.id,
350
+ commentRank: drizzle_orm.sql`row_number() over (partition by ${require_sqlite_schema.issueComments.issueId} order by ${require_sqlite_schema.issueComments.createdAt} desc, ${require_sqlite_schema.issueComments}.rowid desc)`.as("comment_rank")
351
+ }).from(require_sqlite_schema.issueComments).where((0, drizzle_orm.and)((0, drizzle_orm.inArray)(require_sqlite_schema.issueComments.issueId, [...issueIds]), authorKinds === void 0 ? void 0 : (0, drizzle_orm.inArray)(require_sqlite_schema.issueComments.authorKind, [...authorKinds]))).as("ranked");
352
+ return database.select((0, drizzle_orm.getTableColumns)(require_sqlite_schema.issueComments)).from(require_sqlite_schema.issueComments).innerJoin(ranked, (0, drizzle_orm.eq)(ranked.id, require_sqlite_schema.issueComments.id)).where((0, drizzle_orm.eq)(ranked.commentRank, 1)).all();
353
+ },
251
354
  findById: (commentId) => resolveDatabase().select().from(require_sqlite_schema.issueComments).where((0, drizzle_orm.eq)(require_sqlite_schema.issueComments.id, commentId)).get() ?? null,
252
355
  findByDedupeKey: (issueId, dedupeKey) => resolveDatabase().select().from(require_sqlite_schema.issueComments).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(require_sqlite_schema.issueComments.issueId, issueId), (0, drizzle_orm.eq)(require_sqlite_schema.issueComments.dedupeKey, dedupeKey))).get() ?? null,
253
356
  listByDedupeKeyPrefix: (issueId, prefix) => resolveDatabase().select().from(require_sqlite_schema.issueComments).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(require_sqlite_schema.issueComments.issueId, issueId), drizzle_orm.sql`substr(${require_sqlite_schema.issueComments.dedupeKey}, 1, ${prefix.length}) = ${prefix}`)).orderBy((0, drizzle_orm.asc)(require_sqlite_schema.issueComments.createdAt), COMMENT_INSERTION_ORDER).all(),
@@ -354,6 +457,7 @@ function createSqliteBoardStore(options) {
354
457
  listAllInBoardOrder: async () => core.statuses.listAllInBoardOrder(),
355
458
  listAll: async () => core.statuses.listAll(),
356
459
  findById: async (statusId) => core.statuses.findById(statusId),
460
+ lockById: async (statusId) => core.statuses.lockById(statusId),
357
461
  findInWorkspace: async (workspaceId, statusId) => core.statuses.findInWorkspace(workspaceId, statusId),
358
462
  countByWorkspace: async (workspaceId) => core.statuses.countByWorkspace(workspaceId),
359
463
  create: async (values) => core.statuses.create(values),
@@ -375,6 +479,8 @@ function createSqliteBoardStore(options) {
375
479
  listNewestFirst: async () => core.issues.listNewestFirst(),
376
480
  listAll: async () => core.issues.listAll(),
377
481
  listByWorkspace: async (workspaceId) => core.issues.listByWorkspace(workspaceId),
482
+ listPage: async (query) => core.issues.listPage(query),
483
+ countByMilestone: async (workspaceId) => core.issues.countByMilestone(workspaceId),
378
484
  listByIds: async (issueIds) => core.issues.listByIds(issueIds),
379
485
  findById: async (issueId) => core.issues.findById(issueId),
380
486
  findInWorkspace: async (workspaceId, issueId) => core.issues.findInWorkspace(workspaceId, issueId),
@@ -391,6 +497,7 @@ function createSqliteBoardStore(options) {
391
497
  },
392
498
  comments: {
393
499
  listByIssue: async (issueId) => core.comments.listByIssue(issueId),
500
+ listLatestByIssues: async (issueIds, authorKinds) => core.comments.listLatestByIssues(issueIds, authorKinds),
394
501
  findById: async (commentId) => core.comments.findById(commentId),
395
502
  findByDedupeKey: async (issueId, dedupeKey) => core.comments.findByDedupeKey(issueId, dedupeKey),
396
503
  listByDedupeKeyPrefix: async (issueId, prefix) => core.comments.listByDedupeKeyPrefix(issueId, prefix),
@@ -448,25 +555,35 @@ function createSqliteBoardStore(options) {
448
555
  };
449
556
  return store;
450
557
  }
558
+ /** The logger a board handle is opened with: the counter while one is on, otherwise none. */
559
+ function boardQueryLogger() {}
451
560
  //#endregion
452
561
  //#region src/sqlite/open-database.ts
453
562
  const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
454
563
  /** The tables a board file must already carry for this package to read it. */
455
564
  const REQUIRED_BOARD_TABLES = ["issue_runs", "kanban_meta"];
456
- /** What a caller is told when the native module a board file needs is not installed. */
457
- const BETTER_SQLITE3_MISSING_MESSAGE = "Install better-sqlite3 to use a board file";
458
565
  /**
459
- * `better-sqlite3` is an optional peer dependency (ruling 5): a Postgres-only
460
- * or capabilities-only install carries no native module, and importing
461
- * `kanbo/sqlite` for a type or for `runSqliteTransaction` should not
462
- * demand one either. So the require happens here, the one place a board file
463
- * is actually opened, rather than at the top of this module.
566
+ * What a caller is told when the native module a board file needs does not
567
+ * load: which Node, system and processor it was tried on — a prebuilt module
568
+ * exists for most, not all — what the loader said, and how to get it back.
569
+ */
570
+ function betterSqlite3MissingMessage(cause) {
571
+ const said = cause instanceof Error ? cause.message.split("\n")[0] : String(cause);
572
+ return `Could not load better-sqlite3, which kanbo opens board files with, on Node ${process.version} (${process.platform}-${process.arch}): ${said}. Reinstall kanbo with npm install -g kanbo-cli, which brings it along; a program that uses kanbo as a library installs better-sqlite3 itself.`;
573
+ }
574
+ /**
575
+ * `better-sqlite3` is an optional dependency (ruling 5): an install where it
576
+ * could not be built carries no native module, and a Postgres-only or
577
+ * capabilities-only use — or importing `kanbo/sqlite` for a type or for
578
+ * `runSqliteTransaction` — should not demand one. So the require happens here,
579
+ * the one place a board file is actually opened, rather than at the top of
580
+ * this module.
464
581
  */
465
582
  function loadBetterSqlite3() {
466
583
  try {
467
584
  return require$1("better-sqlite3");
468
585
  } catch (error) {
469
- throw new Error(BETTER_SQLITE3_MISSING_MESSAGE, { cause: error });
586
+ throw new Error(betterSqlite3MissingMessage(error), { cause: error });
470
587
  }
471
588
  }
472
589
  /**
@@ -479,7 +596,7 @@ function loadBetterSqlite3() {
479
596
  * this module, `kanbo capabilities` and `kanbo/mcp` included, and the
480
597
  * friendly message above becomes unreachable. A dynamic import keeps the whole
481
598
  * chain behind the one call that actually opens a file, which is what makes the
482
- * optional peer honest. `createRequire` would be the smaller change and is the
599
+ * optional dependency honest. `createRequire` would be the smaller change and is the
483
600
  * wrong one: it would load drizzle's CJS copy beside the ESM one this package
484
601
  * already holds, and it would resolve from `node_modules`, which the packaged
485
602
  * `dist/cli.cjs` deliberately does not have beside it.
@@ -512,7 +629,10 @@ async function openBoardDatabase(path) {
512
629
  connection.pragma("busy_timeout = 5000");
513
630
  connection.pragma("synchronous = NORMAL");
514
631
  return {
515
- database: drizzle(connection, { schema: require_sqlite_schema.boardSqliteSchema }),
632
+ database: drizzle(connection, {
633
+ schema: require_sqlite_schema.boardSqliteSchema,
634
+ logger: void 0
635
+ }),
516
636
  connection,
517
637
  close: () => connection.close()
518
638
  };
@@ -657,12 +777,24 @@ Object.defineProperty(exports, "assertBoardSchema", {
657
777
  return assertBoardSchema;
658
778
  }
659
779
  });
780
+ Object.defineProperty(exports, "boardQueryLogger", {
781
+ enumerable: true,
782
+ get: function() {
783
+ return boardQueryLogger;
784
+ }
785
+ });
660
786
  Object.defineProperty(exports, "createSqliteBoardStore", {
661
787
  enumerable: true,
662
788
  get: function() {
663
789
  return createSqliteBoardStore;
664
790
  }
665
791
  });
792
+ Object.defineProperty(exports, "foldCase", {
793
+ enumerable: true,
794
+ get: function() {
795
+ return foldCase;
796
+ }
797
+ });
666
798
  Object.defineProperty(exports, "getBoardSqliteMigrationsPath", {
667
799
  enumerable: true,
668
800
  get: function() {
@@ -705,5 +837,3 @@ Object.defineProperty(exports, "runSqliteTransaction", {
705
837
  return runSqliteTransaction;
706
838
  }
707
839
  });
708
-
709
- //# sourceMappingURL=migrate-DnMjTCvu.cjs.map