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/README.md
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
- **Status lines and runs.** Every card says what it is doing right now, and every launch of a card is recorded as a run with an attempt number.
|
|
15
15
|
- **Subtasks, comments, pull request links, sprints** and **column entry rules** (`checklist_complete`, `pull_request_linked`, `ci_green`, `approved`).
|
|
16
16
|
- **Storage you choose.** A board file in the project (`.kanbo/board.db`), or an external Postgres database such as Supabase shared by a team, with `kanban_person` / `kanban_agent` roles.
|
|
17
|
+
- **MCP alone is enough to start.** `kanbo mcp` sends the board's basic rules as MCP server instructions when a client connects, so an agent needs no instruction file for the basics.
|
|
17
18
|
- **One-command project setup.** `kanbo init` binds the project, writes an instruction block into `CLAUDE.md` or `AGENTS.md`, and registers the MCP server with Claude Code, Codex or Cursor.
|
|
18
19
|
- **Board over HTTP.** `kanbo serve` answers a JSON API and serves a board page on `http://127.0.0.1:4318`, loopback-only and token-guarded.
|
|
19
20
|
- **Machine-readable capabilities.** `kanbo capabilities --json` describes every tool, command, rule and limit for an orchestrator to read.
|
|
@@ -34,8 +35,7 @@ This creates `.kanbo/board.db`, binds the project to it in `.kanbo/binding.json`
|
|
|
34
35
|
```bash
|
|
35
36
|
kanbo card create --column to_do --title "Add rate limiting" --description "60 requests per minute per client"
|
|
36
37
|
kanbo ready
|
|
37
|
-
|
|
38
|
-
kanbo serve # open http://127.0.0.1:4318 and paste the token into the Token field
|
|
38
|
+
kanbo serve # opens the board in your browser
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Now start your agent in the project. It reads the instruction block, runs `kanbo prime`, takes a card from `kanbo ready`, and works it. When it runs `kanbo card wait-approval`, you answer with `kanbo approve <card>` or `kanbo return <card> --comment "..."` — or with the buttons on the board page.
|
package/dist/cli.cjs
CHANGED
|
@@ -3083,7 +3083,7 @@ var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin((
|
|
|
3083
3083
|
* `package.version`. A build carries it as a literal; running from source
|
|
3084
3084
|
* (tests, `tsx`) reads the package's own `package.json` instead.
|
|
3085
3085
|
*/
|
|
3086
|
-
const KANBO_PACKAGE_VERSION = "0.1.
|
|
3086
|
+
const KANBO_PACKAGE_VERSION = "0.1.2";
|
|
3087
3087
|
/**
|
|
3088
3088
|
* Every exit code this tool promises, and what it means — the capabilities
|
|
3089
3089
|
* manifest's `exitCodes` reads this rather than naming the codes again. `0` and
|
|
@@ -11178,32 +11178,61 @@ async function migrateCards(store, input) {
|
|
|
11178
11178
|
};
|
|
11179
11179
|
}
|
|
11180
11180
|
//#endregion
|
|
11181
|
-
//#region src/ops/
|
|
11181
|
+
//#region src/ops/agent-rules.ts
|
|
11182
11182
|
/**
|
|
11183
|
-
* The
|
|
11183
|
+
* The sentences that tell an agent how to work this board, written once.
|
|
11184
|
+
*
|
|
11185
|
+
* Four surfaces say them: the text `kanbo prime` returns (`BOARD_RULES`), the
|
|
11186
|
+
* block `kanbo init` writes into a project's `CLAUDE.md` / `AGENTS.md`, the
|
|
11187
|
+
* `instructions` the `kanbo mcp` server sends when a client connects, and the
|
|
11188
|
+
* `kanbo_card_create` tool's description. Each surface adds its own way of
|
|
11189
|
+
* doing the thing — a command, a tool name — but the rule itself is read from
|
|
11190
|
+
* here, so the four cannot come to say different things.
|
|
11191
|
+
*
|
|
11192
|
+
* No sentence names a tool or a command: the same text reaches an agent holding
|
|
11193
|
+
* MCP tools, one with a command line, and one talking to a server over HTTP.
|
|
11194
|
+
*/
|
|
11195
|
+
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.";
|
|
11196
|
+
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.";
|
|
11197
|
+
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.";
|
|
11198
|
+
const PERSON_ONLY_RULE = "Only a person approves a card, and only a person takes one out of waiting. Never do either yourself.";
|
|
11199
|
+
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.";
|
|
11200
|
+
/** When to split a card at all — said on its own where the surface names the parent its own way. */
|
|
11201
|
+
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.";
|
|
11202
|
+
/** How to split one, with the way this surface names the parent card. */
|
|
11203
|
+
function subtaskSplitSentence(parentReference) {
|
|
11204
|
+
return `When you do split a card, make the parts subtasks of it (${parentReference}), not separate top-level cards.`;
|
|
11205
|
+
}
|
|
11206
|
+
const SUBTASK_RULE = `${SUBTASK_WHEN_SENTENCE} ${subtaskSplitSentence("the parent card's id")}`;
|
|
11207
|
+
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.";
|
|
11208
|
+
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.";
|
|
11209
|
+
/** Said by the `kanbo init` block and the MCP instructions, which name the comment command or tool beside it. */
|
|
11210
|
+
const COMMENT_RULE = "Findings, results, decisions and questions go on the card as comments.";
|
|
11211
|
+
/**
|
|
11212
|
+
* The rules of this board, as `kanbo prime` says them under its column list.
|
|
11184
11213
|
*
|
|
11185
11214
|
* They are about the board and nothing else. The board does not hand out a role, a
|
|
11186
11215
|
* method or a pipeline — that belongs to the instructions the person wrote for
|
|
11187
11216
|
* their own agent — so the list stops at what the board itself guarantees and
|
|
11188
|
-
* expects.
|
|
11189
|
-
* tools, one with a command line, and one talking to a server over HTTP, and each
|
|
11190
|
-
* of them already knows what it is holding.
|
|
11217
|
+
* expects.
|
|
11191
11218
|
*
|
|
11192
|
-
*
|
|
11193
|
-
*
|
|
11194
|
-
*
|
|
11219
|
+
* Also the source `buildCapabilitiesManifest` reads its `rules` from — one list,
|
|
11220
|
+
* read by the prompt an agent sees and by the manifest an orchestrator reads
|
|
11221
|
+
* about the board.
|
|
11195
11222
|
*/
|
|
11196
11223
|
const BOARD_RULES = [
|
|
11197
|
-
|
|
11198
|
-
|
|
11199
|
-
|
|
11200
|
-
|
|
11201
|
-
|
|
11202
|
-
|
|
11203
|
-
|
|
11204
|
-
|
|
11224
|
+
MOVE_CARD_RULE,
|
|
11225
|
+
STATUS_LINE_RULE,
|
|
11226
|
+
WAIT_FOR_PERSON_RULE,
|
|
11227
|
+
PERSON_ONLY_RULE,
|
|
11228
|
+
EXECUTION_MODE_RULE,
|
|
11229
|
+
SUBTASK_RULE,
|
|
11230
|
+
PULL_REQUEST_RULE,
|
|
11231
|
+
OWN_SESSION_RULE,
|
|
11205
11232
|
"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."
|
|
11206
11233
|
];
|
|
11234
|
+
//#endregion
|
|
11235
|
+
//#region src/ops/prime.ts
|
|
11207
11236
|
/**
|
|
11208
11237
|
* What the board tells an agent about itself: the columns it has, what each one
|
|
11209
11238
|
* means, and how a card is expected to travel across them.
|
|
@@ -22777,6 +22806,33 @@ function readCardNumber(session, reference) {
|
|
|
22777
22806
|
return reference.slice(0, separator).toUpperCase() === prefix ? Number(digits) : null;
|
|
22778
22807
|
}
|
|
22779
22808
|
//#endregion
|
|
22809
|
+
//#region src/domain/card-display-title.ts
|
|
22810
|
+
/**
|
|
22811
|
+
* What a card is called wherever a person or an agent reads it by name.
|
|
22812
|
+
*
|
|
22813
|
+
* A card written as a description alone is titled with its own key (every
|
|
22814
|
+
* writer does that). Named by its key, such a card says nothing — so its name
|
|
22815
|
+
* is the first line of its description instead. A browser bundle that does not
|
|
22816
|
+
* load this package keeps the same rule on its own, tested with the same
|
|
22817
|
+
* examples.
|
|
22818
|
+
*/
|
|
22819
|
+
/** How long a description line may run before it is cut with an ellipsis. */
|
|
22820
|
+
const DESCRIPTION_LINE_LIMIT = 120;
|
|
22821
|
+
function cardDisplayTitle(card) {
|
|
22822
|
+
if (card.title !== card.id) return card.title;
|
|
22823
|
+
return descriptionFirstLine(card.description) ?? card.title;
|
|
22824
|
+
}
|
|
22825
|
+
function descriptionFirstLine(description) {
|
|
22826
|
+
for (const rawLine of (description ?? "").split("\n")) {
|
|
22827
|
+
const line = stripMarkdown(rawLine);
|
|
22828
|
+
if (line) return line.length > DESCRIPTION_LINE_LIMIT ? `${line.slice(0, 119).trimEnd()}…` : line;
|
|
22829
|
+
}
|
|
22830
|
+
return null;
|
|
22831
|
+
}
|
|
22832
|
+
function stripMarkdown(line) {
|
|
22833
|
+
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();
|
|
22834
|
+
}
|
|
22835
|
+
//#endregion
|
|
22780
22836
|
//#region src/cli/view.ts
|
|
22781
22837
|
/** Every field `kanbo card list|get --json` may ask for, in listing order. */
|
|
22782
22838
|
const CARD_VIEW_FIELDS = [
|
|
@@ -22893,16 +22949,16 @@ function projectRun(run) {
|
|
|
22893
22949
|
launchedByKind: run.launchedByKind
|
|
22894
22950
|
};
|
|
22895
22951
|
}
|
|
22896
|
-
/** One card per line: the key, the column it is in, and what it is called. */
|
|
22952
|
+
/** One card per line: the key, the column it is in, and what it is called (`cardDisplayTitle`). */
|
|
22897
22953
|
function describeCards(cards) {
|
|
22898
22954
|
if (cards.length === 0) return "No cards";
|
|
22899
22955
|
const keyWidth = Math.max(...cards.map((card) => card.id.length));
|
|
22900
22956
|
const columnWidth = Math.max(...cards.map((card) => (card.column ?? "—").length));
|
|
22901
|
-
return cards.map((card) => `${card.id.padEnd(keyWidth)} ${(card.column ?? "—").padEnd(columnWidth)} ${card
|
|
22957
|
+
return cards.map((card) => `${card.id.padEnd(keyWidth)} ${(card.column ?? "—").padEnd(columnWidth)} ${cardDisplayTitle(card)}`).join("\n");
|
|
22902
22958
|
}
|
|
22903
22959
|
/** The whole card, for a person who asked about exactly one. */
|
|
22904
22960
|
function describeCard(card) {
|
|
22905
|
-
const lines = [`${card.id} ${card.column ?? "—"} ${card.executionMode}`, card
|
|
22961
|
+
const lines = [`${card.id} ${card.column ?? "—"} ${card.executionMode}`, cardDisplayTitle(card)];
|
|
22906
22962
|
if (card.description) lines.push("", card.description);
|
|
22907
22963
|
const facts = [
|
|
22908
22964
|
card.statusLine ? `status line: ${card.statusLine}` : null,
|
|
@@ -23090,7 +23146,7 @@ const SUBTASK_SENTENCE = "To create a subtask (sub-card) under another card, pas
|
|
|
23090
23146
|
const cardCreateTool = defineKanboTool({
|
|
23091
23147
|
name: "kanbo_card_create",
|
|
23092
23148
|
title: "Create a card",
|
|
23093
|
-
description: `Put a new card on this board. ${SUBTASK_SENTENCE}
|
|
23149
|
+
description: `Put a new card on this board. ${SUBTASK_SENTENCE} ${SUBTASK_WHEN_SENTENCE} ${subtaskSplitSentence("pass `parent`")}`,
|
|
23094
23150
|
inputSchema: {
|
|
23095
23151
|
title: 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."),
|
|
23096
23152
|
description: string().optional().describe("What the card is about, spelled out for whoever picks it up."),
|
|
@@ -24154,7 +24210,8 @@ const CODEX_TABLE_PATTERN = new RegExp(String.raw`^\s*\[\s*mcp_servers\s*\.\s*(?
|
|
|
24154
24210
|
*
|
|
24155
24211
|
* It says what to run and when, and nothing about how to do the work: the
|
|
24156
24212
|
* method is the person's own instructions to their agent, and the board only
|
|
24157
|
-
* insists on the part that keeps the card honest.
|
|
24213
|
+
* insists on the part that keeps the card honest. The rules are the sentences
|
|
24214
|
+
* `ops/agent-rules.ts` holds for every surface, with the command beside each.
|
|
24158
24215
|
*/
|
|
24159
24216
|
const INSTRUCTION_BLOCK = [
|
|
24160
24217
|
INSTRUCTION_START,
|
|
@@ -24163,12 +24220,13 @@ const INSTRUCTION_BLOCK = [
|
|
|
24163
24220
|
"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.",
|
|
24164
24221
|
"",
|
|
24165
24222
|
"- Take a card from `kanbo ready`.",
|
|
24166
|
-
|
|
24167
|
-
|
|
24168
|
-
|
|
24169
|
-
|
|
24170
|
-
|
|
24171
|
-
|
|
24223
|
+
`- ${MOVE_CARD_RULE} (\`kanbo card move <id> <column>\`)`,
|
|
24224
|
+
`- ${STATUS_LINE_RULE} (\`kanbo card status-line <id> --text "..."\`)`,
|
|
24225
|
+
`- ${SUBTASK_RULE} (\`kanbo card create --description "..." --parent <id>\`)`,
|
|
24226
|
+
`- ${COMMENT_RULE} (\`kanbo card comment <id> --content "..."\`)`,
|
|
24227
|
+
`- ${WAIT_FOR_PERSON_RULE} (\`kanbo card wait-approval <id>\`)`,
|
|
24228
|
+
`- ${PERSON_ONLY_RULE}`,
|
|
24229
|
+
`- ${OWN_SESSION_RULE} (\`kanbo run start <id> --agent <name> --session <ref>\`)`,
|
|
24172
24230
|
"- Run `kanbo capabilities` for the full list of tools, commands and rules.",
|
|
24173
24231
|
INSTRUCTION_END
|
|
24174
24232
|
].join("\n");
|
|
@@ -40692,6 +40750,36 @@ function registerKanboCapabilitiesResources(server) {
|
|
|
40692
40750
|
}] }));
|
|
40693
40751
|
}
|
|
40694
40752
|
//#endregion
|
|
40753
|
+
//#region src/mcp/instructions.ts
|
|
40754
|
+
/** A tool name as the instructions print it — typed, so a renamed tool fails the build here. */
|
|
40755
|
+
function tool(name) {
|
|
40756
|
+
return `\`${name}\``;
|
|
40757
|
+
}
|
|
40758
|
+
/**
|
|
40759
|
+
* What `kanbo mcp` tells a client when it connects — the `instructions` of the
|
|
40760
|
+
* MCP initialize result, which a client hands its model before any tool is
|
|
40761
|
+
* called. An agent with nothing but this server connected learns from it how
|
|
40762
|
+
* to work the board, with no instruction file in the project.
|
|
40763
|
+
*
|
|
40764
|
+
* Static on purpose: it is sent before any board is resolved, so it says
|
|
40765
|
+
* nothing about columns — `kanbo_prime` returns those. The rules are the
|
|
40766
|
+
* sentences `ops/agent-rules.ts` holds for every surface, with the tool beside
|
|
40767
|
+
* each; the full manifest stays a resource.
|
|
40768
|
+
*/
|
|
40769
|
+
const KANBO_MCP_INSTRUCTIONS = [
|
|
40770
|
+
"kanbo is a kanban board shared by people and agents; these tools read and change its cards.",
|
|
40771
|
+
"",
|
|
40772
|
+
`- At the start of a session call ${tool("kanbo_prime")} — it returns this board's columns and what each one means.`,
|
|
40773
|
+
`- Take work from ${tool("kanbo_ready")}.`,
|
|
40774
|
+
`- ${MOVE_CARD_RULE} (${tool("kanbo_card_move")})`,
|
|
40775
|
+
`- ${STATUS_LINE_RULE} (${tool("kanbo_status_line")})`,
|
|
40776
|
+
`- ${COMMENT_RULE} (${tool("kanbo_card_comment")})`,
|
|
40777
|
+
`- ${SUBTASK_RULE} (${tool("kanbo_card_create")} with \`parent\`)`,
|
|
40778
|
+
`- ${WAIT_FOR_PERSON_RULE} (${tool("kanbo_wait_approval")})`,
|
|
40779
|
+
`- ${PERSON_ONLY_RULE}`,
|
|
40780
|
+
`- Every tool, rule and canonical string: the resource \`${KANBO_CAPABILITIES_MARKDOWN_RESOURCE_URI}\`.`
|
|
40781
|
+
].join("\n");
|
|
40782
|
+
//#endregion
|
|
40695
40783
|
//#region src/mcp/server.ts
|
|
40696
40784
|
/** The name a client sees this server under. */
|
|
40697
40785
|
const KANBO_MCP_SERVER_NAME = "kanbo";
|
|
@@ -40718,12 +40806,15 @@ function registerKanboTools(server, transport, options) {
|
|
|
40718
40806
|
* function: it documents its capabilities its own way, and adding a second
|
|
40719
40807
|
* `kanbo://` resource namespace to an inventory that is not only the board's
|
|
40720
40808
|
* is a decision for that server to make, not this one.
|
|
40809
|
+
*
|
|
40810
|
+
* For the same reason it alone sends `KANBO_MCP_INSTRUCTIONS` in the initialize
|
|
40811
|
+
* result: the instructions of a server that is only the board.
|
|
40721
40812
|
*/
|
|
40722
40813
|
function createKanboMcpServer(transport, options) {
|
|
40723
40814
|
const server = new McpServer({
|
|
40724
40815
|
name: KANBO_MCP_SERVER_NAME,
|
|
40725
40816
|
version: KANBO_MCP_SERVER_VERSION
|
|
40726
|
-
});
|
|
40817
|
+
}, { instructions: KANBO_MCP_INSTRUCTIONS });
|
|
40727
40818
|
registerKanboTools(server, transport, options);
|
|
40728
40819
|
registerKanboCapabilitiesResources(server);
|
|
40729
40820
|
return server;
|
|
@@ -42512,12 +42603,11 @@ async function startKanboServer(options) {
|
|
|
42512
42603
|
}
|
|
42513
42604
|
/**
|
|
42514
42605
|
* Who may act for a person on this server, said once when it starts: only a
|
|
42515
|
-
* request presenting the token does, and a server
|
|
42516
|
-
*
|
|
42606
|
+
* request presenting the token does, and a server started from an agent's
|
|
42607
|
+
* shell has nobody who may.
|
|
42517
42608
|
*/
|
|
42518
|
-
function describePersonRights(actors
|
|
42609
|
+
function describePersonRights(actors) {
|
|
42519
42610
|
if (!actors.person) return "no request acts for a person (started from an agent's shell)";
|
|
42520
|
-
if (!token) return "no request acts for a person (no token; set KANBO_SERVE_TOKEN to allow it)";
|
|
42521
42611
|
return `a request with the token acts for ${actors.person.id}`;
|
|
42522
42612
|
}
|
|
42523
42613
|
function stripBrackets(host) {
|
|
@@ -42699,12 +42789,58 @@ function sendJson(response, status, value) {
|
|
|
42699
42789
|
response.end(body);
|
|
42700
42790
|
}
|
|
42701
42791
|
//#endregion
|
|
42792
|
+
//#region src/serve/access.ts
|
|
42793
|
+
/** The token given, or one made up for a loopback server; refused off loopback without one. */
|
|
42794
|
+
function resolveServeAccess(host, givenToken) {
|
|
42795
|
+
assertServeBinding(host, givenToken);
|
|
42796
|
+
if (givenToken) return {
|
|
42797
|
+
token: givenToken,
|
|
42798
|
+
generated: false
|
|
42799
|
+
};
|
|
42800
|
+
return {
|
|
42801
|
+
token: (0, node_crypto.randomBytes)(24).toString("base64url"),
|
|
42802
|
+
generated: true
|
|
42803
|
+
};
|
|
42804
|
+
}
|
|
42805
|
+
/**
|
|
42806
|
+
* Hand the person the board page: a link printed once, and opened in the
|
|
42807
|
+
* browser unless they said not to. A generated token rides in the link's
|
|
42808
|
+
* fragment; one they gave is theirs already and is left out.
|
|
42809
|
+
*/
|
|
42810
|
+
function presentServePage(presentation, output) {
|
|
42811
|
+
const { url, access, open } = presentation;
|
|
42812
|
+
if (presentation.agentShell || !presentation.terminal) {
|
|
42813
|
+
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");
|
|
42814
|
+
return;
|
|
42815
|
+
}
|
|
42816
|
+
const link = access.generated ? `${url}/#token=${access.token}` : `${url}/`;
|
|
42817
|
+
output.print(`kanbo serve: board page ${link}`);
|
|
42818
|
+
if (open) output.openInBrowser(link);
|
|
42819
|
+
}
|
|
42820
|
+
/** Open a link in the default browser, without waiting for it and without failing when there is none. */
|
|
42821
|
+
function openInBrowser(url) {
|
|
42822
|
+
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", [
|
|
42823
|
+
"/c",
|
|
42824
|
+
"start",
|
|
42825
|
+
"\"\"",
|
|
42826
|
+
url
|
|
42827
|
+
]] : ["xdg-open", [url]];
|
|
42828
|
+
try {
|
|
42829
|
+
const child = (0, node_child_process.spawn)(command, args, {
|
|
42830
|
+
detached: true,
|
|
42831
|
+
stdio: "ignore",
|
|
42832
|
+
windowsVerbatimArguments: process.platform === "win32"
|
|
42833
|
+
});
|
|
42834
|
+
child.on("error", () => {});
|
|
42835
|
+
child.unref();
|
|
42836
|
+
} catch {}
|
|
42837
|
+
}
|
|
42838
|
+
//#endregion
|
|
42702
42839
|
//#region src/cli/commands/serve.ts
|
|
42703
42840
|
function registerServeCommand(program) {
|
|
42704
|
-
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) => {
|
|
42841
|
+
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) => {
|
|
42705
42842
|
const port = parsePort(options.port);
|
|
42706
|
-
const
|
|
42707
|
-
assertServeBinding(options.host, token);
|
|
42843
|
+
const access = resolveServeAccess(options.host, options.token?.trim() || process.env.KANBO_SERVE_TOKEN?.trim() || null);
|
|
42708
42844
|
const session = await openBoardSession({
|
|
42709
42845
|
db: options.db,
|
|
42710
42846
|
databaseUrl: options.databaseUrl,
|
|
@@ -42722,15 +42858,25 @@ function registerServeCommand(program) {
|
|
|
42722
42858
|
actors,
|
|
42723
42859
|
host: options.host,
|
|
42724
42860
|
port,
|
|
42725
|
-
token,
|
|
42861
|
+
token: access.token,
|
|
42726
42862
|
corsOrigins: options.corsOrigin
|
|
42727
42863
|
});
|
|
42728
42864
|
} catch (error) {
|
|
42729
42865
|
await session.close();
|
|
42730
42866
|
throw error;
|
|
42731
42867
|
}
|
|
42732
|
-
console.error(`kanbo serve: ${server.url} — workspace ${session.workspace.id}${
|
|
42733
|
-
console.error(`kanbo serve: ${describePersonRights(actors
|
|
42868
|
+
console.error(`kanbo serve: ${server.url} — workspace ${session.workspace.id}, bearer token required${access.generated ? " (generated for this run)" : ""}`);
|
|
42869
|
+
console.error(`kanbo serve: ${describePersonRights(actors)}`);
|
|
42870
|
+
presentServePage({
|
|
42871
|
+
url: server.url,
|
|
42872
|
+
access,
|
|
42873
|
+
agentShell,
|
|
42874
|
+
terminal: process.stdout.isTTY === true,
|
|
42875
|
+
open: options.open
|
|
42876
|
+
}, {
|
|
42877
|
+
print: (line) => console.log(line),
|
|
42878
|
+
openInBrowser
|
|
42879
|
+
});
|
|
42734
42880
|
let stopping = false;
|
|
42735
42881
|
const stop = async () => {
|
|
42736
42882
|
if (stopping) return;
|