kanbo-cli 0.2.0 → 0.3.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 (86) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/LICENSE +75 -21
  3. package/README.md +19 -48
  4. package/dist/{better-sqlite3-fU3G4xnX.cjs → better-sqlite3-DPdSZ6sH.cjs} +244 -247
  5. package/dist/cli-main.cjs +51208 -0
  6. package/dist/cli.cjs +32 -45216
  7. package/dist/dist-IHRTF-b5.cjs +2552 -0
  8. package/dist/lib/{tool-names-WfH4IURz.d.cts → approval-BsBk2xVr.d.cts} +72 -3
  9. package/dist/lib/{tool-names-3q_GEZej.d.mts → approval-LRZ6ontC.d.mts} +72 -3
  10. package/dist/lib/{board-store-BvWn9xTB.d.mts → board-store-D_Mpfb84.d.mts} +75 -2
  11. package/dist/lib/{board-store-BfszzAJB.d.cts → board-store-Dx-mw2I6.d.cts} +75 -2
  12. package/dist/lib/index.cjs +8 -2
  13. package/dist/lib/index.d.cts +202 -91
  14. package/dist/lib/index.d.mts +202 -91
  15. package/dist/lib/index.mjs +3 -3
  16. package/dist/lib/{manifest-RDyoGrVR.cjs → manifest-QPoQl8tn.cjs} +6881 -1556
  17. package/dist/lib/{manifest-BwpC-4BO.mjs → manifest-v2e9x_C8.mjs} +6795 -1556
  18. package/dist/lib/mcp/index.cjs +156 -21
  19. package/dist/lib/mcp/index.d.cts +140 -5
  20. package/dist/lib/mcp/index.d.mts +140 -5
  21. package/dist/lib/mcp/index.mjs +152 -22
  22. package/dist/lib/{migrate-DnMjTCvu.cjs → migrate-Dca5WdYd.cjs} +144 -14
  23. package/dist/lib/{migrate-BhYi9cXw.mjs → migrate-DtBeW3Fs.mjs} +134 -16
  24. package/dist/lib/{migrations-path-Ciwan537.mjs → migrations-path-ClaYRNwk.mjs} +5 -7
  25. package/dist/lib/{migrations-path-CZxVOgeD.cjs → migrations-path-DBCE17dT.cjs} +5 -7
  26. package/dist/lib/postgres/index.cjs +2 -4
  27. package/dist/lib/postgres/index.d.cts +2 -3
  28. package/dist/lib/postgres/index.d.mts +2 -3
  29. package/dist/lib/postgres/index.mjs +2 -4
  30. package/dist/lib/{roles.sql-CvyCGt38.mjs → roles.sql-BoOZSkBp.mjs} +96 -5
  31. package/dist/lib/{roles.sql-B9rt-x4o.cjs → roles.sql-E58x7mLS.cjs} +95 -4
  32. package/dist/lib/schema-D0MyXcej.d.cts +1 -2
  33. package/dist/lib/schema-D0MyXcej.d.mts +1 -2
  34. package/dist/lib/schema-epoch-ZyumVL_A.d.cts +1 -2
  35. package/dist/lib/schema-epoch-ZyumVL_A.d.mts +1 -2
  36. package/dist/lib/sqlite/index.cjs +2 -2
  37. package/dist/lib/sqlite/index.d.cts +3 -4
  38. package/dist/lib/sqlite/index.d.mts +3 -4
  39. package/dist/lib/sqlite/index.mjs +2 -2
  40. package/dist/lib/sqlite/schema.cjs +0 -2
  41. package/dist/lib/sqlite/schema.mjs +0 -2
  42. package/package.json +14 -11
  43. package/dist/better-sqlite3-fU3G4xnX.cjs.map +0 -1
  44. package/dist/cli.cjs.map +0 -1
  45. package/dist/dist-BsCwQFEl.cjs +0 -989
  46. package/dist/dist-BsCwQFEl.cjs.map +0 -1
  47. package/dist/lib/manifest-BwpC-4BO.mjs.map +0 -1
  48. package/dist/lib/manifest-RDyoGrVR.cjs.map +0 -1
  49. package/dist/lib/mcp/index.cjs.map +0 -1
  50. package/dist/lib/mcp/index.mjs.map +0 -1
  51. package/dist/lib/migrate-BhYi9cXw.mjs.map +0 -1
  52. package/dist/lib/migrate-DnMjTCvu.cjs.map +0 -1
  53. package/dist/lib/migrations-path-CZxVOgeD.cjs.map +0 -1
  54. package/dist/lib/migrations-path-Ciwan537.mjs.map +0 -1
  55. package/dist/lib/postgres/index.cjs.map +0 -1
  56. package/dist/lib/postgres/index.mjs.map +0 -1
  57. package/dist/lib/roles.sql-B9rt-x4o.cjs.map +0 -1
  58. package/dist/lib/roles.sql-CvyCGt38.mjs.map +0 -1
  59. package/dist/lib/sqlite/schema.cjs.map +0 -1
  60. package/dist/lib/sqlite/schema.mjs.map +0 -1
  61. package/drizzle-postgres/0000_board.sql +0 -135
  62. package/drizzle-postgres/0001_history_order.sql +0 -2
  63. package/drizzle-postgres/0002_issue_pull_requests.sql +0 -16
  64. package/drizzle-postgres/0003_milestone_start_date.sql +0 -1
  65. package/drizzle-postgres/0004_status_entry_rules.sql +0 -1
  66. package/drizzle-postgres/0005_run_external_session_ref.sql +0 -1
  67. package/drizzle-postgres/README.md +0 -61
  68. package/drizzle-postgres/meta/0000_snapshot.json +0 -1078
  69. package/drizzle-postgres/meta/0001_snapshot.json +0 -1090
  70. package/drizzle-postgres/meta/0002_snapshot.json +0 -1239
  71. package/drizzle-postgres/meta/0003_snapshot.json +0 -1245
  72. package/drizzle-postgres/meta/0004_snapshot.json +0 -1251
  73. package/drizzle-postgres/meta/0005_snapshot.json +0 -1257
  74. package/drizzle-postgres/meta/_journal.json +0 -48
  75. package/drizzle-sqlite/0000_board.sql +0 -133
  76. package/drizzle-sqlite/0001_issue_pull_requests.sql +0 -15
  77. package/drizzle-sqlite/0002_milestone_start_date.sql +0 -1
  78. package/drizzle-sqlite/0003_status_entry_rules.sql +0 -1
  79. package/drizzle-sqlite/0004_run_external_session_ref.sql +0 -1
  80. package/drizzle-sqlite/README.md +0 -67
  81. package/drizzle-sqlite/meta/0000_snapshot.json +0 -946
  82. package/drizzle-sqlite/meta/0001_snapshot.json +0 -1054
  83. package/drizzle-sqlite/meta/0002_snapshot.json +0 -1061
  84. package/drizzle-sqlite/meta/0003_snapshot.json +0 -1068
  85. package/drizzle-sqlite/meta/0004_snapshot.json +0 -1075
  86. 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-BwpC-4BO.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-v2e9x_C8.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,26 @@ 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
