kanbo-cli 0.1.0 → 0.1.2

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 +2 -2
  2. package/dist/cli.cjs +185 -39
  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-CpZOcB_6.cjs → manifest-CmiuDqH-.cjs} +200 -41
  13. package/dist/lib/manifest-CmiuDqH-.cjs.map +1 -0
  14. package/dist/lib/{manifest-C0EcwwgO.mjs → manifest-NRhhhOkC.mjs} +190 -43
  15. package/dist/lib/manifest-NRhhhOkC.mjs.map +1 -0
  16. package/dist/lib/mcp/index.cjs +2 -1
  17. package/dist/lib/mcp/index.d.cts +29 -12
  18. package/dist/lib/mcp/index.d.mts +29 -12
  19. package/dist/lib/mcp/index.mjs +2 -2
  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 commentAuthorReference, A as maskDatabaseUrls, B as readEntryRules, C as cardDisplayTitle, D as readProjectBinding, E as readBinding, F as ENTRY_RULES, G as normalizeStatusName, H as readChangeSeq, I as ENTRY_RULE_DESCRIPTIONS, J as BOARD_ACTOR, K as TRACKED_FIELDS, L as UNMET_ENTRY_RULE_REASONS, M as formatExternalSessionRef, N as parseExternalSessionRef, O as writeBinding, P as pullRequestFactKeys, Q as activityToken, R as evaluateEntryRules, S as renderCapabilitiesMarkdown, T as ignoreBoardFile, U as runBoardWrite, V as serializeEntryRules, W as DEFAULT_STATUSES, X as activityKindOrder, Y as currentUnixSeconds, Z as activityText, a as BOARD_ERROR_RESPONSES, at as isOutsideWriter, ct as formatIssueId, dt as nextIssueOrder, et as formatActivityIssueReference, ft as readIssuePrefix, i as KANBO_TOOL_NAMES, it as readSystemCommentKind, j as createBoardOps, k as maskDatabaseUrl, lt as nextIssueIdentity, n as buildCapabilitiesManifest, nt as isEmptyActivityValue, ot as normalizeCommentAuthorKind, q as diffFieldChanges, r as KANBO_TOOL_CLI_EQUIVALENTS, rt as parseActivityStringArray, st as toIssueView, t as CAPABILITIES_MANIFEST_VERSION, tt as formatPlainActivityValue, ut as nextIssueNumber, w as BINDING_FILE_PATH, x as DEFAULT_BOARD_FILE_PATH, z as readChecklist } from "./manifest-NRhhhOkC.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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"}
@@ -37,6 +37,7 @@ postgres = __toESM(postgres, 1);
37
37
  let node_url = require("node:url");
38
38
  let _modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
39
39
  let _modelcontextprotocol_sdk_server_mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
40
+ let node_child_process = require("node:child_process");
40
41
  let node_http = require("node:http");
