kanbo-cli 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +1 -2
  2. package/dist/cli.cjs +96 -14
  3. package/dist/cli.cjs.map +1 -1
  4. package/dist/lib/{board-store-B16YarvX.d.mts → board-store-BfszzAJB.d.cts} +2 -2
  5. package/dist/lib/{board-store-BHHo_ZlR.d.cts → board-store-BvWn9xTB.d.mts} +2 -2
  6. package/dist/lib/index.cjs +2 -29
  7. package/dist/lib/index.cjs.map +1 -1
  8. package/dist/lib/index.d.cts +363 -363
  9. package/dist/lib/index.d.mts +363 -363
  10. package/dist/lib/index.mjs +1 -28
  11. package/dist/lib/index.mjs.map +1 -1
  12. package/dist/lib/{manifest-C0EcwwgO.mjs → manifest-DfOBqo-Q.mjs} +100 -17
  13. package/dist/lib/manifest-DfOBqo-Q.mjs.map +1 -0
  14. package/dist/lib/{manifest-CpZOcB_6.cjs → manifest-wdwFSquz.cjs} +104 -15
  15. package/dist/lib/manifest-wdwFSquz.cjs.map +1 -0
  16. package/dist/lib/mcp/index.cjs +1 -1
  17. package/dist/lib/mcp/index.d.cts +12 -12
  18. package/dist/lib/mcp/index.d.mts +12 -12
  19. package/dist/lib/mcp/index.mjs +1 -1
  20. package/dist/lib/postgres/index.d.cts +151 -151
  21. package/dist/lib/postgres/index.d.mts +151 -151
  22. package/dist/lib/{schema-fsHqNAip.d.cts → schema-D0MyXcej.d.cts} +41 -41
  23. package/dist/lib/{schema-fsHqNAip.d.mts → schema-D0MyXcej.d.mts} +41 -41
  24. package/dist/lib/sqlite/index.d.cts +12 -12
  25. package/dist/lib/sqlite/index.d.mts +12 -12
  26. package/dist/lib/sqlite/schema.d.cts +1 -1
  27. package/dist/lib/sqlite/schema.d.mts +1 -1
  28. package/dist/lib/{tool-names-9cmCn0gP.d.cts → tool-names-3q_GEZej.d.mts} +5 -5
  29. package/dist/lib/{tool-names-Dxg1UTUl.d.mts → tool-names-WfH4IURz.d.cts} +5 -5
  30. package/dist/page/board.js +1 -1
  31. package/package.json +4 -4
  32. package/dist/lib/manifest-C0EcwwgO.mjs.map +0 -1
  33. package/dist/lib/manifest-CpZOcB_6.cjs.map +0 -1
@@ -1,32 +1,5 @@
1
- import { $ as formatPlainActivityValue, A as formatExternalSessionRef, B as readChangeSeq, C as ignoreBoardFile, D as maskDatabaseUrl, E as writeBinding, F as UNMET_ENTRY_RULE_REASONS, G as diffFieldChanges, H as DEFAULT_STATUSES, I as evaluateEntryRules, J as activityKindOrder, K as BOARD_ACTOR, L as readChecklist, M as pullRequestFactKeys, N as ENTRY_RULES, O as maskDatabaseUrls, P as ENTRY_RULE_DESCRIPTIONS, Q as formatActivityIssueReference, R as readEntryRules, S as BINDING_FILE_PATH, T as readProjectBinding, U as normalizeStatusName, V as runBoardWrite, W as TRACKED_FIELDS, X as activityToken, Y as activityText, Z as commentAuthorReference, a as BOARD_ERROR_RESPONSES, at as toIssueView, b as DEFAULT_BOARD_FILE_PATH, ct as nextIssueNumber, et as isEmptyActivityValue, i as KANBO_TOOL_NAMES, it as normalizeCommentAuthorKind, j as parseExternalSessionRef, k as createBoardOps, lt as nextIssueOrder, n as buildCapabilitiesManifest, nt as readSystemCommentKind, ot as formatIssueId, q as currentUnixSeconds, r as KANBO_TOOL_CLI_EQUIVALENTS, rt as isOutsideWriter, st as nextIssueIdentity, t as CAPABILITIES_MANIFEST_VERSION, tt as parseActivityStringArray, ut as readIssuePrefix, w as readBinding, x as renderCapabilitiesMarkdown, z as serializeEntryRules } from "./manifest-C0EcwwgO.mjs";
1
+ import { $ as formatActivityIssueReference, A as createBoardOps, B as serializeEntryRules, C as BINDING_FILE_PATH, D as writeBinding, E as readProjectBinding, F as ENTRY_RULE_DESCRIPTIONS, G as TRACKED_FIELDS, H as runBoardWrite, I as UNMET_ENTRY_RULE_REASONS, J as currentUnixSeconds, K as diffFieldChanges, L as evaluateEntryRules, M as parseExternalSessionRef, N as pullRequestFactKeys, O as maskDatabaseUrl, P as ENTRY_RULES, Q as commentAuthorReference, R as readChecklist, S as cardDisplayTitle, T as readBinding, U as DEFAULT_STATUSES, V as readChangeSeq, W as normalizeStatusName, X as activityText, Y as activityKindOrder, Z as activityToken, a as BOARD_ERROR_RESPONSES, at as normalizeCommentAuthorKind, b as DEFAULT_BOARD_FILE_PATH, ct as nextIssueIdentity, dt as readIssuePrefix, et as formatPlainActivityValue, i as KANBO_TOOL_NAMES, it as isOutsideWriter, j as formatExternalSessionRef, k as maskDatabaseUrls, lt as nextIssueNumber, n as buildCapabilitiesManifest, nt as parseActivityStringArray, ot as toIssueView, q as BOARD_ACTOR, r as KANBO_TOOL_CLI_EQUIVALENTS, rt as readSystemCommentKind, st as formatIssueId, t as CAPABILITIES_MANIFEST_VERSION, tt as isEmptyActivityValue, ut as nextIssueOrder, w as ignoreBoardFile, x as renderCapabilitiesMarkdown, z as readEntryRules } from "./manifest-DfOBqo-Q.mjs";
2
2
  import { a as BoardError } from "./migrations-path-Ciwan537.mjs";