+ content: z.string(),
101
+ createdAt: z.number()
102
+ });
103
+ const FieldChangeSchema = z.object({
104
+ field: z.string(),
105
+ fromValue: z.string().nullable(),
106
+ toValue: z.string().nullable(),
107
+ actorKind: z.enum(ACTOR_KINDS),
108
+ createdAt: z.number()
109
+ });
90
110
  function createHttpTransport(input) {
91
111
  const { request } = input;
92
112
  async function readColumns() {
@@ -110,36 +130,135 @@ function createHttpTransport(input) {
110
130
  });
111
131
  return column;
112
132
  }
133
+ async function listCards(filters) {
134
+ const query = new URLSearchParams({
135
+ workspaceId: input.workspaceId(),
136
+ ...filters
137
+ });
138
+ return z.array(IssueSchema).parse(await request("GET", `/issues/?${query.toString()}`));
139
+ }
140
+ /**
141
+ * The cards a query picks. A server of the `/issues` routes filters on one
142
+ * column and on a parent, and no more; the rest of the query is applied here,
143
+ * to the cards it answered with, so the answer is the one the board file gives.
144
+ *
145
+ * The server filters on a parent's id only, and an agent may name the parent
146
+ * the way it names any card — `WOR-001`, `WOR-1`, `1`. The id is asked first;
147
+ * when no card answers it, the board is read whole to learn which card the
148
+ * reference names, and a reference that names none is refused as `issue_not_found`.
149
+ */
150
+ async function cardPage(cardQuery) {
151
+ const columns = await readColumns();
152
+ const statusIds = cardQuery.columns === void 0 ? null : new Set(cardQuery.columns.map((column) => requireColumn(columns, column).id));
153
+ const filters = statusIds?.size === 1 ? { statusId: [...statusIds][0] } : {};
154
+ let parent = cardQuery.parent;
155
+ let rows = await listCards(parent === void 0 ? filters : {
156
+ ...filters,
157
+ parentIssueId: parent
158
+ });
159
+ if (parent !== void 0 && rows.length === 0) {
160
+ const board = await listCards({});
161
+ const named = findCardRow(board, parent);
162
+ if (!named) throw new BoardError("issue_not_found", { issueId: parent });
163
+ parent = named.id;
164
+ rows = board;
165
+ }
166
+ const matched = rows.filter((row) => matchesKanboCardQuery(row, {
167
+ ...cardQuery,
168
+ parent,
169
+ returned: void 0
170
+ }, statusIds));
171
+ const picked = cardQuery.returned === void 0 ? matched : (await withAttention(matched)).filter((row) => matchesKanboCardQuery(row, cardQuery, null));
172
+ const offset = cardQuery.offset ?? 0;
173
+ return {
174
+ cards: (await withAttention(picked.slice(offset, cardQuery.limit === void 0 ? void 0 : offset + cardQuery.limit))).map((row) => projectKanboCard(row, columns)),
175
+ total: picked.length,
176
+ offset
177
+ };
178
+ }
179
+ /**
180
+ * The cards with what a list says beside the ones a person has to look at or
181
+ * sent back — their comments read one card at a time, since the route lists
182
+ * none, and only for cards that are waiting, or neither waiting nor running.
183
+ */
184
+ async function withAttention(rows) {
185
+ return await Promise.all(rows.map(async (row) => {
186
+ if (row.attention || row.waitingFor !== "human" && row.activeRun !== null) return row;
187
+ const comments = z.array(CardCommentSchema).parse(await request("GET", `/issues/${encodeURIComponent(row.id)}/comments`));
188
+ const decision = comments.filter((comment) => comment.authorKind === "system.approved" || comment.authorKind === "system.returned").at(-1);
189
+ const returned = row.waitingFor !== "human" && decision?.authorKind === "system.returned";
190
+ const latest = comments.filter((comment) => comment.authorKind !== "system.run").at(-1);
191
+ const lastComment = (row.waitingFor === "human" || returned) && latest ? previewComment({
192
+ ...latest,
193
+ authorKind: latest.authorKind
194
+ }) : null;
195
+ return {
196
+ ...row,
197
+ attention: {
198
+ returned,
199
+ lastComment
200
+ }
201
+ };
202
+ }));
203
+ }
204
+ /** The ready cards, a page of them; the server answers the whole queue. */
205
+ async function readyPage({ returnedLimit, ...page }) {
206
+ const query = new URLSearchParams({ workspaceId: input.workspaceId() });
207
+ const cards = z.array(IssueSchema).parse(await request("GET", `/issues/ready?${query.toString()}`));
208
+ const columns = await readColumns();
209
+ const offset = page.offset ?? 0;
210
+ const ready = {
211
+ cards: cards.slice(offset, page.limit === void 0 ? void 0 : offset + page.limit).map((row) => projectKanboCard(row, columns)),
212
+ total: cards.length,
213
+ offset
214
+ };
215
+ return returnedLimit === void 0 ? ready : {
216
+ ...ready,
217
+ returned: await cardPage({
218
+ returned: true,
219
+ limit: returnedLimit
220
+ })
221
+ };
222
+ }
113
223
  return {
114
224
  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
- },
225
+ ready: async ({ limit }) => (await readyPage({ limit })).cards,
226
+ readyPage,
122
227
  columns: readColumns,
