@bussolabs/closeyourit-cli 0.25.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -4
- package/dist/base.d.ts +3 -18
- package/dist/base.js +16 -66
- package/dist/commands/kb/context.d.ts +12 -0
- package/dist/commands/kb/context.js +93 -0
- package/dist/commands/kb/doctor.d.ts +21 -0
- package/dist/commands/kb/doctor.js +107 -0
- package/dist/commands/kb/search.d.ts +1 -0
- package/dist/commands/kb/search.js +7 -0
- package/dist/commands/mcp.d.ts +10 -0
- package/dist/commands/mcp.js +33 -0
- package/dist/lib/api.d.ts +16 -0
- package/dist/lib/api.js +21 -1
- package/dist/lib/git.d.ts +17 -0
- package/dist/lib/git.js +56 -0
- package/dist/lib/knowledge-context.d.ts +27 -0
- package/dist/lib/knowledge-context.js +68 -0
- package/dist/lib/knowledge-doctor.d.ts +127 -0
- package/dist/lib/knowledge-doctor.js +310 -0
- package/dist/lib/knowledge.d.ts +30 -0
- package/dist/lib/knowledge.js +64 -0
- package/dist/lib/lookup.d.ts +30 -0
- package/dist/lib/lookup.js +82 -0
- package/dist/lib/mcp/protocol.d.ts +51 -0
- package/dist/lib/mcp/protocol.js +67 -0
- package/dist/lib/mcp/server.d.ts +41 -0
- package/dist/lib/mcp/server.js +221 -0
- package/dist/lib/mcp/tools.d.ts +43 -0
- package/dist/lib/mcp/tools.js +340 -0
- package/oclif.manifest.json +4276 -4081
- package/opencli.json +154 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -157,14 +157,16 @@ CLOSEYOURIT_TOKEN=cyi_u_… CLOSEYOURIT_API_URL=https://www.closeyour.it \
|
|
|
157
157
|
| `logs link <id> --to <Errors::Group:uuid\|Ticketing::Ticket:uuid>` | Manually link a log entry to an error group or ticket. |
|
|
158
158
|
| `logs unlink <id> <link-id> --confirm` | Remove a manual log link. |
|
|
159
159
|
| `kb list [--project <id\|key>] [--kind] [--status] [--awaiting-consolidation] [--per] [--page]` | List knowledge pages. Cross-project: omit `--project` for every visible project. Published only unless `--status` says otherwise. |
|
|
160
|
-
| `kb search <query…> [--project <id\|key>] [--kind] [--per] [--page] [--with-related]` | Search knowledge pages (semantic when available, title ILIKE fallback). |
|
|
160
|
+
| `kb search <query…> [--project <id\|key>] [--kind] [--status] [--per] [--page] [--with-related]` | Search knowledge pages (semantic when available, title ILIKE fallback). Accepted pages only unless `--status` says otherwise: `--status all` also searches the proposals waiting for review and the rejected ones, so a duplicate that is not accepted yet shows up before you propose it again. Results carry a `STATUS` column as soon as one row is not published. |
|
|
161
161
|
| `kb show <id> [--related] [--question <q>]` | Show one knowledge page: metadata header (every project it belongs to, its groups and tags), raw markdown body, then the technical section. |
|
|
162
162
|
| `kb related <id> [--question <q>] [--links-only]` | Pages to read next: linked with `[[wiki links]]` plus close matches by meaning. |
|
|
163
|
-
| `kb
|
|
163
|
+
| `kb context [--project <id\|key>] [--limit] [--quiet] [--timeout <seconds>]` | Print the knowledge context of the current project: the short list of pages whoever starts working here should already know. Without `--project` the project is resolved from the `origin` git remote of the current directory (project → repository map). `--quiet` never fails — outside a project it prints nothing, on an unreachable service one line — so a session-start hook can call it blindly. `KB_CONTEXT_DISABLE=1` turns it off. |
|
|
164
|
+
| `kb create [--project <id\|key>…] [--group <id\|name>…] [--tag <tag>…] --title <t> [--kind note\|decision\|guide] (--body <md> \| --body-file <path>) [--tech-spec <md> \| --tech-spec-file <path>] [--in-review] [--review-note <line>] [--author-origin <name>]` | Create a knowledge page (optional technical section, kept separate from the body). `--project`, `--group` and `--tag` are repeatable — a page can belong to several projects and groups at once, and to groups alone; at least one project or group is required. Length caps: title 255, body 4000, technical section 1500, review note 240 characters — every field over the cap is listed at once, before the call. With `--in-review` the page waits for a human and stays out of ordinary search, answers and related panels — `kb search --status in_review\|all` finds it on purpose, so nobody proposes the same page twice. `--author-origin` declares the assistant or skill that wrote the text, which is not the owner of the token. |
|
|
164
165
|
| `kb publish --project <id\|key> --publication-key <key> --title <t> [--kind note\|decision\|guide] (--body <md> \| --body-file <path>)` | Create or update a knowledge page in one atomic call, keyed by a stable `--publication-key` (1-255 URL-safe characters). Safe to repeat: the same key always lands on the same page, and the output says whether it was `created` or `updated`. Exactly one body source is required. Omitting `--kind` keeps the kind the page already has (a new page is a `note`). Same title/body caps as `kb create`. |
|
|
165
166
|
| `kb approve <id>` | Accept a page waiting for review: it enters search, answers and related panels. A human decision: a service token is refused, and a page the automatic reviewer rejected must be fixed and saved first. |
|
|
166
|
-
| `kb reject <id>` | Discard a page waiting for review: it stays archived as rejected, out of search and answers, so it is not proposed again. Nothing is deleted. |
|
|
167
|
+
| `kb reject <id>` | Discard a page waiting for review: it stays archived as rejected, out of ordinary search and answers (`kb search --status rejected\|all` still finds it), so it is not proposed again. Nothing is deleted. |
|
|
167
168
|
| `kb consolidate <id> --path <doc-path>` | Mark an accepted page as written to the versioned docs, recording where the document lives. |
|
|
169
|
+
| `kb doctor [--project <id\|key>] [--stale-days <n>] [--docs-root <path>] [--limit <n>]` | Check the health of the knowledge base: pages no search can find, `[[wiki links]]` pointing nowhere, proposals nobody decided, accepted pages never filed, and — with `--docs-root` — filed documents missing from the repository on this machine. Every check reports its outcome, what it means and the safe next step; nothing is ever changed. Exits 1 when a check fails, 0 on warnings alone. `--project` narrows the scan but marks the two organization-wide checks (`duplicate-title`, `dangling-link`) as skipped, because the service resolves those across every project. `--limit` caps how many pages each listing reads (default 500) and the report says when it stopped short. |
|
|
168
170
|
| `kb update <id> [--title] [--kind] [--body \| --body-file] [--tech-spec \| --tech-spec-file] [--project <id\|key>…] [--group <id\|name>…] [--tag <tag>…] [--author-origin <name>]` | Update a page (unpassed fields keep their current value — including projects, groups, tags and attachments). Passing `--project`, `--group` or `--tag` **replaces** that list with what you name; `--tag ""` clears the tags. Changing only scope or tags never rewrites the text, so a concurrent edit is not overwritten. Same length caps as `kb create`; text already over the cap is refused only if it grows. |
|
|
169
171
|
| `kb delete <id> --confirm` | Delete a knowledge page (irreversible). |
|
|
170
172
|
| `kb attachments <id>` | List every file attached to a knowledge page — all pages, never truncated (`SCRIPT` says whether the server considers it executable). |
|
|
@@ -175,6 +177,7 @@ CLOSEYOURIT_TOKEN=cyi_u_… CLOSEYOURIT_API_URL=https://www.closeyour.it \
|
|
|
175
177
|
| `kb book list [--project <id\|key>] [--q <text>] [--per] [--page]` | List knowledge books (ordered collections of pages). Cross-project: omit `--project` for every visible project. |
|
|
176
178
|
| `kb book show <id>` | Show a book: metadata, then the summary of the pages it collects (`#` is the 0-based position). |
|
|
177
179
|
| `kb book add-page <id> --page <page-id> [--position <n>]` | Add a page to a book's summary. `--page` takes a page **id** (alias: `--page-id`), not a page number; `--position` is 0-based (`0` = top, omitted = last), like the position the summary shows back. Idempotent: repeating it never lists the page twice. |
|
|
180
|
+
| `mcp [--timeout <seconds>]` | Serve the knowledge base over the Model Context Protocol on stdin/stdout, so any MCP-speaking assistant can search it, ask it, read pages and propose new ones with your own access. Started by the client as a subprocess — see [Connecting an assistant (MCP)](#connecting-an-assistant-mcp). |
|
|
178
181
|
| `assistant ask <question…> [--conversation <id>] [--timeout <seconds>]` | Ask the assistant a question about your own data; it reads what you are allowed to see, answers, and lists the tools it used. Prints the conversation id so the next question can keep the context (`--conversation <id>`). |
|
|
179
182
|
| `assistant conversations list [--per] [--page]` | List your conversations with the assistant, most recently active first. |
|
|
180
183
|
| `assistant conversations create [--title]` | Open an empty conversation; without `--title` the server names it after its first question. |
|
|
@@ -285,6 +288,89 @@ backend `R404-KNOWLEDGE-001` is reserved for a project UUID that is missing or n
|
|
|
285
288
|
caller. `R403-KNOWLEDGE-003` means the key already belongs to a page you cannot manage, and
|
|
286
289
|
`R409-KNOWLEDGE-001` that two untagged pages share the title the key would adopt.
|
|
287
290
|
|
|
291
|
+
### Knowledge at the start of a session
|
|
292
|
+
|
|
293
|
+
`kb context` is built to be called by a `SessionStart` hook, so what the project already learned is
|
|
294
|
+
on screen before anyone asks for it. In `--quiet` mode it never gets in the way: a directory that is
|
|
295
|
+
not a project prints nothing, an unreachable service costs one line, and both exit 0.
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
#!/usr/bin/env bash
|
|
299
|
+
# SessionStart hook: stdout becomes the session's additional context.
|
|
300
|
+
command -v cyi >/dev/null 2>&1 || exit 0
|
|
301
|
+
cyi kb context --quiet --limit 5 --timeout 3 2>/dev/null
|
|
302
|
+
exit 0
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`kb context` reads the context route of the backend and falls back to the project's page list on a
|
|
306
|
+
server that does not expose it yet, so the list shows up either way.
|
|
307
|
+
|
|
308
|
+
### Connecting an assistant (MCP)
|
|
309
|
+
|
|
310
|
+
`cyi mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server on stdio: the same
|
|
311
|
+
procedure works for every assistant that speaks MCP, so reaching the knowledge base no longer means
|
|
312
|
+
writing instructions for one particular tool.
|
|
313
|
+
|
|
314
|
+
Most clients declare their servers in a JSON file. Add this entry under the key your client uses —
|
|
315
|
+
`mcpServers` for Claude Code, Cursor, Windsurf and most others, `context_servers` for
|
|
316
|
+
[Zed](https://zed.dev/docs/ai/mcp):
|
|
317
|
+
|
|
318
|
+
```json
|
|
319
|
+
{
|
|
320
|
+
"mcpServers": {
|
|
321
|
+
"closeyourit": {
|
|
322
|
+
"command": "cyi",
|
|
323
|
+
"args": ["mcp"]
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
The client launches it as a subprocess; there is nothing to start or keep running by hand.
|
|
330
|
+
|
|
331
|
+
It authenticates exactly like the terminal does, with the token `cyi login` already stored in
|
|
332
|
+
`~/.config/closeyourit/config.json` (mode 0600). In the ordinary case the entry above is the whole
|
|
333
|
+
configuration and no secret goes anywhere near it.
|
|
334
|
+
|
|
335
|
+
For a service account, keep the token out of that file too — a client configuration is a dotfile
|
|
336
|
+
like any other and frequently ends up committed. Export `CLOSEYOURIT_TOKEN` into the environment the
|
|
337
|
+
client is started from, taking it from wherever the secret already lives, rather than writing the
|
|
338
|
+
value into JSON:
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
CLOSEYOURIT_TOKEN=cyi_u_… your-editor-or-agent
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Six tools are exposed, each one the call its command already makes:
|
|
345
|
+
|
|
346
|
+
| Tool | What it does |
|
|
347
|
+
| --- | --- |
|
|
348
|
+
| `kb_search` | Search the pages (`kb search`). Accepted pages only, unless `status` says otherwise — `"all"` also finds the proposals waiting for review. |
|
|
349
|
+
| `kb_ask` | Ask a question and get an answer with its sources (`kb ask`). |
|
|
350
|
+
| `kb_show` | Read one page in full (`kb show`). |
|
|
351
|
+
| `kb_related` | The pages to read next (`kb related`). |
|
|
352
|
+
| `kb_context` | What to know about the project of the working directory (`kb context`). |
|
|
353
|
+
| `kb_propose` | Propose a new page — **always** into the review queue (`kb create --in-review`). |
|
|
354
|
+
|
|
355
|
+
Two things hold whatever the assistant asks for. `kb_propose` sends `in_review` and nothing else:
|
|
356
|
+
the page waits for a person to accept it and stays out of ordinary search, answers and related
|
|
357
|
+
panels until then — `kb_search` with `status: "all"` looks it up on purpose, before proposing a
|
|
358
|
+
duplicate. There is no tool here that publishes, accepts, edits or deletes. And every call travels on
|
|
359
|
+
your own token, so a project your access does not include returns nothing, exactly as it does from
|
|
360
|
+
the terminal.
|
|
361
|
+
|
|
362
|
+
`kb_context` recognises the project from the git remote of the directory the client started the
|
|
363
|
+
server in. Clients that launch it elsewhere can name the project instead (`project`), or pass it to
|
|
364
|
+
every call.
|
|
365
|
+
|
|
366
|
+
To check the server by hand, talk to it the way a client would:
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
printf '%s\n' \
|
|
370
|
+
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' \
|
|
371
|
+
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | cyi mcp
|
|
372
|
+
```
|
|
373
|
+
|
|
288
374
|
When a command fails in `--json` mode it exits non-zero and prints an error envelope on stdout —
|
|
289
375
|
never a silent success:
|
|
290
376
|
|
|
@@ -315,9 +401,10 @@ to clear it locally; revoke the token on the server at `/account/cli/tokens`.
|
|
|
315
401
|
|
|
316
402
|
| Variable | Purpose | Default |
|
|
317
403
|
| --- | --- | --- |
|
|
318
|
-
| `CLOSEYOURIT_TOKEN` | User token (`cyi_u_…`) for headless/CI/agent use; overrides the stored config token. | — |
|
|
404
|
+
| `CLOSEYOURIT_TOKEN` | User token (`cyi_u_…`) for headless/CI/agent use, including the `mcp` server; overrides the stored config token. | — |
|
|
319
405
|
| `CLOSEYOURIT_API_URL` | Default backend base URL (overridden by a persisted `--api-url`). | `https://www.closeyour.it` |
|
|
320
406
|
| `CLOSEYOURIT_CONFIG_DIR` | Directory holding `config.json`. | `~/.config/closeyourit` |
|
|
407
|
+
| `KB_CONTEXT_DISABLE` | Set to `1` to make `kb context` print nothing and call nothing (the switch a session-start hook honours). | — |
|
|
321
408
|
|
|
322
409
|
## Development
|
|
323
410
|
|
package/dist/base.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ArgOutput, FlagOutput, Input, ParserOutput } from '@oclif/core/lib
|
|
|
3
3
|
import { type CliConfig } from './lib/config';
|
|
4
4
|
import { CliApi } from './lib/api';
|
|
5
5
|
import { type TicketLookup } from './lib/ticket-lookup';
|
|
6
|
-
export
|
|
6
|
+
export { UUID_RE } from './lib/lookup';
|
|
7
7
|
/** Shared `--project <id|key>` flag (required). */
|
|
8
8
|
export declare const projectFlag: {
|
|
9
9
|
project: Interfaces.OptionFlag<string, Interfaces.CustomOptions>;
|
|
@@ -43,11 +43,7 @@ export declare abstract class BaseCommand extends Command {
|
|
|
43
43
|
init(): Promise<void>;
|
|
44
44
|
/** Parse as oclif does, then hand `--yes` to the api client before any request leaves. */
|
|
45
45
|
protected parse<F extends FlagOutput, B extends FlagOutput, A extends ArgOutput>(options?: Input<F, B, A>, argv?: string[]): Promise<ParserOutput<F, B, A>>;
|
|
46
|
-
/**
|
|
47
|
-
* Resolve a project reference (UUID passes through; otherwise matched by key, case-insensitive).
|
|
48
|
-
* Walks every page of `/cli/v1/projects`: a key can live past the first page, so a single-page
|
|
49
|
-
* lookup would 404 keys like legalbloom LBRA/LBWB while their UUID (and first-page keys) worked (CYCL-1).
|
|
50
|
-
*/
|
|
46
|
+
/** Resolve a project reference (UUID passes through; otherwise matched by key, case-insensitive). */
|
|
51
47
|
protected resolveProjectId(value: string): Promise<string>;
|
|
52
48
|
/** Resolve a role reference (UUID passes through; otherwise matched by name, case-insensitive). */
|
|
53
49
|
protected resolveRoleId(value: string): Promise<string>;
|
|
@@ -81,18 +77,7 @@ export declare abstract class BaseCommand extends Command {
|
|
|
81
77
|
protected resolveAll(values: string[], resolve: (value: string) => Promise<string>): Promise<string[]>;
|
|
82
78
|
/** Resolve an `{id, name}` reference by name (UUID passes through). */
|
|
83
79
|
private resolveByName;
|
|
84
|
-
/**
|
|
85
|
-
* Every row of an organization lookup (teams, roles, groups, members) — all its pages.
|
|
86
|
-
*
|
|
87
|
-
* The server serves ten per page: stopping at the first made the eleventh team or the eleventh
|
|
88
|
-
* person invisible, and a perfectly good name or email came back as "not found" (CYCL-42, the
|
|
89
|
-
* lesson `resolveProjectId` already learned on project keys in CYCL-1).
|
|
90
|
-
*
|
|
91
|
-
* A 403 here is not about the command being run: listing teams needs `permissions.manage` and
|
|
92
|
-
* listing members `members.view`, permissions that a team workload board never asks for. Left
|
|
93
|
-
* alone it would read as "Permesso negato" on an operation the caller is entitled to, so the
|
|
94
|
-
* refusal carries the way out instead — name the id, skip the lookup.
|
|
95
|
-
*/
|
|
80
|
+
/** Every row of an organization lookup (teams, roles, groups, members) — all its pages. */
|
|
96
81
|
private fetchLookupRows;
|
|
97
82
|
/**
|
|
98
83
|
* Surface API errors, then exit non-zero. In `--json` mode `logToStderr` is a no-op, so emit the
|
package/dist/base.js
CHANGED
|
@@ -9,7 +9,11 @@ const output_1 = require("./lib/output");
|
|
|
9
9
|
const error_codes_1 = require("./errors/error-codes");
|
|
10
10
|
const input_error_1 = require("./errors/input-error");
|
|
11
11
|
const ticket_lookup_1 = require("./lib/ticket-lookup");
|
|
12
|
-
|
|
12
|
+
const lookup_1 = require("./lib/lookup");
|
|
13
|
+
// La ricerca per riferimento leggibile sta in `lib/lookup`: la usano i comandi da qui e il server
|
|
14
|
+
// MCP direttamente (CYCL-56). Ri-esportata perché è già il nome che i comandi importano da `base`.
|
|
15
|
+
var lookup_2 = require("./lib/lookup");
|
|
16
|
+
Object.defineProperty(exports, "UUID_RE", { enumerable: true, get: function () { return lookup_2.UUID_RE; } });
|
|
13
17
|
/** Shared `--project <id|key>` flag (required). */
|
|
14
18
|
exports.projectFlag = {
|
|
15
19
|
project: core_1.Flags.string({ char: 'p', description: 'Project id (UUID) or key', required: true }),
|
|
@@ -68,26 +72,9 @@ class BaseCommand extends core_1.Command {
|
|
|
68
72
|
this.api.confirm = parsed.flags.yes === true;
|
|
69
73
|
return parsed;
|
|
70
74
|
}
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
* lookup would 404 keys like legalbloom LBRA/LBWB while their UUID (and first-page keys) worked (CYCL-1).
|
|
75
|
-
*/
|
|
76
|
-
async resolveProjectId(value) {
|
|
77
|
-
if (exports.UUID_RE.test(value))
|
|
78
|
-
return value;
|
|
79
|
-
let page = 1;
|
|
80
|
-
let totalPages = 1;
|
|
81
|
-
do {
|
|
82
|
-
// eslint-disable-next-line no-await-in-loop
|
|
83
|
-
const res = await this.api.get(`/cli/v1/projects?page=${page}&per=100`);
|
|
84
|
-
const match = (res.data ?? []).find((project) => (project.key ?? '').toLowerCase() === value.toLowerCase());
|
|
85
|
-
if (match)
|
|
86
|
-
return match.id;
|
|
87
|
-
totalPages = Number(res.meta?.total_pages ?? 1);
|
|
88
|
-
page += 1;
|
|
89
|
-
} while (page <= totalPages);
|
|
90
|
-
throw new api_1.ApiRequestError(404, error_codes_1.ErrorCodes.Project.notFound, `Project not found: ${value}`);
|
|
75
|
+
/** Resolve a project reference (UUID passes through; otherwise matched by key, case-insensitive). */
|
|
76
|
+
resolveProjectId(value) {
|
|
77
|
+
return (0, lookup_1.resolveProjectId)(this.api, value);
|
|
91
78
|
}
|
|
92
79
|
/** Resolve a role reference (UUID passes through; otherwise matched by name, case-insensitive). */
|
|
93
80
|
resolveRoleId(value) {
|
|
@@ -103,7 +90,7 @@ class BaseCommand extends core_1.Command {
|
|
|
103
90
|
}
|
|
104
91
|
/** Resolve a member reference (UUID account id passes through; otherwise matched by email). */
|
|
105
92
|
async resolveMemberId(value) {
|
|
106
|
-
if (
|
|
93
|
+
if (lookup_1.UUID_RE.test(value))
|
|
107
94
|
return value;
|
|
108
95
|
const rows = await this.fetchLookupRows('/cli/v1/members', 'Listing the organization members needs the members.view permission — pass the account id instead of the email.');
|
|
109
96
|
const match = rows.find((m) => (m.email ?? '').toLowerCase() === value.toLowerCase());
|
|
@@ -123,7 +110,7 @@ class BaseCommand extends core_1.Command {
|
|
|
123
110
|
*/
|
|
124
111
|
async resolveTicketId(value) {
|
|
125
112
|
const ref = value.trim();
|
|
126
|
-
if (
|
|
113
|
+
if (lookup_1.UUID_RE.test(ref))
|
|
127
114
|
return ref;
|
|
128
115
|
// Stessa forma che il server accetta come code: chiave progetto (max 4) + numero.
|
|
129
116
|
const match = /^([a-z\d]{1,4})-\d+$/i.exec(ref);
|
|
@@ -149,7 +136,7 @@ class BaseCommand extends core_1.Command {
|
|
|
149
136
|
*/
|
|
150
137
|
async resolveTicketLookupId(lookup, value) {
|
|
151
138
|
const wanted = value ?? lookup.fallback;
|
|
152
|
-
if (
|
|
139
|
+
if (lookup_1.UUID_RE.test(wanted))
|
|
153
140
|
return wanted;
|
|
154
141
|
const rows = (await this.api.get(lookup.path)).data;
|
|
155
142
|
const matches = (0, ticket_lookup_1.matchTicketLookup)(rows, wanted);
|
|
@@ -162,49 +149,12 @@ class BaseCommand extends core_1.Command {
|
|
|
162
149
|
return Promise.all(values.map((value) => resolve(value)));
|
|
163
150
|
}
|
|
164
151
|
/** Resolve an `{id, name}` reference by name (UUID passes through). */
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return value;
|
|
168
|
-
const noun = label.toLowerCase();
|
|
169
|
-
const rows = await this.fetchLookupRows(path, `Listing the ${noun}s needs the permissions.manage permission — pass the ${noun} id instead of its name.`);
|
|
170
|
-
const match = rows.find((row) => (row.name ?? '').toLowerCase() === value.toLowerCase());
|
|
171
|
-
if (!match) {
|
|
172
|
-
throw new api_1.ApiRequestError(404, code, `${label} not found: ${value}`);
|
|
173
|
-
}
|
|
174
|
-
return match.id;
|
|
152
|
+
resolveByName(path, value, code, label) {
|
|
153
|
+
return (0, lookup_1.resolveNamedId)(this.api, path, value, code, label);
|
|
175
154
|
}
|
|
176
|
-
/**
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
* The server serves ten per page: stopping at the first made the eleventh team or the eleventh
|
|
180
|
-
* person invisible, and a perfectly good name or email came back as "not found" (CYCL-42, the
|
|
181
|
-
* lesson `resolveProjectId` already learned on project keys in CYCL-1).
|
|
182
|
-
*
|
|
183
|
-
* A 403 here is not about the command being run: listing teams needs `permissions.manage` and
|
|
184
|
-
* listing members `members.view`, permissions that a team workload board never asks for. Left
|
|
185
|
-
* alone it would read as "Permesso negato" on an operation the caller is entitled to, so the
|
|
186
|
-
* refusal carries the way out instead — name the id, skip the lookup.
|
|
187
|
-
*/
|
|
188
|
-
async fetchLookupRows(path, hint) {
|
|
189
|
-
const rows = [];
|
|
190
|
-
let page = 1;
|
|
191
|
-
let totalPages = 1;
|
|
192
|
-
try {
|
|
193
|
-
do {
|
|
194
|
-
// eslint-disable-next-line no-await-in-loop
|
|
195
|
-
const res = await this.api.get(`${path}?page=${page}&per=100`);
|
|
196
|
-
rows.push(...(res.data ?? []));
|
|
197
|
-
totalPages = Number(res.meta?.total_pages ?? 1);
|
|
198
|
-
page += 1;
|
|
199
|
-
} while (page <= totalPages);
|
|
200
|
-
}
|
|
201
|
-
catch (error) {
|
|
202
|
-
if (error instanceof api_1.ApiRequestError && error.status === 403) {
|
|
203
|
-
throw new api_1.ApiRequestError(error.status, error.code, `${error.message}. ${hint}`);
|
|
204
|
-
}
|
|
205
|
-
throw error;
|
|
206
|
-
}
|
|
207
|
-
return rows;
|
|
155
|
+
/** Every row of an organization lookup (teams, roles, groups, members) — all its pages. */
|
|
156
|
+
fetchLookupRows(path, hint) {
|
|
157
|
+
return (0, lookup_1.fetchLookupRows)(this.api, path, hint);
|
|
208
158
|
}
|
|
209
159
|
/**
|
|
210
160
|
* Surface API errors, then exit non-zero. In `--json` mode `logToStderr` is a no-op, so emit the
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base';
|
|
2
|
+
export default class KbContext extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
project: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
7
|
+
limit: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
8
|
+
quiet: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
timeout: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<unknown>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const core_1 = require("@oclif/core");
|
|
4
|
+
const base_1 = require("../../base");
|
|
5
|
+
const api_1 = require("../../lib/api");
|
|
6
|
+
const knowledge_context_1 = require("../../lib/knowledge-context");
|
|
7
|
+
const knowledge_1 = require("../../lib/knowledge");
|
|
8
|
+
/** Environment switch that turns the context off wherever it is invoked from, hook included. */
|
|
9
|
+
const DISABLE_ENV = 'KB_CONTEXT_DISABLE';
|
|
10
|
+
/** The answer when there is nothing to show: no project here, switched off, or service unreachable. */
|
|
11
|
+
function nothing(unavailable = false) {
|
|
12
|
+
return { data: { project: null, pages: [], ...(unavailable ? { unavailable: true } : {}) } };
|
|
13
|
+
}
|
|
14
|
+
class KbContext extends base_1.BaseCommand {
|
|
15
|
+
static description = 'Print the knowledge context of the current project: the short list of pages whoever starts working here should already know';
|
|
16
|
+
static examples = [
|
|
17
|
+
'<%= config.bin %> kb context',
|
|
18
|
+
'<%= config.bin %> kb context --project ACME --limit 5',
|
|
19
|
+
'<%= config.bin %> kb context --quiet --timeout 3',
|
|
20
|
+
];
|
|
21
|
+
static flags = {
|
|
22
|
+
// Not the shared `projectFlag`: omitted, the project is resolved from the git remote of the
|
|
23
|
+
// current directory, which is the whole point of a context that shows up on its own (CYCL-55).
|
|
24
|
+
project: core_1.Flags.string({
|
|
25
|
+
char: 'p',
|
|
26
|
+
description: 'Project id (UUID) or key; omitted, it is resolved from the git remote of the current directory',
|
|
27
|
+
}),
|
|
28
|
+
limit: core_1.Flags.integer({ description: 'How many pages to print at most', default: 10 }),
|
|
29
|
+
quiet: core_1.Flags.boolean({
|
|
30
|
+
description: 'Never fail: outside a project print nothing, on an unreachable service print one line — always exit 0 (for session-start hooks)',
|
|
31
|
+
}),
|
|
32
|
+
timeout: core_1.Flags.integer({ description: 'Seconds to wait for the service before giving up (0 = no limit)', default: 5 }),
|
|
33
|
+
};
|
|
34
|
+
async run() {
|
|
35
|
+
// Read before the command line is even parsed: switched off means switched off, not "off unless
|
|
36
|
+
// the command line is also wrong". A hook written for a newer CLI would otherwise be refused by
|
|
37
|
+
// an older one — loudly, at every session start, with the switch that should have silenced it
|
|
38
|
+
// already set.
|
|
39
|
+
if (process.env[DISABLE_ENV] === '1') {
|
|
40
|
+
// Parsed anyway, failure and all: oclif warns about a command that never parsed its arguments,
|
|
41
|
+
// and a warning on stderr is exactly the noise the switch was set to avoid.
|
|
42
|
+
await this.parse(KbContext).catch(() => undefined);
|
|
43
|
+
return nothing();
|
|
44
|
+
}
|
|
45
|
+
const { flags } = await this.parse(KbContext);
|
|
46
|
+
if (flags.limit < 1)
|
|
47
|
+
this.error(`Invalid --limit ${flags.limit}: ask for at least 1 page.`, { exit: 2 });
|
|
48
|
+
// One deadline for the whole command, not one per call: two requests with a timeout each would
|
|
49
|
+
// let a slow server cost twice the wait the caller agreed to.
|
|
50
|
+
const signal = flags.timeout > 0 ? AbortSignal.timeout(flags.timeout * 1000) : undefined;
|
|
51
|
+
const opts = signal ? { signal } : {};
|
|
52
|
+
try {
|
|
53
|
+
const project = flags.project ?? (await (0, knowledge_context_1.projectFromGitRemote)(this.api, process.cwd(), flags.timeout * 1000, opts));
|
|
54
|
+
if (project === undefined) {
|
|
55
|
+
// Scenario 2: a directory that is not a project is a normal state, not a failure.
|
|
56
|
+
if (!flags.quiet && !this.jsonEnabled())
|
|
57
|
+
this.log('No CloseYourIt project matches this directory.');
|
|
58
|
+
return nothing();
|
|
59
|
+
}
|
|
60
|
+
const answer = await (0, knowledge_context_1.fetchKnowledgeContext)(this.api, project, flags.limit, opts);
|
|
61
|
+
// The cap holds on both outputs — printed list and JSON envelope — so `--limit 5` means five
|
|
62
|
+
// pages however the server chose to interpret `per`.
|
|
63
|
+
const res = { ...answer, data: (0, knowledge_1.capContextPages)(answer.data, flags.limit) };
|
|
64
|
+
if (!this.jsonEnabled()) {
|
|
65
|
+
const pages = (0, knowledge_1.contextPages)(res.data);
|
|
66
|
+
if (pages.length === 0) {
|
|
67
|
+
if (!flags.quiet)
|
|
68
|
+
this.log('No knowledge pages for this project yet.');
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
this.log((0, knowledge_1.renderKnowledgeContext)(pages, (0, knowledge_1.contextProjectLabel)(res.data, project)));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return res;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// Scenario 3: with --quiet an unreachable (or unauthenticated) service costs one line and a
|
|
78
|
+
// zero exit — the session starts anyway. Anything that is not an API failure still surfaces.
|
|
79
|
+
if (!flags.quiet || !(error instanceof api_1.ApiRequestError))
|
|
80
|
+
throw error;
|
|
81
|
+
if (this.jsonEnabled())
|
|
82
|
+
this.logJson(nothing(true));
|
|
83
|
+
else
|
|
84
|
+
this.log('Knowledge context unavailable.');
|
|
85
|
+
// Exit instead of returning: a connection that never completes leaves its socket — and the
|
|
86
|
+
// event loop — alive until the OS gives up (~10s measured), well past the abort that already
|
|
87
|
+
// produced the line above. Returning would hand the caller exactly the wait the deadline was
|
|
88
|
+
// there to prevent, only after the message instead of before it.
|
|
89
|
+
return this.exit(0);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
exports.default = KbContext;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base';
|
|
2
|
+
/**
|
|
3
|
+
* Il controllo di salute della conoscenza (CYCL-57): dice quali pagine nessuno troverà mai, quali
|
|
4
|
+
* collegamenti sono rimasti appesi e quali code sono ferme da settimane.
|
|
5
|
+
*
|
|
6
|
+
* Legge e basta. Ogni verifica porta il comando che la sistemerebbe, ma non lo esegue: la
|
|
7
|
+
* riparazione resta una scelta esplicita di chi legge il referto.
|
|
8
|
+
*/
|
|
9
|
+
export default class KbDoctor extends BaseCommand {
|
|
10
|
+
static description: string;
|
|
11
|
+
static examples: string[];
|
|
12
|
+
static flags: {
|
|
13
|
+
project: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
14
|
+
'stale-days': import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
15
|
+
'docs-root': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
16
|
+
limit: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<unknown>;
|
|
19
|
+
/** Il percorso è una cartella che esiste qui? Un percorso illeggibile vale come inesistente. */
|
|
20
|
+
private isDirectory;
|
|
21
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const node_fs_1 = require("node:fs");
|
|
4
|
+
const core_1 = require("@oclif/core");
|
|
5
|
+
const base_1 = require("../../base");
|
|
6
|
+
const knowledge_doctor_1 = require("../../lib/knowledge-doctor");
|
|
7
|
+
/**
|
|
8
|
+
* Il controllo di salute della conoscenza (CYCL-57): dice quali pagine nessuno troverà mai, quali
|
|
9
|
+
* collegamenti sono rimasti appesi e quali code sono ferme da settimane.
|
|
10
|
+
*
|
|
11
|
+
* Legge e basta. Ogni verifica porta il comando che la sistemerebbe, ma non lo esegue: la
|
|
12
|
+
* riparazione resta una scelta esplicita di chi legge il referto.
|
|
13
|
+
*/
|
|
14
|
+
class KbDoctor extends base_1.BaseCommand {
|
|
15
|
+
static description = 'Check the health of the knowledge base: unfindable pages, dangling links, queues nobody moved. Reads only — exits 1 when a check fails';
|
|
16
|
+
static examples = [
|
|
17
|
+
'<%= config.bin %> kb doctor',
|
|
18
|
+
'<%= config.bin %> kb doctor --project acme-api',
|
|
19
|
+
'<%= config.bin %> kb doctor --docs-root ~/knowledge-base',
|
|
20
|
+
'<%= config.bin %> kb doctor --stale-days 14 --json',
|
|
21
|
+
];
|
|
22
|
+
static flags = {
|
|
23
|
+
// Facoltativo come su `kb list`: senza, il controllo guarda ogni progetto visibile.
|
|
24
|
+
project: core_1.Flags.string({ char: 'p', description: 'Only the pages of this project (key or UUID)' }),
|
|
25
|
+
'stale-days': core_1.Flags.integer({ description: 'Days after which a queue counts as stuck', default: 30 }),
|
|
26
|
+
'docs-root': core_1.Flags.string({
|
|
27
|
+
description: 'Path of the knowledge-base repository, to check that the filed documents are really there',
|
|
28
|
+
}),
|
|
29
|
+
// Il tetto vale per CIASCUNA delle due letture (l'elenco e la coda di archiviazione), non per
|
|
30
|
+
// il loro totale: detto qui, perché chi lo alza sappia quanto sta davvero chiedendo.
|
|
31
|
+
limit: core_1.Flags.integer({ description: 'How many pages to read at most, per listing', default: 500 }),
|
|
32
|
+
};
|
|
33
|
+
async run() {
|
|
34
|
+
const { flags } = await this.parse(KbDoctor);
|
|
35
|
+
if (flags.limit < 1)
|
|
36
|
+
this.error(`Invalid --limit ${flags.limit}: examine at least 1 page.`, { exit: 2 });
|
|
37
|
+
if (flags['stale-days'] < 1)
|
|
38
|
+
this.error(`Invalid --stale-days ${flags['stale-days']}: use at least 1 day.`, { exit: 2 });
|
|
39
|
+
// 6 — Il documento archiviato è un FILE. `existsSync` direbbe di sì anche per una cartella, e
|
|
40
|
+
// una pagina che punta a una directory passerebbe il controllo che esiste apposta per accorgersene.
|
|
41
|
+
const isFile = (path) => {
|
|
42
|
+
try {
|
|
43
|
+
return (0, node_fs_1.statSync)(path).isFile();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
// Una radice sbagliata non deve produrre un referto: senza questo controllo, un percorso battuto
|
|
50
|
+
// male segnalerebbe come mancante OGNI documento del parco, che è l'allarme inventato più grosso
|
|
51
|
+
// che questo comando possa dare.
|
|
52
|
+
const docsRoot = flags['docs-root'];
|
|
53
|
+
if (docsRoot !== undefined && !this.isDirectory(docsRoot)) {
|
|
54
|
+
this.error(`Invalid --docs-root ${docsRoot}: it is not a directory that exists here.`, { exit: 2 });
|
|
55
|
+
}
|
|
56
|
+
const scan = await (0, knowledge_doctor_1.fetchDoctorPages)(this.api, { limit: flags.limit, project: flags.project });
|
|
57
|
+
// Il servizio è l'unico a sapere quali pagine ASPETTANO davvero di essere archiviate: la coda
|
|
58
|
+
// esclude quelle scritte a mano dal web, che non hanno mai chiesto niente a nessuno. Derivarla
|
|
59
|
+
// qui da `consolidated_at` vuoto avrebbe fatto nascere l'elenco con decine di righe innocenti.
|
|
60
|
+
const awaiting = await (0, knowledge_doctor_1.fetchDoctorPages)(this.api, {
|
|
61
|
+
awaitingConsolidation: true,
|
|
62
|
+
limit: flags.limit,
|
|
63
|
+
project: flags.project,
|
|
64
|
+
});
|
|
65
|
+
const checks = (0, knowledge_doctor_1.runDoctorChecks)({
|
|
66
|
+
awaitingConsolidation: awaiting.pages,
|
|
67
|
+
docsRoot,
|
|
68
|
+
fileExists: docsRoot === undefined ? undefined : isFile,
|
|
69
|
+
now: new Date(),
|
|
70
|
+
pages: scan.pages,
|
|
71
|
+
scopedToProject: flags.project !== undefined,
|
|
72
|
+
staleDays: flags['stale-days'],
|
|
73
|
+
});
|
|
74
|
+
const report = {
|
|
75
|
+
scanned: scan.pages.length,
|
|
76
|
+
truncated: scan.truncated || awaiting.truncated,
|
|
77
|
+
stale_days: flags['stale-days'],
|
|
78
|
+
checks,
|
|
79
|
+
};
|
|
80
|
+
const totals = (0, knowledge_doctor_1.doctorTotals)(checks);
|
|
81
|
+
const envelope = { data: report, meta: totals };
|
|
82
|
+
if (!this.jsonEnabled())
|
|
83
|
+
this.log((0, knowledge_doctor_1.renderDoctorReport)(report));
|
|
84
|
+
// Esce non-zero quando una verifica è fallita — come lo script di audit che questo comando
|
|
85
|
+
// porta dentro il prodotto — così una pipeline può fermarsi su una conoscenza rotta. Un avviso
|
|
86
|
+
// non basta: segnala una coda ferma, non una pagina irraggiungibile.
|
|
87
|
+
if (totals.failures > 0) {
|
|
88
|
+
// In modalità dati l'envelope va emesso a mano: `this.exit` scarta il valore di ritorno, e
|
|
89
|
+
// uscire in silenzio darebbe a chi legge da un programma un'uscita vuota indistinguibile da
|
|
90
|
+
// un guasto.
|
|
91
|
+
if (this.jsonEnabled())
|
|
92
|
+
this.logJson(envelope);
|
|
93
|
+
return this.exit(1);
|
|
94
|
+
}
|
|
95
|
+
return envelope;
|
|
96
|
+
}
|
|
97
|
+
/** Il percorso è una cartella che esiste qui? Un percorso illeggibile vale come inesistente. */
|
|
98
|
+
isDirectory(path) {
|
|
99
|
+
try {
|
|
100
|
+
return (0, node_fs_1.statSync)(path).isDirectory();
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
exports.default = KbDoctor;
|
|
@@ -10,6 +10,7 @@ export default class KbSearch extends BaseCommand {
|
|
|
10
10
|
page: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
11
11
|
project: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
12
12
|
kind: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
13
|
+
status: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
13
14
|
per: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
14
15
|
'with-related': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
15
16
|
};
|
|
@@ -12,6 +12,7 @@ class KbSearch extends base_1.BaseCommand {
|
|
|
12
12
|
'<%= config.bin %> kb search "connection pool timeout" --project acme-api',
|
|
13
13
|
'<%= config.bin %> kb search kamal rollback --with-related',
|
|
14
14
|
'<%= config.bin %> kb search retry backoff --kind decision --json',
|
|
15
|
+
'<%= config.bin %> kb search cache in prova --status all',
|
|
15
16
|
];
|
|
16
17
|
// Non-required: with strict = false the arg validation is off, so we guard the empty case ourselves.
|
|
17
18
|
static args = {
|
|
@@ -20,6 +21,11 @@ class KbSearch extends base_1.BaseCommand {
|
|
|
20
21
|
static flags = {
|
|
21
22
|
project: core_1.Flags.string({ char: 'p', description: 'Filter by project (key or UUID)' }),
|
|
22
23
|
kind: core_1.Flags.string({ description: 'Filter by kind: note|decision|guide (repeatable or comma-separated)', multiple: true }),
|
|
24
|
+
// Single value, exactly like `kb list --status`: the endpoint reads one `status=` and answers
|
|
25
|
+
// R422-KNOWLEDGE-011 to anything else, so a repeatable flag would only build queries the server
|
|
26
|
+
// rejects. `all` is the one that looks everywhere at once — the way to spot a duplicate that
|
|
27
|
+
// exists as a proposal without running a second command.
|
|
28
|
+
status: core_1.Flags.string({ description: 'Filter by review state', options: [...knowledge_1.KNOWLEDGE_STATUSES] }),
|
|
23
29
|
per: core_1.Flags.integer({ description: 'Page size' }),
|
|
24
30
|
// Opt-in: the server only computes this when asked, for the first few results.
|
|
25
31
|
'with-related': core_1.Flags.boolean({ description: 'Also show the pages each result links to' }),
|
|
@@ -37,6 +43,7 @@ class KbSearch extends base_1.BaseCommand {
|
|
|
37
43
|
project: flags.project,
|
|
38
44
|
q,
|
|
39
45
|
related: flags['with-related'],
|
|
46
|
+
status: flags.status,
|
|
40
47
|
});
|
|
41
48
|
const res = await this.api.get(`/cli/v1/knowledge/pages?${query}`);
|
|
42
49
|
const pages = res.data ?? [];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseCommand } from '../base';
|
|
2
|
+
export default class Mcp extends BaseCommand {
|
|
3
|
+
static enableJsonFlag: boolean;
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
timeout: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
8
|
+
};
|
|
9
|
+
run(): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const core_1 = require("@oclif/core");
|
|
4
|
+
const base_1 = require("../base");
|
|
5
|
+
const server_1 = require("../lib/mcp/server");
|
|
6
|
+
class Mcp extends base_1.BaseCommand {
|
|
7
|
+
// Niente `--json`: su questo comando lo standard output è il canale del protocollo, e una busta
|
|
8
|
+
// in più alla fine sarebbe l'unica riga che il client non sa leggere.
|
|
9
|
+
static enableJsonFlag = false;
|
|
10
|
+
static description = 'Serve the knowledge base over the Model Context Protocol on stdin/stdout, so any assistant that speaks MCP can search it, ask it and propose pages — with your own access and no instructions written for it';
|
|
11
|
+
static examples = [
|
|
12
|
+
'<%= config.bin %> mcp',
|
|
13
|
+
'CLOSEYOURIT_TOKEN=cyi_u_… <%= config.bin %> mcp',
|
|
14
|
+
];
|
|
15
|
+
static flags = {
|
|
16
|
+
timeout: core_1.Flags.integer({
|
|
17
|
+
description: 'Seconds to wait for git when recognising the project of the current directory',
|
|
18
|
+
default: 5,
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
async run() {
|
|
22
|
+
const { flags } = await this.parse(Mcp);
|
|
23
|
+
// Il server vive finché il client tiene aperto il suo standard input: è il client a lanciarlo
|
|
24
|
+
// come sottoprocesso e a chiuderlo, non c'è nulla da stampare prima né dopo.
|
|
25
|
+
await (0, server_1.serveStdio)(process.stdin, process.stdout, (0, server_1.createSession)({
|
|
26
|
+
api: this.api,
|
|
27
|
+
version: this.config.version,
|
|
28
|
+
cwd: process.cwd(),
|
|
29
|
+
timeoutMs: flags.timeout * 1000,
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.default = Mcp;
|