3
- //#region src/domain/card-display-title.ts
4
- /**
5
- * What a card is called wherever a person or an agent reads it by name.
6
- *
7
- * A card written as a description alone is titled with its own key (every
8
- * writer does that). Named by its key, such a card says nothing — so its name
9
- * is the first line of its description instead. A browser bundle that does not
10
- * load this package keeps the same rule on its own, tested with the same
11
- * examples.
12
- */
13
- /** How long a description line may run before it is cut with an ellipsis. */
14
- const DESCRIPTION_LINE_LIMIT = 120;
15
- function cardDisplayTitle(card) {
16
- if (card.title !== card.id) return card.title;
17
- return descriptionFirstLine(card.description) ?? card.title;
18
- }
19
- function descriptionFirstLine(description) {
20
- for (const rawLine of (description ?? "").split("\n")) {
21
- const line = stripMarkdown(rawLine);
22
- if (line) return line.length > DESCRIPTION_LINE_LIMIT ? `${line.slice(0, 119).trimEnd()}…` : line;
23
- }
24
- return null;
25
- }
26
- function stripMarkdown(line) {
27
- return line.replace(/^\s*(?:>\s*)+/, "").replace(/^\s*#{1,6}\s+/, "").replace(/^\s*(?:[-*+]|\d+[.)])\s+(?:\[[ x]\]\s+)?/i, "").replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/<[^>]+>/g, "").replace(/[*_~`]/g, "").replace(/\s+/g, " ").trim();
28
- }
29
- //#endregion
30
3
  //#region src/ops/board-copy.ts
31
4
  /** Nothing copied — what a move that had nothing to move reports. */
32
5
  function emptyCopyCounts() {
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/domain/card-display-title.ts","../../src/ops/board-copy.ts"],"sourcesContent":["/**\n * What a card is called wherever a person or an agent reads it by name.\n *\n * A card written as a description alone is titled with its own key (every\n * writer does that). Named by its key, such a card says nothing — so its name\n * is the first line of its description instead. A browser bundle that does not\n * load this package keeps the same rule on its own, tested with the same\n * examples.\n */\n\n/** How long a description line may run before it is cut with an ellipsis. */\nconst DESCRIPTION_LINE_LIMIT = 120\n\nexport function cardDisplayTitle(card: { id: string, title: string, description?: string | null }): string {\n if (card.title !== card.id) {\n return card.title\n }\n return descriptionFirstLine(card.description) ?? card.title\n}\n\nfunction descriptionFirstLine(description: string | null | undefined): string | null {\n for (const rawLine of (description ?? '').split('\\n')) {\n const line = stripMarkdown(rawLine)\n if (line) {\n return line.length > DESCRIPTION_LINE_LIMIT ? `${line.slice(0, DESCRIPTION_LINE_LIMIT - 1).trimEnd()}…` : line\n }\n }\n return null\n}\n\nfunction stripMarkdown(line: string): string {\n return line\n .replace(/^\\s*(?:>\\s*)+/, '')\n .replace(/^\\s*#{1,6}\\s+/, '')\n .replace(/^\\s*(?:[-*+]|\\d+[.)])\\s+(?:\\[[ x]\\]\\s+)?/i, '')\n .replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n .replace(/<[^>]+>/g, '')\n .replace(/[*_~`]/g, '')\n .replace(/\\s+/g, ' ')\n .trim()\n}\n","import type { BoardStore } from '../board-store'\nimport { BoardError } from '../domain/errors'\nimport { currentUnixSeconds } from '../domain/time'\nimport type {\n Issue,\n IssueComment,\n IssueFieldChange,\n IssueMilestone,\n IssuePullRequest,\n IssueRelation,\n IssueRun,\n IssueStatus,\n} from '../sqlite/schema'\n\n/** How many rows of each kind a copy wrote into the target board. */\nexport interface BoardCopyCounts {\n statuses: number\n milestones: number\n issues: number\n comments: number\n runs: number\n relations: number\n fieldChanges: number\n /** Cards that carried refs, not rows: context refs are a column on the card. */\n contextRefs: number\n pullRequests: number\n}\n\n/** Everything one workspace has on a board, read before anything is written. */\nexport interface BoardSlice {\n statuses: IssueStatus[]\n milestones: IssueMilestone[]\n /** In an order where a card's parent always comes before it. */\n issues: Issue[]\n comments: IssueComment[]\n runs: IssueRun[]\n relations: IssueRelation[]\n fieldChanges: IssueFieldChange[]\n pullRequests: IssuePullRequest[]\n /** What reading it had to leave behind, said to a person. */\n warnings: string[]\n}\n\n/** Nothing copied — what a move that had nothing to move reports. */\nexport function emptyCopyCounts(): BoardCopyCounts {\n return { statuses: 0, milestones: 0, issues: 0, comments: 0, runs: 0, relations: 0, fieldChanges: 0, contextRefs: 0, pullRequests: 0 }\n}\n\n/**\n * Everything the workspace has on this board, read in one pass before a single\n * row is written anywhere.\n *\n * Links are collected by id because an edge is listed by both of the cards it\n * touches, and one reaching a card outside this workspace is left behind rather\n * than copied: the target holds this workspace's board and nothing else, so the\n * far end of that edge is not there to point at.\n */\nexport async function readBoardSlice(from: BoardStore, workspaceId: string): Promise<BoardSlice> {\n const warnings: string[] = []\n const statuses = await from.statuses.listByWorkspace(workspaceId)\n const milestones = await from.milestones.listByWorkspace(workspaceId)\n const issues = orderParentsFirst(await from.issues.listByWorkspace(workspaceId))\n const issueIds = new Set(issues.map(issue => issue.id))\n\n const comments: IssueComment[] = []\n const runs: IssueRun[] = []\n const relations = new Map<string, IssueRelation>()\n const fieldChanges: IssueFieldChange[] = []\n const pullRequests: IssuePullRequest[] = []\n const skippedRelations = new Set<string>()\n for (const issue of issues) {\n // The store's own order — insertion order on either engine, for runs, for\n // comments and for field changes alike — is the order they are written back\n // in. That is what keeps an attempt number, and the last word on a card,\n // reading the same after the move as before it.\n comments.push(...await from.comments.listByIssue(issue.id))\n runs.push(...await from.runs.listByIssue(issue.id))\n for (const relation of await from.relations.listByIssue(issue.id)) {\n if (issueIds.has(relation.sourceIssueId) && issueIds.has(relation.targetIssueId)) {\n relations.set(relation.id, relation)\n continue\n }\n skippedRelations.add(relation.id)\n }\n fieldChanges.push(...await from.fieldChanges.listByIssue(issue.id))\n pullRequests.push(...await from.pullRequests.listByIssue(issue.id))\n }\n if (skippedRelations.size > 0) {\n warnings.push(`${skippedRelations.size} links to cards in other workspaces were not moved`)\n }\n\n return { statuses, milestones, issues, comments, runs, relations: [...relations.values()], fieldChanges, pullRequests, warnings }\n}\n\n/**\n * Write a slice into the target board, in one transaction, and say what was\n * written and what a person should know about it — the slice's own warnings\n * first.\n *\n * It is idempotent by id. A row the target already has is left exactly as it\n * is and counted as already present, so a copy interrupted half-way is finished\n * by running it again, and a second run of a finished one copies nothing. A\n * clash that is not the same row — a column name or a card number the target\n * holds under another id — refuses the whole copy before its first insert\n * (`board_copy_conflict`, ruling 3-55).\n */\nexport async function writeBoardSlice(\n to: BoardStore,\n slice: BoardSlice,\n workspaceId: string,\n): Promise<{ copied: BoardCopyCounts, warnings: string[] }> {\n const warnings = [...slice.warnings]\n const copied = emptyCopyCounts()\n let alreadyPresent = 0\n\n await to.transaction(async (tx) => {\n await assertTheCopyFits(tx, slice, workspaceId)\n\n for (const status of slice.statuses) {\n if (await tx.statuses.findById(status.id)) {\n alreadyPresent += 1\n continue\n }\n await tx.statuses.create(status)\n copied.statuses += 1\n }\n\n for (const milestone of slice.milestones) {\n if (await tx.milestones.findById(milestone.id)) {\n alreadyPresent += 1\n continue\n }\n await tx.milestones.create(milestone)\n copied.milestones += 1\n }\n\n const movingIssueIds = new Set(slice.issues.map(issue => issue.id))\n for (const issue of slice.issues) {\n if (await tx.issues.findById(issue.id)) {\n alreadyPresent += 1\n continue\n }\n // A card whose parent is neither moving with it nor already on the target\n // would point at a row that is not there. The board's own rule is that a\n // parent which goes away leaves its children parentless, so that is what\n // happens here too — said out loud, because a person is losing a link.\n const parentIsReachable = issue.parentIssueId === null\n || movingIssueIds.has(issue.parentIssueId)\n || (await tx.issues.findById(issue.parentIssueId)) !== null\n if (!parentIsReachable) {\n warnings.push(`${issue.id} lost its parent card, which is not on this board`)\n }\n const { contextRefs, ...row } = issue\n await tx.issues.create({ ...row, parentIssueId: parentIsReachable ? issue.parentIssueId : null })\n copied.issues += 1\n if (contextRefs !== '[]') {\n await tx.contextRefs.replace(issue.id, contextRefs, issue.updatedAt)\n copied.contextRefs += 1\n }\n }\n\n for (const comment of slice.comments) {\n if (await tx.comments.findById(comment.id)) {\n alreadyPresent += 1\n continue\n }\n await tx.comments.create(comment)\n copied.comments += 1\n }\n\n for (const run of slice.runs) {\n if (await tx.runs.findById(run.id)) {\n alreadyPresent += 1\n continue\n }\n await tx.runs.create(run)\n copied.runs += 1\n }\n\n for (const relation of slice.relations) {\n if (await tx.relations.findById(relation.id)) {\n alreadyPresent += 1\n continue\n }\n await tx.relations.create(relation)\n copied.relations += 1\n }\n\n // Field changes carry no lookup by id of their own — they are read a card\n // at a time — so what the target already has is read a card at a time too.\n const changesByIssue = new Map<string, IssueFieldChange[]>()\n for (const change of slice.fieldChanges) {\n changesByIssue.set(change.issueId, [...(changesByIssue.get(change.issueId) ?? []), change])\n }\n for (const [issueId, changes] of changesByIssue) {\n const present = new Set((await tx.fieldChanges.listByIssue(issueId)).map(change => change.id))\n for (const change of changes) {\n if (present.has(change.id)) {\n alreadyPresent += 1\n continue\n }\n await tx.fieldChanges.create(change)\n copied.fieldChanges += 1\n }\n }\n\n // Pull request links, like field changes, are read a card at a time. One\n // the target holds under another id names the same pull request — `link`\n // hands back that standing row — so it is already present too.\n const linksByIssue = new Map<string, IssuePullRequest[]>()\n for (const link of slice.pullRequests) {\n linksByIssue.set(link.issueId, [...(linksByIssue.get(link.issueId) ?? []), link])\n }\n for (const [issueId, links] of linksByIssue) {\n const present = new Set((await tx.pullRequests.listByIssue(issueId)).map(link => link.id))\n for (const link of links) {\n if (present.has(link.id) || (await tx.pullRequests.link(link)).id !== link.id) {\n alreadyPresent += 1\n continue\n }\n copied.pullRequests += 1\n }\n }\n\n // One bump for the whole copy: a reader watching the target refetches once,\n // not once per card.\n await tx.meta.bumpRevision('board', currentUnixSeconds())\n })\n\n if (alreadyPresent > 0) {\n warnings.push(`${alreadyPresent} rows were already present on the target board and were left as they are`)\n }\n return { copied, warnings }\n}\n\n/**\n * Empty the slice off the board it was read from, in a transaction of its own.\n *\n * Only the cards, the columns and the milestones are named: comments, runs,\n * links, pull request links and the field-change history hang off a card by a\n * foreign key that cascades on both engines, so deleting the card takes them\n * with it.\n */\nexport async function deleteBoardSlice(from: BoardStore, slice: BoardSlice): Promise<void> {\n await from.transaction(async (tx) => {\n for (const issue of slice.issues) {\n await tx.issues.delete(issue.id)\n }\n for (const milestone of slice.milestones) {\n await tx.milestones.delete(milestone.id)\n }\n for (const status of slice.statuses) {\n await tx.statuses.delete(status.id)\n }\n await tx.meta.bumpRevision('board', currentUnixSeconds())\n })\n}\n\n/**\n * What a copy would collide with on the target, said before a row is written.\n *\n * The board carries two uniqueness rules that a *second* board of the same\n * workspace can break: a column's name within its workspace, and a card's\n * number within it. In practice it is the columns that collide: a standard set\n * created a second time carries the same names under new ids, while a card's id\n * is its key (`BST-001`), so two generations of card *number* 1 are two rows\n * with one id — which the copy treats as already present rather than as a\n * clash. The number check covers the case where they are not: a workspace whose\n * identifier changed between the two generations numbers from 1 again under\n * different ids.\n *\n * A row the target already has *by id* is not a collision — it is the\n * idempotent half of the copy, which is why the holder's id is compared rather\n * than merely counted. Milestones need no check: neither engine makes their\n * titles unique.\n */\nasync function assertTheCopyFits(tx: BoardStore, slice: BoardSlice, workspaceId: string): Promise<void> {\n const conflicts: { kind: 'card' | 'column', value: string | number, heldBy: string }[] = []\n\n const numbers = new Map((await tx.issues.listByWorkspace(workspaceId)).map(issue => [issue.number, issue.id]))\n for (const issue of slice.issues) {\n const holder = numbers.get(issue.number)\n if (holder !== undefined && holder !== issue.id) {\n conflicts.push({ kind: 'card', value: issue.number, heldBy: holder })\n }\n }\n\n const names = new Map((await tx.statuses.listByWorkspace(workspaceId)).map(status => [status.name, status.id]))\n for (const status of slice.statuses) {\n const holder = names.get(status.name)\n if (holder !== undefined && holder !== status.id) {\n conflicts.push({ kind: 'column', value: status.name, heldBy: holder })\n }\n }\n\n if (conflicts.length > 0) {\n throw new BoardError('board_copy_conflict', { workspaceId, conflicts })\n }\n}\n\n/**\n * The same cards, ordered so that a parent is always written before its\n * children. The column is a foreign key onto the same table, so any other order\n * writes a card pointing at one that is not there yet.\n */\nfunction orderParentsFirst(issues: Issue[]): Issue[] {\n const byId = new Map(issues.map(issue => [issue.id, issue]))\n const ordered: Issue[] = []\n const placed = new Set<string>()\n\n function place(issue: Issue, ancestors: Set<string>): void {\n if (placed.has(issue.id) || ancestors.has(issue.id)) {\n return\n }\n ancestors.add(issue.id)\n const parent = issue.parentIssueId === null ? undefined : byId.get(issue.parentIssueId)\n if (parent) {\n place(parent, ancestors)\n }\n placed.add(issue.id)\n ordered.push(issue)\n }\n\n for (const issue of issues) {\n place(issue, new Set())\n }\n return ordered\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/ops/board-copy.ts"],"sourcesContent":["import type { BoardStore } from '../board-store'\nimport { BoardError } from '../domain/errors'\nimport { currentUnixSeconds } from '../domain/time'\nimport type {\n Issue,\n IssueComment,\n IssueFieldChange,\n IssueMilestone,\n IssuePullRequest,\n IssueRelation,\n IssueRun,\n IssueStatus,\n} from '../sqlite/schema'\n\n/** How many rows of each kind a copy wrote into the target board. */\nexport interface BoardCopyCounts {\n statuses: number\n milestones: number\n issues: number\n comments: number\n runs: number\n relations: number\n fieldChanges: number\n /** Cards that carried refs, not rows: context refs are a column on the card. */\n contextRefs: number\n pullRequests: number\n}\n\n/** Everything one workspace has on a board, read before anything is written. */\nexport interface BoardSlice {\n statuses: IssueStatus[]\n milestones: IssueMilestone[]\n /** In an order where a card's parent always comes before it. */\n issues: Issue[]\n comments: IssueComment[]\n runs: IssueRun[]\n relations: IssueRelation[]\n fieldChanges: IssueFieldChange[]\n pullRequests: IssuePullRequest[]\n /** What reading it had to leave behind, said to a person. */\n warnings: string[]\n}\n\n/** Nothing copied — what a move that had nothing to move reports. */\nexport function emptyCopyCounts(): BoardCopyCounts {\n return { statuses: 0, milestones: 0, issues: 0, comments: 0, runs: 0, relations: 0, fieldChanges: 0, contextRefs: 0, pullRequests: 0 }\n}\n\n/**\n * Everything the workspace has on this board, read in one pass before a single\n * row is written anywhere.\n *\n * Links are collected by id because an edge is listed by both of the cards it\n * touches, and one reaching a card outside this workspace is left behind rather\n * than copied: the target holds this workspace's board and nothing else, so the\n * far end of that edge is not there to point at.\n */\nexport async function readBoardSlice(from: BoardStore, workspaceId: string): Promise<BoardSlice> {\n const warnings: string[] = []\n const statuses = await from.statuses.listByWorkspace(workspaceId)\n const milestones = await from.milestones.listByWorkspace(workspaceId)\n const issues = orderParentsFirst(await from.issues.listByWorkspace(workspaceId))\n const issueIds = new Set(issues.map(issue => issue.id))\n\n const comments: IssueComment[] = []\n const runs: IssueRun[] = []\n const relations = new Map<string, IssueRelation>()\n const fieldChanges: IssueFieldChange[] = []\n const pullRequests: IssuePullRequest[] = []\n const skippedRelations = new Set<string>()\n for (const issue of issues) {\n // The store's own order — insertion order on either engine, for runs, for\n // comments and for field changes alike — is the order they are written back\n // in. That is what keeps an attempt number, and the last word on a card,\n // reading the same after the move as before it.\n comments.push(...await from.comments.listByIssue(issue.id))\n runs.push(...await from.runs.listByIssue(issue.id))\n for (const relation of await from.relations.listByIssue(issue.id)) {\n if (issueIds.has(relation.sourceIssueId) && issueIds.has(relation.targetIssueId)) {\n relations.set(relation.id, relation)\n continue\n }\n skippedRelations.add(relation.id)\n }\n fieldChanges.push(...await from.fieldChanges.listByIssue(issue.id))\n pullRequests.push(...await from.pullRequests.listByIssue(issue.id))\n }\n if (skippedRelations.size > 0) {\n warnings.push(`${skippedRelations.size} links to cards in other workspaces were not moved`)\n }\n\n return { statuses, milestones, issues, comments, runs, relations: [...relations.values()], fieldChanges, pullRequests, warnings }\n}\n\n/**\n * Write a slice into the target board, in one transaction, and say what was\n * written and what a person should know about it — the slice's own warnings\n * first.\n *\n * It is idempotent by id. A row the target already has is left exactly as it\n * is and counted as already present, so a copy interrupted half-way is finished\n * by running it again, and a second run of a finished one copies nothing. A\n * clash that is not the same row — a column name or a card number the target\n * holds under another id — refuses the whole copy before its first insert\n * (`board_copy_conflict`, ruling 3-55).\n */\nexport async function writeBoardSlice(\n to: BoardStore,\n slice: BoardSlice,\n workspaceId: string,\n): Promise<{ copied: BoardCopyCounts, warnings: string[] }> {\n const warnings = [...slice.warnings]\n const copied = emptyCopyCounts()\n let alreadyPresent = 0\n\n await to.transaction(async (tx) => {\n await assertTheCopyFits(tx, slice, workspaceId)\n\n for (const status of slice.statuses) {\n if (await tx.statuses.findById(status.id)) {\n alreadyPresent += 1\n continue\n }\n await tx.statuses.create(status)\n copied.statuses += 1\n }\n\n for (const milestone of slice.milestones) {\n if (await tx.milestones.findById(milestone.id)) {\n alreadyPresent += 1\n continue\n }\n await tx.milestones.create(milestone)\n copied.milestones += 1\n }\n\n const movingIssueIds = new Set(slice.issues.map(issue => issue.id))\n for (const issue of slice.issues) {\n if (await tx.issues.findById(issue.id)) {\n alreadyPresent += 1\n continue\n }\n // A card whose parent is neither moving with it nor already on the target\n // would point at a row that is not there. The board's own rule is that a\n // parent which goes away leaves its children parentless, so that is what\n // happens here too — said out loud, because a person is losing a link.\n const parentIsReachable = issue.parentIssueId === null\n || movingIssueIds.has(issue.parentIssueId)\n || (await tx.issues.findById(issue.parentIssueId)) !== null\n if (!parentIsReachable) {\n warnings.push(`${issue.id} lost its parent card, which is not on this board`)\n }\n const { contextRefs, ...row } = issue\n await tx.issues.create({ ...row, parentIssueId: parentIsReachable ? issue.parentIssueId : null })\n copied.issues += 1\n if (contextRefs !== '[]') {\n await tx.contextRefs.replace(issue.id, contextRefs, issue.updatedAt)\n copied.contextRefs += 1\n }\n }\n\n for (const comment of slice.comments) {\n if (await tx.comments.findById(comment.id)) {\n alreadyPresent += 1\n continue\n }\n await tx.comments.create(comment)\n copied.comments += 1\n }\n\n for (const run of slice.runs) {\n if (await tx.runs.findById(run.id)) {\n alreadyPresent += 1\n continue\n }\n await tx.runs.create(run)\n copied.runs += 1\n }\n\n for (const relation of slice.relations) {\n if (await tx.relations.findById(relation.id)) {\n alreadyPresent += 1\n continue\n }\n await tx.relations.create(relation)\n copied.relations += 1\n }\n\n // Field changes carry no lookup by id of their own — they are read a card\n // at a time — so what the target already has is read a card at a time too.\n const changesByIssue = new Map<string, IssueFieldChange[]>()\n for (const change of slice.fieldChanges) {\n changesByIssue.set(change.issueId, [...(changesByIssue.get(change.issueId) ?? []), change])\n }\n for (const [issueId, changes] of changesByIssue) {\n const present = new Set((await tx.fieldChanges.listByIssue(issueId)).map(change => change.id))\n for (const change of changes) {\n if (present.has(change.id)) {\n alreadyPresent += 1\n continue\n }\n await tx.fieldChanges.create(change)\n copied.fieldChanges += 1\n }\n }\n\n // Pull request links, like field changes, are read a card at a time. One\n // the target holds under another id names the same pull request — `link`\n // hands back that standing row — so it is already present too.\n const linksByIssue = new Map<string, IssuePullRequest[]>()\n for (const link of slice.pullRequests) {\n linksByIssue.set(link.issueId, [...(linksByIssue.get(link.issueId) ?? []), link])\n }\n for (const [issueId, links] of linksByIssue) {\n const present = new Set((await tx.pullRequests.listByIssue(issueId)).map(link => link.id))\n for (const link of links) {\n if (present.has(link.id) || (await tx.pullRequests.link(link)).id !== link.id) {\n alreadyPresent += 1\n continue\n }\n copied.pullRequests += 1\n }\n }\n\n // One bump for the whole copy: a reader watching the target refetches once,\n // not once per card.\n await tx.meta.bumpRevision('board', currentUnixSeconds())\n })\n\n if (alreadyPresent > 0) {\n warnings.push(`${alreadyPresent} rows were already present on the target board and were left as they are`)\n }\n return { copied, warnings }\n}\n\n/**\n * Empty the slice off the board it was read from, in a transaction of its own.\n *\n * Only the cards, the columns and the milestones are named: comments, runs,\n * links, pull request links and the field-change history hang off a card by a\n * foreign key that cascades on both engines, so deleting the card takes them\n * with it.\n */\nexport async function deleteBoardSlice(from: BoardStore, slice: BoardSlice): Promise<void> {\n await from.transaction(async (tx) => {\n for (const issue of slice.issues) {\n await tx.issues.delete(issue.id)\n }\n for (const milestone of slice.milestones) {\n await tx.milestones.delete(milestone.id)\n }\n for (const status of slice.statuses) {\n await tx.statuses.delete(status.id)\n }\n await tx.meta.bumpRevision('board', currentUnixSeconds())\n })\n}\n\n/**\n * What a copy would collide with on the target, said before a row is written.\n *\n * The board carries two uniqueness rules that a *second* board of the same\n * workspace can break: a column's name within its workspace, and a card's\n * number within it. In practice it is the columns that collide: a standard set\n * created a second time carries the same names under new ids, while a card's id\n * is its key (`BST-001`), so two generations of card *number* 1 are two rows\n * with one id — which the copy treats as already present rather than as a\n * clash. The number check covers the case where they are not: a workspace whose\n * identifier changed between the two generations numbers from 1 again under\n * different ids.\n *\n * A row the target already has *by id* is not a collision — it is the\n * idempotent half of the copy, which is why the holder's id is compared rather\n * than merely counted. Milestones need no check: neither engine makes their\n * titles unique.\n */\nasync function assertTheCopyFits(tx: BoardStore, slice: BoardSlice, workspaceId: string): Promise<void> {\n const conflicts: { kind: 'card' | 'column', value: string | number, heldBy: string }[] = []\n\n const numbers = new Map((await tx.issues.listByWorkspace(workspaceId)).map(issue => [issue.number, issue.id]))\n for (const issue of slice.issues) {\n const holder = numbers.get(issue.number)\n if (holder !== undefined && holder !== issue.id) {\n conflicts.push({ kind: 'card', value: issue.number, heldBy: holder })\n }\n }\n\n const names = new Map((await tx.statuses.listByWorkspace(workspaceId)).map(status => [status.name, status.id]))\n for (const status of slice.statuses) {\n const holder = names.get(status.name)\n if (holder !== undefined && holder !== status.id) {\n conflicts.push({ kind: 'column', value: status.name, heldBy: holder })\n }\n }\n\n if (conflicts.length > 0) {\n throw new BoardError('board_copy_conflict', { workspaceId, conflicts })\n }\n}\n\n/**\n * The same cards, ordered so that a parent is always written before its\n * children. The column is a foreign key onto the same table, so any other order\n * writes a card pointing at one that is not there yet.\n */\nfunction orderParentsFirst(issues: Issue[]): Issue[] {\n const byId = new Map(issues.map(issue => [issue.id, issue]))\n const ordered: Issue[] = []\n const placed = new Set<string>()\n\n function place(issue: Issue, ancestors: Set<string>): void {\n if (placed.has(issue.id) || ancestors.has(issue.id)) {\n return\n }\n ancestors.add(issue.id)\n const parent = issue.parentIssueId === null ? undefined : byId.get(issue.parentIssueId)\n if (parent) {\n place(parent, ancestors)\n }\n placed.add(issue.id)\n ordered.push(issue)\n }\n\n for (const issue of issues) {\n place(issue, new Set())\n }\n return ordered\n}\n"],"mappings":"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"}
@@ -4,7 +4,7 @@ import { a as openBoardDatabase, c as createSqliteBoardStore, i as markBoardFile
4
4
  import { Command } from "commander";
5
5
  import { z } from "zod";
6
6
  import { hostname, userInfo } from "node:os";
7
- import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
7
+ import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
8
8
  import { sql } from "drizzle-orm";
9
9
  import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
10
10
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -14,6 +14,7 @@ import postgres from "postgres";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
16
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
+ import { spawn } from "node:child_process";
17
18
  import { createServer } from "node:http";
18
19
  /**
19
20
  * Every exit code this tool promises, and what it means — the capabilities
@@ -3653,6 +3654,33 @@ function readCardNumber(session, reference) {
3653
3654
  return reference.slice(0, separator).toUpperCase() === prefix ? Number(digits) : null;
3654
3655
  }
3655
3656
  //#endregion
3657
+ //#region src/domain/card-display-title.ts
3658
+ /**
3659
+ * What a card is called wherever a person or an agent reads it by name.
3660
+ *
3661
+ * A card written as a description alone is titled with its own key (every
3662
+ * writer does that). Named by its key, such a card says nothing — so its name
3663
+ * is the first line of its description instead. A browser bundle that does not
3664
+ * load this package keeps the same rule on its own, tested with the same
3665
+ * examples.
3666
+ */
3667
+ /** How long a description line may run before it is cut with an ellipsis. */
3668
+ const DESCRIPTION_LINE_LIMIT = 120;
3669
+ function cardDisplayTitle(card) {
3670
+ if (card.title !== card.id) return card.title;
3671
+ return descriptionFirstLine(card.description) ?? card.title;
3672
+ }
3673
+ function descriptionFirstLine(description) {
3674
+ for (const rawLine of (description ?? "").split("\n")) {
3675
+ const line = stripMarkdown(rawLine);
3676
+ if (line) return line.length > DESCRIPTION_LINE_LIMIT ? `${line.slice(0, 119).trimEnd()}…` : line;
3677
+ }
3678
+ return null;
3679
+ }
3680
+ function stripMarkdown(line) {
3681
+ return line.replace(/^\s*(?:>\s*)+/, "").replace(/^\s*#{1,6}\s+/, "").replace(/^\s*(?:[-*+]|\d+[.)])\s+(?:\[[ x]\]\s+)?/i, "").replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/<[^>]+>/g, "").replace(/[*_~`]/g, "").replace(/\s+/g, " ").trim();
3682
+ }
3683
+ //#endregion
3656
3684
  //#region src/cli/view.ts
3657
3685
  /** Every field `kanbo card list|get --json` may ask for, in listing order. */
3658
3686
  const CARD_VIEW_FIELDS = [
@@ -3769,16 +3797,16 @@ function projectRun(run) {
3769
3797
  launchedByKind: run.launchedByKind
3770
3798
  };
3771
3799
  }
3772
- /** One card per line: the key, the column it is in, and what it is called. */
3800
+ /** One card per line: the key, the column it is in, and what it is called (`cardDisplayTitle`). */
3773
3801
  function describeCards(cards) {
3774
3802
  if (cards.length === 0) return "No cards";
3775
3803
  const keyWidth = Math.max(...cards.map((card) => card.id.length));
3776
3804
  const columnWidth = Math.max(...cards.map((card) => (card.column ?? "—").length));
3777
- return cards.map((card) => `${card.id.padEnd(keyWidth)} ${(card.column ?? "—").padEnd(columnWidth)} ${card.title}`).join("\n");
3805
+ return cards.map((card) => `${card.id.padEnd(keyWidth)} ${(card.column ?? "—").padEnd(columnWidth)} ${cardDisplayTitle(card)}`).join("\n");
3778
3806
  }
3779
3807
  /** The whole card, for a person who asked about exactly one. */
3780
3808
  function describeCard(card) {
3781
- const lines = [`${card.id} ${card.column ?? "—"} ${card.executionMode}`, card.title];
3809
+ const lines = [`${card.id} ${card.column ?? "—"} ${card.executionMode}`, cardDisplayTitle(card)];
3782
3810
  if (card.description) lines.push("", card.description);
3783
3811
  const facts = [
3784
3812
  card.statusLine ? `status line: ${card.statusLine}` : null,
@@ -5112,7 +5140,7 @@ dirname(fileURLToPath(import.meta.url));
5112
5140
  * `package.version`. A build carries it as a literal; running from source
5113
5141
  * (tests, `tsx`) reads the package's own `package.json` instead.
5114
5142
  */
5115
- const KANBO_PACKAGE_VERSION = "0.1.0";
5143
+ const KANBO_PACKAGE_VERSION = "0.1.1";
5116
5144
  //#endregion
5117
5145
  //#region src/mcp/resources.ts
5118
5146
  /** The URI a client reads the manifest as JSON from. */
@@ -7316,12 +7344,11 @@ async function startKanboServer(options) {
7316
7344
  }
7317
7345
  /**
7318
7346
  * Who may act for a person on this server, said once when it starts: only a
7319
- * request presenting the token does, and a server with no token, or started
7320
- * from an agent's shell, has nobody who may.
7347
+ * request presenting the token does, and a server started from an agent's
7348
+ * shell has nobody who may.
7321
7349
  */
7322
- function describePersonRights(actors, token) {
7350
+ function describePersonRights(actors) {
7323
7351
  if (!actors.person) return "no request acts for a person (started from an agent's shell)";
7324
- if (!token) return "no request acts for a person (no token; set KANBO_SERVE_TOKEN to allow it)";
7325
7352
  return `a request with the token acts for ${actors.person.id}`;
7326
7353
  }
7327
7354
  function stripBrackets(host) {
@@ -7503,12 +7530,58 @@ function sendJson(response, status, value) {
7503
7530
  response.end(body);
7504
7531
  }
7505
7532
  //#endregion
7533
+ //#region src/serve/access.ts
7534
+ /** The token given, or one made up for a loopback server; refused off loopback without one. */
7535
+ function resolveServeAccess(host, givenToken) {
7536
+ assertServeBinding(host, givenToken);
7537
+ if (givenToken) return {
7538
+ token: givenToken,
7539
+ generated: false
7540
+ };
7541
+ return {
7542
+ token: randomBytes(24).toString("base64url"),
7543
+ generated: true
7544
+ };
7545
+ }
7546
+ /**
7547
+ * Hand the person the board page: a link printed once, and opened in the
7548
+ * browser unless they said not to. A generated token rides in the link's
7549
+ * fragment; one they gave is theirs already and is left out.
7550
+ */
7551
+ function presentServePage(presentation, output) {
7552
+ const { url, access, open } = presentation;
7553
+ if (presentation.agentShell || !presentation.terminal) {
7554
+ if (access.generated) output.print("kanbo serve: a token was generated for this run and is not shown here; set KANBO_SERVE_TOKEN to choose your own");
7555
+ return;
7556
+ }
7557
+ const link = access.generated ? `${url}/#token=${access.token}` : `${url}/`;
7558
+ output.print(`kanbo serve: board page ${link}`);
7559
+ if (open) output.openInBrowser(link);
7560
+ }
7561
+ /** Open a link in the default browser, without waiting for it and without failing when there is none. */
7562
+ function openInBrowser(url) {
7563
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", [
7564
+ "/c",
7565
+ "start",
7566
+ "\"\"",
7567
+ url
7568
+ ]] : ["xdg-open", [url]];
7569
+ try {
7570
+ const child = spawn(command, args, {
7571
+ detached: true,
7572
+ stdio: "ignore",
7573
+ windowsVerbatimArguments: process.platform === "win32"
7574
+ });
7575
+ child.on("error", () => {});
7576
+ child.unref();
7577
+ } catch {}
7578
+ }
7579
+ //#endregion
7506
7580
  //#region src/cli/commands/serve.ts
7507
7581
  function registerServeCommand(program) {
7508
- program.command("serve").description("serve this board over HTTP, with a board page for the browser").option("--port <port>", "port to listen on", String(DEFAULT_SERVE_PORT)).option("--host <host>", "address to bind; anything but loopback needs a token", DEFAULT_SERVE_HOST).option("--db <path>", "board database file to open").option("--database-url <url>", "external Postgres board to serve instead of a board file").option("--workspace <nameOrId>", "workspace the server is about").option("--token <token>", "bearer token every request must carry (or set KANBO_SERVE_TOKEN)").option("--cors-origin <origin>", "let a browser call from this exact origin; repeat for more", collect, []).action(async (options) => {
7582
+ program.command("serve").description("serve this board over HTTP, with a board page for the browser").option("--port <port>", "port to listen on", String(DEFAULT_SERVE_PORT)).option("--host <host>", "address to bind; anything but loopback needs a token", DEFAULT_SERVE_HOST).option("--db <path>", "board database file to open").option("--database-url <url>", "external Postgres board to serve instead of a board file").option("--workspace <nameOrId>", "workspace the server is about").option("--token <token>", "bearer token every request must carry (or set KANBO_SERVE_TOKEN); made up for the run on loopback when absent").option("--cors-origin <origin>", "let a browser call from this exact origin; repeat for more", collect, []).option("--no-open", "print the board page link without opening it in the browser").action(async (options) => {
7509
7583
  const port = parsePort(options.port);
7510
- const token = options.token?.trim() || process.env.KANBO_SERVE_TOKEN?.trim() || null;
7511
- assertServeBinding(options.host, token);
7584
+ const access = resolveServeAccess(options.host, options.token?.trim() || process.env.KANBO_SERVE_TOKEN?.trim() || null);
7512
7585
  const session = await openBoardSession({
7513
7586
  db: options.db,
7514
7587
  databaseUrl: options.databaseUrl,
@@ -7526,15 +7599,25 @@ function registerServeCommand(program) {
7526
7599
  actors,
7527
7600
  host: options.host,
7528
7601
  port,
7529
- token,
7602
+ token: access.token,
7530
7603
  corsOrigins: options.corsOrigin
7531
7604
  });
7532
7605
  } catch (error) {
7533
7606
  await session.close();
7534
7607
  throw error;
7535
7608
  }
7536
- console.error(`kanbo serve: ${server.url} — workspace ${session.workspace.id}${token ? ", bearer token required" : ""}`);
7537
- console.error(`kanbo serve: ${describePersonRights(actors, token)}`);
7609
+ console.error(`kanbo serve: ${server.url} — workspace ${session.workspace.id}, bearer token required${access.generated ? " (generated for this run)" : ""}`);
7610
+ console.error(`kanbo serve: ${describePersonRights(actors)}`);
7611
+ presentServePage({
7612
+ url: server.url,
7613
+ access,
7614
+ agentShell,
7615
+ terminal: process.stdout.isTTY === true,
7616
+ open: options.open
7617
+ }, {
7618
+ print: (line) => console.log(line),
7619
+ openInBrowser
7620
+ });
7538
7621
  let stopping = false;
7539
7622
  const stop = async () => {
7540
7623
  if (stopping) return;
@@ -7861,6 +7944,6 @@ function buildCapabilitiesManifest() {
7861
7944
  };
7862
7945
  }
7863
7946
  //#endregion
7864
- export { formatPlainActivityValue as $, formatExternalSessionRef as A, readChangeSeq as B, ignoreBoardFile as C, maskDatabaseUrl as D, writeBinding as E, UNMET_ENTRY_RULE_REASONS as F, diffFieldChanges as G, DEFAULT_STATUSES as H, evaluateEntryRules as I, activityKindOrder as J, BOARD_ACTOR as K, readChecklist as L, pullRequestFactKeys as M, ENTRY_RULES as N, maskDatabaseUrls as O, ENTRY_RULE_DESCRIPTIONS as P, formatActivityIssueReference as Q, readEntryRules as R, BINDING_FILE_PATH as S, readProjectBinding as T, normalizeStatusName as U, runBoardWrite as V, TRACKED_FIELDS as W, activityToken as X, activityText as Y, commentAuthorReference as Z, projectKanboRun as _, BOARD_ERROR_RESPONSES as a, toIssueView as at, DEFAULT_BOARD_FILE_PATH as b, registerKanboTools as c, nextIssueNumber as ct, KANBO_CAPABILITIES_MARKDOWN_RESOURCE_URI as d, isEmptyActivityValue as et, KANBO_CAPABILITIES_RESOURCE_URI as f, projectKanboPullRequest as g, projectKanboColumn as h, KANBO_TOOL_NAMES as i, normalizeCommentAuthorKind as it, parseExternalSessionRef as j, createBoardOps as k, KANBO_TOOLS as l, nextIssueOrder as lt, projectKanboCard as m, buildCapabilitiesManifest as n, readSystemCommentKind as nt, KANBO_MCP_SERVER_NAME as o, formatIssueId as ot, registerKanboCapabilitiesResources as p, currentUnixSeconds as q, KANBO_TOOL_CLI_EQUIVALENTS as r, isOutsideWriter as rt, createKanboMcpServer as s, nextIssueIdentity as st, CAPABILITIES_MANIFEST_VERSION as t, parseActivityStringArray as tt, buildKanboTools as u, readIssuePrefix as ut, projectKanboSprint as v, readBinding as w, renderCapabilitiesMarkdown as x, projectKanboSubCard as y, serializeEntryRules as z };
7947
+ export { formatActivityIssueReference as $, createBoardOps as A, serializeEntryRules as B, BINDING_FILE_PATH as C, writeBinding as D, readProjectBinding as E, ENTRY_RULE_DESCRIPTIONS as F, TRACKED_FIELDS as G, runBoardWrite as H, UNMET_ENTRY_RULE_REASONS as I, currentUnixSeconds as J, diffFieldChanges as K, evaluateEntryRules as L, parseExternalSessionRef as M, pullRequestFactKeys as N, maskDatabaseUrl as O, ENTRY_RULES as P, commentAuthorReference as Q, readChecklist as R, cardDisplayTitle as S, readBinding as T, DEFAULT_STATUSES as U, readChangeSeq as V, normalizeStatusName as W, activityText as X, activityKindOrder as Y, activityToken as Z, projectKanboRun as _, BOARD_ERROR_RESPONSES as a, normalizeCommentAuthorKind as at, DEFAULT_BOARD_FILE_PATH as b, registerKanboTools as c, nextIssueIdentity as ct, KANBO_CAPABILITIES_MARKDOWN_RESOURCE_URI as d, readIssuePrefix as dt, formatPlainActivityValue as et, KANBO_CAPABILITIES_RESOURCE_URI as f, projectKanboPullRequest as g, projectKanboColumn as h, KANBO_TOOL_NAMES as i, isOutsideWriter as it, formatExternalSessionRef as j, maskDatabaseUrls as k, KANBO_TOOLS as l, nextIssueNumber as lt, projectKanboCard as m, buildCapabilitiesManifest as n, parseActivityStringArray as nt, KANBO_MCP_SERVER_NAME as o, toIssueView as ot, registerKanboCapabilitiesResources as p, BOARD_ACTOR as q, KANBO_TOOL_CLI_EQUIVALENTS as r, readSystemCommentKind as rt, createKanboMcpServer as s, formatIssueId as st, CAPABILITIES_MANIFEST_VERSION as t, isEmptyActivityValue as tt, buildKanboTools as u, nextIssueOrder as ut, projectKanboSprint as v, ignoreBoardFile as w, renderCapabilitiesMarkdown as x, projectKanboSubCard as y, readEntryRules as z };
7865
7948
 
7866
- //# sourceMappingURL=manifest-C0EcwwgO.mjs.map
7949
+ //# sourceMappingURL=manifest-DfOBqo-Q.mjs.map