41
42
  /**
42
43
  * Every exit code this tool promises, and what it means — the capabilities
@@ -2313,32 +2314,61 @@ async function migrateCards(store, input) {
2313
2314
  };
2314
2315
  }
2315
2316
  //#endregion
2316
- //#region src/ops/prime.ts
2317
- /**
2318
- * The rules of this board, as an agent needs to hear them.
2317
+ //#region src/ops/agent-rules.ts
2318
+ /**
2319
+ * The sentences that tell an agent how to work this board, written once.
2320
+ *
2321
+ * Four surfaces say them: the text `kanbo prime` returns (`BOARD_RULES`), the
2322
+ * block `kanbo init` writes into a project's `CLAUDE.md` / `AGENTS.md`, the
2323
+ * `instructions` the `kanbo mcp` server sends when a client connects, and the
2324
+ * `kanbo_card_create` tool's description. Each surface adds its own way of
2325
+ * doing the thing — a command, a tool name — but the rule itself is read from
2326
+ * here, so the four cannot come to say different things.
2327
+ *
2328
+ * No sentence names a tool or a command: the same text reaches an agent holding
2329
+ * MCP tools, one with a command line, and one talking to a server over HTTP.
2330
+ */
2331
+ const MOVE_CARD_RULE = "You move the card between columns yourself, at the moment its real state changes. The board never moves a card for you.";
2332
+ const STATUS_LINE_RULE = "Write a status line at every step, including before and after anything long-running. One sentence, present tense, about what is happening right now.";
2333
+ const WAIT_FOR_PERSON_RULE = "When you need a person, mark the card as waiting for approval and end your turn. You will be told when the answer comes.";
2334
+ const PERSON_ONLY_RULE = "Only a person approves a card, and only a person takes one out of waiting. Never do either yourself.";
2335
+ const EXECUTION_MODE_RULE = "Each card says where it runs: `worktree` means a separate copy of the project, `main` means the project folder itself. Work in the mode the card names.";
2336
+ /** When to split a card at all — said on its own where the surface names the parent its own way. */
2337
+ const SUBTASK_WHEN_SENTENCE = "Create subtasks only when the person asks for them, or when a task has parts that can be done and checked separately (different stages, owners or pull requests); do not split small work you will finish in one go.";
2338
+ /** How to split one, with the way this surface names the parent card. */
2339
+ function subtaskSplitSentence(parentReference) {
2340
+ return `When you do split a card, make the parts subtasks of it (${parentReference}), not separate top-level cards.`;
2341
+ }
2342
+ const SUBTASK_RULE = `${SUBTASK_WHEN_SENTENCE} ${subtaskSplitSentence("the parent card's id")}`;
2343
+ const PULL_REQUEST_RULE = "Opened a pull request? Link it to the card. Its state and CI checks then reach the card as comments on their own — nothing else tells the board a pull request exists.";
2344
+ const OWN_SESSION_RULE = "Started the work yourself, not launched by an app that tracks the run for you? Say so when you start the run — `claude:<session id>` from Claude Code, `codex:<session id>` from Codex (the session id Codex prints) — so your own log of it can be found later.";
2345
+ /** Said by the `kanbo init` block and the MCP instructions, which name the comment command or tool beside it. */
2346
+ const COMMENT_RULE = "Findings, results, decisions and questions go on the card as comments.";
2347
+ /**
2348
+ * The rules of this board, as `kanbo prime` says them under its column list.
2319
2349
  *
2320
2350
  * They are about the board and nothing else. The board does not hand out a role, a
2321
2351
  * method or a pipeline — that belongs to the instructions the person wrote for
2322
2352
  * their own agent — so the list stops at what the board itself guarantees and
2323
- * expects. No tool is named either: the same text reaches an agent holding MCP
2324
- * tools, one with a command line, and one talking to a server over HTTP, and each
2325
- * of them already knows what it is holding.
2353
+ * expects.
2326
2354
  *
2327
- * Exported because it is also the source `buildCapabilitiesManifest` reads its
2328
- * `rules` from — one list, read by the prompt an agent sees and by the manifest
2329
- * an orchestrator reads about the board.
2355
+ * Also the source `buildCapabilitiesManifest` reads its `rules` from — one list,
2356
+ * read by the prompt an agent sees and by the manifest an orchestrator reads
2357
+ * about the board.
2330
2358
  */
