@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 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 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 search, answers and related panels. `--author-origin` declares the assistant or skill that wrote the text, which is not the owner of the token. |
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 declare const UUID_RE: RegExp;
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
- exports.UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
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
- * Resolve a project reference (UUID passes through; otherwise matched by key, case-insensitive).
73
- * Walks every page of `/cli/v1/projects`: a key can live past the first page, so a single-page
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 (exports.UUID_RE.test(value))
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 (exports.UUID_RE.test(ref))
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 (exports.UUID_RE.test(wanted))
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
- async resolveByName(path, value, code, label) {
166
- if (exports.UUID_RE.test(value))
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
- * Every row of an organization lookup (teams, roles, groups, members) — all its pages.
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;