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.
- package/README.md +2 -2
- package/dist/cli.cjs +185 -39
- package/dist/cli.cjs.map +1 -1
- package/dist/lib/{board-store-B16YarvX.d.mts → board-store-BfszzAJB.d.cts} +2 -2
- package/dist/lib/{board-store-BHHo_ZlR.d.cts → board-store-BvWn9xTB.d.mts} +2 -2
- package/dist/lib/index.cjs +2 -29
- package/dist/lib/index.cjs.map +1 -1
- package/dist/lib/index.d.cts +363 -363
- package/dist/lib/index.d.mts +363 -363
- package/dist/lib/index.mjs +1 -28
- package/dist/lib/index.mjs.map +1 -1
- package/dist/lib/{manifest-CpZOcB_6.cjs → manifest-CmiuDqH-.cjs} +200 -41
- package/dist/lib/manifest-CmiuDqH-.cjs.map +1 -0
- package/dist/lib/{manifest-C0EcwwgO.mjs → manifest-NRhhhOkC.mjs} +190 -43
- package/dist/lib/manifest-NRhhhOkC.mjs.map +1 -0
- package/dist/lib/mcp/index.cjs +2 -1
- package/dist/lib/mcp/index.d.cts +29 -12
- package/dist/lib/mcp/index.d.mts +29 -12
- package/dist/lib/mcp/index.mjs +2 -2
- package/dist/lib/postgres/index.d.cts +151 -151
- package/dist/lib/postgres/index.d.mts +151 -151
- package/dist/lib/{schema-fsHqNAip.d.cts → schema-D0MyXcej.d.cts} +41 -41
- package/dist/lib/{schema-fsHqNAip.d.mts → schema-D0MyXcej.d.mts} +41 -41
- package/dist/lib/sqlite/index.d.cts +12 -12
- package/dist/lib/sqlite/index.d.mts +12 -12
- package/dist/lib/sqlite/schema.d.cts +1 -1
- package/dist/lib/sqlite/schema.d.mts +1 -1
- package/dist/lib/{tool-names-9cmCn0gP.d.cts → tool-names-3q_GEZej.d.mts} +5 -5
- package/dist/lib/{tool-names-Dxg1UTUl.d.mts → tool-names-WfH4IURz.d.cts} +5 -5
- package/dist/page/board.js +1 -1
- package/package.json +4 -4
- package/dist/lib/manifest-C0EcwwgO.mjs.map +0 -1
- package/dist/lib/manifest-CpZOcB_6.cjs.map +0 -1
package/dist/lib/index.mjs
CHANGED
|
@@ -1,32 +1,5 @@
|
|
|
1
|
-
import { $ as
|
|
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() {
|
package/dist/lib/index.mjs.map
CHANGED
|
@@ -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/
|
|
2317
|
-
/**
|
|
2318
|
-
* The
|
|
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.
|
|
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
|
-
*
|
|
2328
|
-
*
|
|
2329
|
-
*
|
|
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
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
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.
|
|
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}
|
|
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
|
|
7343
|
-
*
|
|
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
|
|
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
|
|
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}${
|
|
7560
|
-
console.error(`kanbo serve: ${describePersonRights(actors
|
|
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-
|
|
8431
|
+
//# sourceMappingURL=manifest-CmiuDqH-.cjs.map
|