2331
2359
  const BOARD_RULES = [
2332
- "You move the card between columns yourself, at the moment its real state changes. The board never moves a card for you.",
2333
- "Write a status line at every step, including before and after anything long-running. One sentence, present tense, about what is happening right now.",
2334
- "When you need a person, mark the card as waiting for approval and end your turn. You will be told when the answer comes.",
2335
- "Only a person approves a card, and only a person takes one out of waiting. Never do either yourself.",
2336
- "Each card says where it runs: `worktree` means a separate copy of the project, `main` means the project folder itself. Work in the mode the card names.",
2337
- "Create subtasks only when the person asks for them, or when a task has parts that can be done and checked separately (different stages, owners or pull requests); do not split small work you will finish in one go. When you do split a card, make the parts subtasks of it (the parent card's id), not separate top-level cards.",
2338
- "Opened a pull request? Link it to the card. Its state and CI checks then reach the card as comments on their own — nothing else tells the board a pull request exists.",
2339
- "Started the work yourself, not launched by an app that tracks the run for you? Say so when you start the run — `claude:<session id>` from Claude Code, `codex:<session id>` from Codex (the session id Codex prints) — so your own log of it can be found later.",
2360
+ MOVE_CARD_RULE,
2361
+ STATUS_LINE_RULE,
2362
+ WAIT_FOR_PERSON_RULE,
2363
+ PERSON_ONLY_RULE,
2364
+ EXECUTION_MODE_RULE,
2365
+ SUBTASK_RULE,
2366
+ PULL_REQUEST_RULE,
2367
+ OWN_SESSION_RULE,
2340
2368
  "Read the column descriptions above before moving a card, and do all of this through the board tools or commands you have — a statement in your reply changes nothing."
2341
2369
  ];
2370
+ //#endregion
2371
+ //#region src/ops/prime.ts
2342
2372
  /**
2343
2373
  * What the board tells an agent about itself: the columns it has, what each one
2344
2374
  * means, and how a card is expected to travel across them.
@@ -3676,6 +3706,33 @@ function readCardNumber(session, reference) {
3676
3706
  return reference.slice(0, separator).toUpperCase() === prefix ? Number(digits) : null;
3677
3707
  }
3678
3708
  //#endregion
3709
+ //#region src/domain/card-display-title.ts
3710
+ /**
3711
+ * What a card is called wherever a person or an agent reads it by name.
3712
+ *
3713
+ * A card written as a description alone is titled with its own key (every
3714
+ * writer does that). Named by its key, such a card says nothing — so its name
3715
+ * is the first line of its description instead. A browser bundle that does not
3716
+ * load this package keeps the same rule on its own, tested with the same
3717
+ * examples.
3718
+ */
3719
+ /** How long a description line may run before it is cut with an ellipsis. */
3720
+ const DESCRIPTION_LINE_LIMIT = 120;
3721
+ function cardDisplayTitle(card) {
3722
+ if (card.title !== card.id) return card.title;
3723
+ return descriptionFirstLine(card.description) ?? card.title;
3724
+ }
3725
+ function descriptionFirstLine(description) {
3726
+ for (const rawLine of (description ?? "").split("\n")) {
3727
+ const line = stripMarkdown(rawLine);
3728
+ if (line) return line.length > DESCRIPTION_LINE_LIMIT ? `${line.slice(0, 119).trimEnd()}…` : line;
3729
+ }
3730
+ return null;
3731
+ }
3732
+ function stripMarkdown(line) {
3733
+ 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();
3734
+ }
3735
+ //#endregion
3679
3736
  //#region src/cli/view.ts
3680
3737
  /** Every field `kanbo card list|get --json` may ask for, in listing order. */
3681
3738
  const CARD_VIEW_FIELDS = [
@@ -3792,16 +3849,16 @@ function projectRun(run) {
3792
3849
  launchedByKind: run.launchedByKind
3793
3850
  };
3794
3851
  }
3795
- /** One card per line: the key, the column it is in, and what it is called. */
3852
+ /** One card per line: the key, the column it is in, and what it is called (`cardDisplayTitle`). */
3796
3853
  function describeCards(cards) {
3797
3854
  if (cards.length === 0) return "No cards";
3798
3855
  const keyWidth = Math.max(...cards.map((card) => card.id.length));
3799
3856
  const columnWidth = Math.max(...cards.map((card) => (card.column ?? "—").length));
3800
- return cards.map((card) => `${card.id.padEnd(keyWidth)} ${(card.column ?? "—").padEnd(columnWidth)} ${card.title}`).join("\n");
3857
+ return cards.map((card) => `${card.id.padEnd(keyWidth)} ${(card.column ?? "—").padEnd(columnWidth)} ${cardDisplayTitle(card)}`).join("\n");
3801
3858
  }