123
228
  sprints: async () => z.array(SprintSchema).parse(await request("GET", `/issues/sprints?workspaceId=${encodeURIComponent(input.workspaceId())}`)).map(projectKanboSprint),
124
- cardGet: async ({ card }) => {
229
+ cardGet: async ({ card, include = DEFAULT_KANBO_CARD_INCLUDES, commentLimit = 10 }) => {
125
230
  const found = IssueSchema.parse(await request("GET", `/issues/${encodeURIComponent(card)}`));
126
- const columns = await readColumns();
231
+ const wants = new Set(include);
232
+ const path = `/issues/${encodeURIComponent(found.id)}`;
127
233
  const query = new URLSearchParams({
128
234
  workspaceId: input.workspaceId(),
129
235
  parentIssueId: found.id
130
236
  });
131
- const subCards = z.array(IssueSchema).parse(await request("GET", `/issues/?${query.toString()}`));
237
+ const [columns, subCards, comments, runs, history, pullRequests] = await Promise.all([
238
+ readColumns(),
239
+ wants.has("subCards") ? request("GET", `/issues/?${query.toString()}`).then((rows) => z.array(IssueSchema).parse(rows)) : null,
240
+ wants.has("comments") ? request("GET", `${path}/comments`).then((rows) => z.array(CardCommentSchema).parse(rows)) : null,
241
+ wants.has("runs") ? request("GET", `${path}/runs`).then((rows) => z.array(RunSchema).parse(rows)) : null,
242
+ wants.has("history") ? request("GET", `${path}/field-changes`).then((rows) => z.array(FieldChangeSchema).parse(rows)) : null,
243
+ wants.has("prs") ? request("GET", `${path}/pull-requests`).then((rows) => z.array(PullRequestSchema).parse(rows)) : null
244
+ ]);
132
245
  return {
133
246
  ...projectKanboCard(found, columns),
134
- subCards: subCards.map((row) => projectKanboSubCard(row, columns))
247
+ ...subCards ? { subCards: subCards.map((row) => projectKanboSubCard(row, columns)) } : {},
248
+ ...comments ? {
249
+ comments: comments.slice(-commentLimit).map(projectKanboCardComment),
250
+ commentCount: comments.length
251
+ } : {},
252
+ ...runs ? { runs: runs.map(projectKanboRun) } : {},
253
+ ...history ? { history: history.map(projectKanboFieldChange) } : {},
254
+ ...pullRequests ? { pullRequests: pullRequests.map(projectKanboPullRequest) } : {}
135
255
  };
136
256
  },
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
- },
257
+ cardList: async ({ column, limit }) => (await cardPage({
258
+ columns: column ? [column] : void 0,
259
+ limit
260
+ })).cards,
261
+ cardPage,
143
262
  cardCreate: async ({ title, description, column, parent, executionMode }) => await projectOne(await request("POST", "/issues/", {
144
263
  workspaceId: input.workspaceId(),
145
264
  ...title !== void 0 ? { title } : {},
@@ -167,7 +286,18 @@ function createHttpTransport(input) {
167
286
  })))
168
287
  };
169
288
  }
289
+ /**
290
+ * The card a reference names among cards already read, the way the board file
291
+ * resolves one: its id, its number, or its key without the padding (`WOR-1`).
292
+ */
293
+ function findCardRow(rows, reference) {
294
+ const trimmed = reference.trim();
295
+ const byId = rows.find((row) => row.id === trimmed);
296
+ if (byId) return byId;
297
+ const key = /^(?:(.+)-)?(\d+)$/.exec(trimmed);
298
+ if (!key) return;
299
+ const [, prefix, digits] = key;
300
+ return rows.find((row) => row.number === Number(digits) && (prefix === void 0 || row.id.slice(0, row.id.lastIndexOf("-")).toUpperCase() === prefix.toUpperCase()));
301
+ }
170
302
  //#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
303
+ 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