3802
3859
  /** The whole card, for a person who asked about exactly one. */
3803
3860
  function describeCard(card) {
3804
- const lines = [`${card.id} ${card.column ?? "—"} ${card.executionMode}`, card.title];
3861
+ const lines = [`${card.id} ${card.column ?? "—"} ${card.executionMode}`, cardDisplayTitle(card)];
3805
3862
  if (card.description) lines.push("", card.description);
3806
3863
  const facts = [
3807
3864
  card.statusLine ? `status line: ${card.statusLine}` : null,
@@ -4329,7 +4386,8 @@ const CODEX_TABLE_PATTERN = new RegExp(String.raw`^\s*\[\s*mcp_servers\s*\.\s*(?
4329
4386
  *
4330
4387
  * It says what to run and when, and nothing about how to do the work: the
4331
4388
  * method is the person's own instructions to their agent, and the board only
4332
- * insists on the part that keeps the card honest.
4389
+ * insists on the part that keeps the card honest. The rules are the sentences
4390
+ * `ops/agent-rules.ts` holds for every surface, with the command beside each.
4333
4391
  */
4334
4392
  const INSTRUCTION_BLOCK = [
4335
4393
  INSTRUCTION_START,
@@ -4338,12 +4396,13 @@ const INSTRUCTION_BLOCK = [
4338
4396
  "Work on this project is tracked on a kanbo board. Before starting a task run `kanbo prime` — it prints the board's columns and what each one means.",
4339
4397
  "",
4340
4398
  "- Take a card from `kanbo ready`.",
4341
- "- Move the card yourself (`kanbo card move <id> <column>`) — a card in the wrong column is a lie.",
4342
- "- Write a status line at every step (`kanbo card status-line <id> --text \"...\"`).",
4343
- "- Create subtasks only when the person asks for them, or when a task has parts that can be done and checked separately (different stages, owners or pull requests); do not split small work you will finish in one go. When you do split a card, make the parts subtasks of it (the parent card's id), not separate top-level cards. (`kanbo card create --description \"...\" --parent <id>`).",
4344
- "- Findings, decisions and questions go on the card (`kanbo card comment <id> --content \"...\"`).",
4345
- "- When you need a person, run `kanbo card wait-approval <id>` and end your turn. Never approve your own work, and never take a card out of a column where it is waiting for a person.",
4346
- "- Started the work yourself, not launched by an app that tracks the run? Pass your own session to `kanbo run start --session` — `claude:<session id>` from Claude Code, `codex:<session id>` from Codex (the session id Codex prints).",
4399
+ `- ${MOVE_CARD_RULE} (\`kanbo card move <id> <column>\`)`,
4400
+ `- ${STATUS_LINE_RULE} (\`kanbo card status-line <id> --text "..."\`)`,
4401
+ `- ${SUBTASK_RULE} (\`kanbo card create --description "..." --parent <id>\`)`,
4402
+ `- ${COMMENT_RULE} (\`kanbo card comment <id> --content "..."\`)`,
4403
+ `- ${WAIT_FOR_PERSON_RULE} (\`kanbo card wait-approval <id>\`)`,
4404
+ `- ${PERSON_ONLY_RULE}`,
4405
+ `- ${OWN_SESSION_RULE} (\`kanbo run start <id> --agent <name> --session <ref>\`)`,
4347
4406
  "- Run `kanbo capabilities` for the full list of tools, commands and rules.",
4348
4407
  INSTRUCTION_END
4349
4408
  ].join("\n");
@@ -5135,7 +5194,7 @@ async function createDbTransport(input) {
5135
5194
  * `package.version`. A build carries it as a literal; running from source
5136
5195
  * (tests, `tsx`) reads the package's own `package.json` instead.
5137
5196
  */
5138
- const KANBO_PACKAGE_VERSION = "0.1.0";
5197
+ const KANBO_PACKAGE_VERSION = "0.1.2";
5139
5198
  //#endregion
5140
5199
  //#region src/mcp/resources.ts
5141
5200
  /** The URI a client reads the manifest as JSON from. */
@@ -5176,6 +5235,36 @@ function registerKanboCapabilitiesResources(server) {
5176
5235
  }] }));
5177
5236
  }
5178
5237
  //#endregion
5238
+ //#region src/mcp/instructions.ts
5239
+ /** A tool name as the instructions print it — typed, so a renamed tool fails the build here. */
5240
+ function tool(name) {
5241
+ return `\`${name}\``;
5242
+ }
5243
+ /**
5244
+ * What `kanbo mcp` tells a client when it connects — the `instructions` of the
5245
+ * MCP initialize result, which a client hands its model before any tool is
5246
+ * called. An agent with nothing but this server connected learns from it how
5247
+ * to work the board, with no instruction file in the project.
5248
+ *
5249
+ * Static on purpose: it is sent before any board is resolved, so it says
5250
+ * nothing about columns — `kanbo_prime` returns those. The rules are the
5251
+ * sentences `ops/agent-rules.ts` holds for every surface, with the tool beside
5252
+ * each; the full manifest stays a resource.
5253
+ */
5254
+ const KANBO_MCP_INSTRUCTIONS = [
5255
+ "kanbo is a kanban board shared by people and agents; these tools read and change its cards.",
5256
+ "",
5257
+ `- At the start of a session call ${tool("kanbo_prime")} — it returns this board's columns and what each one means.`,
5258
+ `- Take work from ${tool("kanbo_ready")}.`,
5259
+ `- ${MOVE_CARD_RULE} (${tool("kanbo_card_move")})`,
5260
+ `- ${STATUS_LINE_RULE} (${tool("kanbo_status_line")})`,
5261
+ `- ${COMMENT_RULE} (${tool("kanbo_card_comment")})`,
5262
+ `- ${SUBTASK_RULE} (${tool("kanbo_card_create")} with \`parent\`)`,
5263
+ `- ${WAIT_FOR_PERSON_RULE} (${tool("kanbo_wait_approval")})`,
5264
+ `- ${PERSON_ONLY_RULE}`,
5265
+ `- Every tool, rule and canonical string: the resource \`${KANBO_CAPABILITIES_MARKDOWN_RESOURCE_URI}\`.`
5266
+ ].join("\n");
5267
+ //#endregion
5179
5268
  //#region src/mcp/tools/tool.ts
5180
5269
  /**
5181
5270
  * Declare a tool: what it is called, what it takes, and the one thing it does.
@@ -5273,7 +5362,7 @@ const SUBTASK_SENTENCE = "To create a subtask (sub-card) under another card, pas
5273
5362
  const cardCreateTool = defineKanboTool({
5274
5363
  name: "kanbo_card_create",
5275
5364
  title: "Create a card",
5276
- description: `Put a new card on this board. ${SUBTASK_SENTENCE} Create subtasks only when the person asks for them, or when a task has parts that can be done and checked separately (different stages, owners or pull requests); do not split small work you will finish in one go. When you do split a card, make the parts subtasks of it (pass \`parent\`), not separate top-level cards.`,
5365
+ description: `Put a new card on this board. ${SUBTASK_SENTENCE} ${SUBTASK_WHEN_SENTENCE} ${subtaskSplitSentence("pass `parent`")}`,
5277
5366
  inputSchema: {
5278
5367
  title: zod.z.string().optional().describe("What the card is called. Leave it out and the card is titled with its own key; the description then says what the task is."),
5279
5368
  description: zod.z.string().optional().describe("What the card is about, spelled out for whoever picks it up."),
@@ -5545,12 +5634,15 @@ function registerKanboTools(server, transport, options) {
5545
5634
  * function: it documents its capabilities its own way, and adding a second
5546
5635
  * `kanbo://` resource namespace to an inventory that is not only the board's
5547
5636
  * is a decision for that server to make, not this one.
5637
+ *
5638
+ * For the same reason it alone sends `KANBO_MCP_INSTRUCTIONS` in the initialize
5639
+ * result: the instructions of a server that is only the board.
5548
5640
  */
5549
5641
  function createKanboMcpServer(transport, options) {
5550
5642
  const server = new _modelcontextprotocol_sdk_server_mcp_js.McpServer({
5551
5643
  name: KANBO_MCP_SERVER_NAME,
5552
5644
  version: KANBO_MCP_SERVER_VERSION
5553
- });
5645
+ }, { instructions: KANBO_MCP_INSTRUCTIONS });
5554
5646
  registerKanboTools(server, transport, options);
5555
5647
  registerKanboCapabilitiesResources(server);
5556
5648
  return server;
@@ -7339,12 +7431,11 @@ async function startKanboServer(options) {
7339
7431
  }
7340
7432
  /**
7341
7433
  * Who may act for a person on this server, said once when it starts: only a
7342
- * request presenting the token does, and a server with no token, or started
7343
- * from an agent's shell, has nobody who may.
7434
+ * request presenting the token does, and a server started from an agent's
7435
+ * shell has nobody who may.
7344
7436
  */
7345
- function describePersonRights(actors, token) {
7437
+ function describePersonRights(actors) {
7346
7438
  if (!actors.person) return "no request acts for a person (started from an agent's shell)";
7347
- if (!token) return "no request acts for a person (no token; set KANBO_SERVE_TOKEN to allow it)";
7348
7439
  return `a request with the token acts for ${actors.person.id}`;
7349
7440
  }
7350
7441
  function stripBrackets(host) {
@@ -7526,12 +7617,58 @@ function sendJson(response, status, value) {
7526
7617
  response.end(body);
7527
7618
  }
7528
7619
  //#endregion
7620
+ //#region src/serve/access.ts
7621
+ /** The token given, or one made up for a loopback server; refused off loopback without one. */
7622
+ function resolveServeAccess(host, givenToken) {
7623
+ assertServeBinding(host, givenToken);
7624
+ if (givenToken) return {
7625
+ token: givenToken,
7626
+ generated: false
7627
+ };
7628
+ return {
7629
+ token: (0, node_crypto.randomBytes)(24).toString("base64url"),
7630
+ generated: true
7631
+ };
7632
+ }
7633
+ /**
7634
+ * Hand the person the board page: a link printed once, and opened in the
7635
+ * browser unless they said not to. A generated token rides in the link's
7636
+ * fragment; one they gave is theirs already and is left out.
7637
+ */
7638
+ function presentServePage(presentation, output) {
7639
+ const { url, access, open } = presentation;
7640
+ if (presentation.agentShell || !presentation.terminal) {
7641
+ 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");
7642
+ return;
7643
+ }
7644
+ const link = access.generated ? `${url}/#token=${access.token}` : `${url}/`;
7645
+ output.print(`kanbo serve: board page ${link}`);
7646
+ if (open) output.openInBrowser(link);
7647
+ }
7648
+ /** Open a link in the default browser, without waiting for it and without failing when there is none. */
7649
+ function openInBrowser(url) {
7650
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", [
7651
+ "/c",
7652
+ "start",
7653
+ "\"\"",
7654
+ url
7655
+ ]] : ["xdg-open", [url]];
7656
+ try {
7657
+ const child = (0, node_child_process.spawn)(command, args, {
7658
+ detached: true,
7659
+ stdio: "ignore",
7660
+ windowsVerbatimArguments: process.platform === "win32"
7661
+ });
7662
+ child.on("error", () => {});
7663
+ child.unref();
7664
+ } catch {}
7665
+ }
7666
+ //#endregion
7529
7667
  //#region src/cli/commands/serve.ts
7530
7668
  function registerServeCommand(program) {
7531
- 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) => {
7669
+ 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) => {
7532
7670
  const port = parsePort(options.port);
7533
- const token = options.token?.trim() || process.env.KANBO_SERVE_TOKEN?.trim() || null;
7534
- assertServeBinding(options.host, token);
7671
+ const access = resolveServeAccess(options.host, options.token?.trim() || process.env.KANBO_SERVE_TOKEN?.trim() || null);
7535
7672
  const session = await openBoardSession({
7536
7673
  db: options.db,
7537
7674
  databaseUrl: options.databaseUrl,
@@ -7549,15 +7686,25 @@ function registerServeCommand(program) {
7549
7686
  actors,
7550
7687
  host: options.host,
7551
7688
  port,
7552
- token,
7689
+ token: access.token,
7553
7690
  corsOrigins: options.corsOrigin
7554
7691
  });
7555
7692
  } catch (error) {
7556
7693
  await session.close();
7557
7694
  throw error;
7558
7695
  }
7559
- console.error(`kanbo serve: ${server.url} — workspace ${session.workspace.id}${token ? ", bearer token required" : ""}`);
7560
- console.error(`kanbo serve: ${describePersonRights(actors, token)}`);
7696
+ console.error(`kanbo serve: ${server.url} — workspace ${session.workspace.id}, bearer token required${access.generated ? " (generated for this run)" : ""}`);
7697
+ console.error(`kanbo serve: ${describePersonRights(actors)}`);
7698
+ presentServePage({
7699
+ url: server.url,
7700
+ access,
7701
+ agentShell,
7702
+ terminal: process.stdout.isTTY === true,
7703
+ open: options.open
7704
+ }, {
7705
+ print: (line) => console.log(line),
7706
+ openInBrowser
7707
+ });
7561
7708
  let stopping = false;
7562
7709
  const stop = async () => {
7563
7710
  if (stopping) return;
@@ -7944,6 +8091,12 @@ Object.defineProperty(exports, "KANBO_CAPABILITIES_RESOURCE_URI", {
7944
8091
  return KANBO_CAPABILITIES_RESOURCE_URI;
7945
8092
  }
7946
8093
  });
8094
+ Object.defineProperty(exports, "KANBO_MCP_INSTRUCTIONS", {
8095
+ enumerable: true,
8096
+ get: function() {
8097
+ return KANBO_MCP_INSTRUCTIONS;
8098
+ }
8099
+ });
7947
8100
  Object.defineProperty(exports, "KANBO_MCP_SERVER_NAME", {
7948
8101
  enumerable: true,
7949
8102
  get: function() {
@@ -8010,6 +8163,12 @@ Object.defineProperty(exports, "buildKanboTools", {
8010
8163
  return buildKanboTools;
8011
8164
  }
8012
8165
  });
8166
+ Object.defineProperty(exports, "cardDisplayTitle", {
8167
+ enumerable: true,
8168
+ get: function() {
8169
+ return cardDisplayTitle;
8170
+ }
8171
+ });
8013
8172
  Object.defineProperty(exports, "commentAuthorReference", {
8014
8173
  enumerable: true,
8015
8174
  get: function() {
@@ -8269,4 +8428,4 @@ Object.defineProperty(exports, "writeBinding", {
8269
8428
  }
8270
8429
  });
8271
8430
 
8272
- //# sourceMappingURL=manifest-CpZOcB_6.cjs.map
8431
+ //# sourceMappingURL=manifest-CmiuDqH-.cjs.map