@jentrix/cli 0.5.14 → 0.5.15

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.
@@ -2,6 +2,7 @@
2
2
  description: Align this session to a Jentrix work layer (workspace → project → task → owner) with a server-confirmed alignment snapshot
3
3
  allowed-tools: Bash(jentrix align:*), Bash(jentrix session:*)
4
4
  ---
5
+ <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
5
6
 
6
7
  Run the Jentrix MVP alignment wizard for THIS session. The question set is
7
8
  fixed — workspace, project, project name, work item, task title, owner,
@@ -28,12 +29,19 @@ and do NOT answer any question yourself.
28
29
  choice is the explicit free-text escape: when picked, let the operator
29
30
  type the id/slug. A question whose `status` is `"skipped"` was resolved
30
31
  by detection — tell the operator what was resolved (e.g. the workspace),
31
- don't re-ask it.
32
- 5. After the project answer, re-run
33
- `jentrix align --questions --json --project <chosen>` to load the
34
- work-item and owner choices, then continue asking. If the operator picked
35
- "Create a new project from this repo", the NEXT question is
36
- `project_name` (already in the payload, `kind: "freetext"`): relay its
32
+ don't re-ask it. When a question carries more choices than the native
33
+ picker can show (the work-item question can carry up to 15 tasks plus its
34
+ escapes), put the overflow choices label AND id — in the prompt text and
35
+ say they are reachable through "Other…". Never silently drop a choice.
36
+ 5. Re-run `--questions --json` after every answer that unlocks the next
37
+ question, carrying the answers so far as flags. After the WORKSPACE
38
+ answer: `jentrix align --questions --json --workspace <chosen>` — it
39
+ narrows the project list to that workspace, and without it the project
40
+ question stays `pending` and the flow stalls. After the PROJECT answer:
41
+ `jentrix align --questions --json [--workspace <chosen>] --project <chosen>`
42
+ loads the work-item and owner choices, then continue asking. If the
43
+ operator picked "Create a new project from this repo", the NEXT question
44
+ is `project_name` (already in the payload, `kind: "freetext"`): relay its
37
45
  `prompt` verbatim and collect the name as free text FROM THE OPERATOR —
38
46
  never propose name options, never fill it in yourself. Then re-run
39
47
  `jentrix align --questions --json --new-project "<name>"` — the work-item
@@ -2,6 +2,7 @@
2
2
  description: Write a distilled state-of-play for this session and push it as a typed Jentrix artifact
3
3
  allowed-tools: Bash(jentrix push:*), Bash(jentrix session:*), Bash(jentrix task:*)
4
4
  ---
5
+ <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
5
6
 
6
7
  Write down what this session currently knows, and push it.
7
8
 
@@ -2,6 +2,7 @@
2
2
  description: Connect this Claude Code session to a Jentrix project (session anchor + capture)
3
3
  allowed-tools: Bash(jentrix session:*)
4
4
  ---
5
+ <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
5
6
 
6
7
  Run this exact command with the Bash tool and relay its output to the user:
7
8
 
@@ -2,6 +2,7 @@
2
2
  description: End the connected Jentrix session (pushes the final artifacts, verifies capture, stores the RUN_SUMMARY)
3
3
  allowed-tools: Bash(jentrix session:*), Bash(jentrix push:*), Bash(jentrix artifact:*), Bash(jentrix column:*), Bash(jentrix task:*), Bash(jentrix tool:*)
4
4
  ---
5
+ <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
5
6
 
6
7
  End the connected Jentrix session honestly. For an ALIGNED session (the MVP
7
8
  pipeline), the typed artifacts ARE the record — push them before closing:
@@ -2,6 +2,7 @@
2
2
  description: Turn the opening prompt into a goal and a small set of Jentrix tasks, confirmed by the operator before anything is created
3
3
  allowed-tools: Bash(jentrix task:*), Bash(jentrix column:*), Bash(jentrix board:*), Bash(jentrix session:*), Bash(jentrix push:*)
4
4
  ---
5
+ <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
5
6
 
6
7
  Decompose the work the operator just asked for into a goal and a short task
7
8
  list, **confirm both before creating anything**, then create them.
@@ -2,6 +2,7 @@
2
2
  description: Review a Jentrix session's typed artifacts by category (input → plan → decision → output → issue → gap → learning → record), check the readiness contract, and post the verdict as a task comment
3
3
  allowed-tools: Bash(jentrix tool:*), Bash(jentrix artifact:*), Bash(jentrix session:*), Bash(jentrix comment:*)
4
4
  ---
5
+ <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
5
6
 
6
7
  Run the category walk over one session's record (docs: Reviewing a session).
7
8
  Read-only until the final comment; never modify the tree, the board, or the
@@ -2,6 +2,7 @@
2
2
  description: Show the connected Jentrix session for this work (capture health included)
3
3
  allowed-tools: Bash(jentrix session:*)
4
4
  ---
5
+ <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
5
6
 
6
7
  Run `jentrix session status` with the Bash tool (append the session id if the
7
8
  user gave one) and relay the output verbatim, including any capture-pending
@@ -2,6 +2,7 @@
2
2
  name: stacks-align
3
3
  description: Align the current Codex task to a Jentrix workspace, project, work item, and owner with an operator-confirmed server snapshot.
4
4
  ---
5
+ <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
5
6
 
6
7
  # Align this Codex task
7
8
 
@@ -12,8 +13,13 @@ description: Align the current Codex task to a Jentrix workspace, project, work
12
13
  4. Present each `status:"ask"` question and its choices verbatim using Codex's
13
14
  structured input UI when available, otherwise ask in chat. Never answer for the
14
15
  operator. Preserve notices and the `Other…` free-text escape.
15
- 5. Re-run `--questions --json` after project selection. Collect `project_name` and
16
- `task_title` as free text from the operator; never propose permanent names.
16
+ 5. Re-run `--questions --json` after every answer that unlocks the next question,
17
+ carrying the answers so far: `--workspace <chosen>` after the workspace (it
18
+ narrows the project list — without it the project question stays `pending`),
19
+ then `--project <chosen>`. Collect `project_name` and `task_title` as free text
20
+ from the operator; never propose permanent names. When the choices outnumber
21
+ the front end's picker, name the overflow (label + id) in the prompt rather
22
+ than dropping it.
17
23
  6. Present the confirmation prompt and every disclosure verbatim.
18
24
  7. On confirmation, submit exactly once with `jentrix align --provider codex --json
19
25
  --yes` plus the confirmed project, task/session-level, owner, agent label, and
@@ -2,6 +2,7 @@
2
2
  name: stacks-checkpoint
3
3
  description: Distill verified session state into a typed Jentrix checkpoint that survives compaction or handoff.
4
4
  ---
5
+ <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
5
6
 
6
7
  # Write a checkpoint
7
8
 
@@ -2,6 +2,7 @@
2
2
  name: stacks-connect
3
3
  description: Connect the current Codex task to a Jentrix project using trusted lifecycle identity and start the session capture host.
4
4
  ---
5
+ <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
5
6
 
6
7
  # Connect this Codex task
7
8
 
@@ -2,6 +2,7 @@
2
2
  name: stacks-end
3
3
  description: End a connected Jentrix session after preserving approved outputs, parking completed work in review, and reporting capture honestly.
4
4
  ---
5
+ <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
5
6
 
6
7
  # End the connected session
7
8
 
@@ -2,6 +2,7 @@
2
2
  name: stacks-plan
3
3
  description: Turn the opening request into an operator-approved goal and small Jentrix task set, then store the prompt and goal as typed artifacts.
4
4
  ---
5
+ <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
5
6
 
6
7
  # Plan confirmed work
7
8
 
@@ -2,6 +2,7 @@
2
2
  name: stacks-review
3
3
  description: Review a Jentrix session's typed artifacts by category, verify the readiness contract, and post the verdict as a comment on the aligned task.
4
4
  ---
5
+ <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
5
6
 
6
7
  # Review a session's record
7
8
 
@@ -2,6 +2,7 @@
2
2
  name: stacks-status
3
3
  description: Show the connected Jentrix session, alignment, capture health, and honest telemetry availability for this Codex task.
4
4
  ---
5
+ <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
5
6
 
6
7
  # Show session status
7
8
 
package/dist/main.js CHANGED
@@ -20882,7 +20882,10 @@ var ACCESS_TOKEN_PREFIX = "tmo_";
20882
20882
  var REFRESH_TOKEN_PREFIX = "tmr_";
20883
20883
  function parseTokenResponse(body) {
20884
20884
  if (typeof body !== "object" || body === null) {
20885
- throw new OAuthTokenError("invalid_response", "token response is not an object");
20885
+ throw new OAuthTokenError(
20886
+ "invalid_response",
20887
+ "token response is not an object"
20888
+ );
20886
20889
  }
20887
20890
  const json = body;
20888
20891
  if (typeof json.error === "string") {
@@ -20962,7 +20965,7 @@ function refreshAccessToken(input) {
20962
20965
 
20963
20966
  // src/client.ts
20964
20967
  var CLI_NAME = "stacks-cli";
20965
- var CLI_VERSION = "0.5.14";
20968
+ var CLI_VERSION = "0.5.15";
20966
20969
  var DEAD_TOKEN_MESSAGE = "authentication failed (HTTP 401): the token is dead or expired \u2014 mint a new PAT at /account/tokens on your Jentrix server and update STACKS_TOKEN (or --token / the config file).";
20967
20970
  var RELOGIN_MESSAGE = "OAuth session expired and could not be refreshed \u2014 run `jentrix login` to sign in again.";
20968
20971
  function originOf2(url2) {
@@ -23840,7 +23843,7 @@ function producerFromFlags(flags) {
23840
23843
  if (!label) return null;
23841
23844
  return { label, emoji: flags.agentEmoji?.trim() || null };
23842
23845
  }
23843
- async function buildCatalog(caller, deps2, flags, detection) {
23846
+ async function buildCatalog(caller, flags, detection) {
23844
23847
  const context = await callStructured(caller, "get_token_context", {});
23845
23848
  let pinnedWorkspace = null;
23846
23849
  if (context.workspacePinned === true && typeof context.workspaceId === "string") {
@@ -23851,10 +23854,13 @@ async function buildCatalog(caller, deps2, flags, detection) {
23851
23854
  name: match?.name ?? "pinned"
23852
23855
  };
23853
23856
  }
23854
- const { candidates, pinned: discoveryPinned } = await resolveCandidates(
23857
+ const flagWorkspaceId = flags.workspace?.trim() ? await resolveWorkspaceFlag(
23855
23858
  caller,
23856
- detection.repoOwnerName
23857
- );
23859
+ flags.workspace.trim(),
23860
+ pinnedWorkspace
23861
+ ) : null;
23862
+ const { candidates: matched, pinned: discoveryPinned } = await resolveCandidates(caller, detection.repoOwnerName);
23863
+ const candidates = flagWorkspaceId ? matched.filter((candidate) => candidate.workspace.id === flagWorkspaceId) : matched;
23858
23864
  let otherProjects = [];
23859
23865
  let discoveryNotice = null;
23860
23866
  if (discoveryPinned && pinnedWorkspace) {
@@ -23879,7 +23885,7 @@ async function buildCatalog(caller, deps2, flags, detection) {
23879
23885
  (project) => project.id === flags.project || project.slug === flags.project
23880
23886
  );
23881
23887
  chosenProjectId = match?.id ?? flags.project;
23882
- } else if (candidates.length === 1) {
23888
+ } else if (candidates.length === 1 && matched.length === 1) {
23883
23889
  chosenProjectId = candidates[0].id;
23884
23890
  }
23885
23891
  let openTasks = null;
@@ -23926,7 +23932,7 @@ async function buildCatalog(caller, deps2, flags, detection) {
23926
23932
  } catch {
23927
23933
  }
23928
23934
  }
23929
- const chosenWorkspaceId = candidates.find((candidate) => candidate.id === chosenProjectId)?.workspace.id ?? pinnedWorkspace?.id ?? null;
23935
+ const chosenWorkspaceId = candidates.find((candidate) => candidate.id === chosenProjectId)?.workspace.id ?? flagWorkspaceId ?? pinnedWorkspace?.id ?? null;
23930
23936
  return {
23931
23937
  detection,
23932
23938
  pinnedWorkspace,
@@ -24305,7 +24311,7 @@ async function runAlign(flags, deps2) {
24305
24311
  provider: detected.provider
24306
24312
  };
24307
24313
  return await withCaller(deps2, async (caller, target) => {
24308
- const catalog = await buildCatalog(caller, deps2, flags, detection);
24314
+ const catalog = await buildCatalog(caller, flags, detection);
24309
24315
  const marker = readAlignmentMarker(
24310
24316
  deps2.configPath,
24311
24317
  inspection.root,
@@ -24427,14 +24433,7 @@ async function runAlign(flags, deps2) {
24427
24433
  let pendingCreateProject = null;
24428
24434
  if (flags.newTask) workItem = `__new__:${flags.newTask}`;
24429
24435
  if (flags.newProject && flags.newProject.trim()) {
24430
- let workspaceId = catalog.chosenWorkspaceId ?? catalog.pinnedWorkspace?.id ?? null;
24431
- if (flags.workspace?.trim()) {
24432
- workspaceId = await resolveWorkspaceFlag(
24433
- caller,
24434
- flags.workspace.trim(),
24435
- catalog.pinnedWorkspace ?? null
24436
- );
24437
- }
24436
+ const workspaceId = catalog.chosenWorkspaceId ?? catalog.pinnedWorkspace?.id ?? null;
24438
24437
  if (!workspaceId) {
24439
24438
  throw new UsageError(
24440
24439
  "--new-project needs a resolved workspace (a workspace-pinned token, a repo-matched candidate, or --workspace <id-or-slug>) \u2014 none resolved here"
@@ -24766,7 +24765,7 @@ function registerAlignCommand(program3, deps2, onExit2) {
24766
24765
  "create a project in the resolved workspace (seeds a board + repo/board links), then align to it"
24767
24766
  ).option(
24768
24767
  "--workspace <id-or-slug>",
24769
- "workspace for --new-project when neither a pinned token nor a repo match resolves one (fresh checkout, non-interactive)"
24768
+ "the workspace question's flag twin: narrows the wizard's project list, and names where --new-project creates when neither a pinned token nor a repo match resolves one"
24770
24769
  ).option("--task <taskId>", "existing work item to align to").option(
24771
24770
  "--new-task <title>",
24772
24771
  "create a task (existing ops core) and align to it"
@@ -25104,7 +25103,9 @@ async function offerCardMint(kind, payload, flags, deps2, sessionId = null) {
25104
25103
  board,
25105
25104
  artifactId: payload.artifactId,
25106
25105
  anchorTaskId: payload.taskId,
25107
- noun: mintNoun(ARTIFACT_TYPE_BY_PUSH_KIND[kind] ?? "FINDINGS"),
25106
+ noun: mintNoun(
25107
+ ARTIFACT_TYPE_BY_PUSH_KIND[kind] ?? "FINDINGS"
25108
+ ),
25108
25109
  // STA-128: the operator's own claim title, when one was given.
25109
25110
  artifactTitle: flags.title?.trim() || null,
25110
25111
  blocks: Boolean(flags.blocks),
@@ -25130,7 +25131,12 @@ async function findMintBoard(deps2, taskId) {
25130
25131
  const boards = (await callStructured(caller, "list_boards", { workspaceId })).boards.filter((b) => b.kind === "BUGS" && !b.archivedAt);
25131
25132
  const candidates = [
25132
25133
  ...boards,
25133
- ...task.boardId ? [{ id: String(task.boardId), name: String(task.boardName ?? "board") }] : []
25134
+ ...task.boardId ? [
25135
+ {
25136
+ id: String(task.boardId),
25137
+ name: String(task.boardName ?? "board")
25138
+ }
25139
+ ] : []
25134
25140
  ];
25135
25141
  for (const board of candidates) {
25136
25142
  const columns = (await callStructured(caller, "list_columns", { boardId: board.id })).columns;
@@ -25260,7 +25266,10 @@ async function runMintIssue(flags, deps2) {
25260
25266
  function registerMintIssueCommand(artifact, deps2, onExit2) {
25261
25267
  artifact.command("mint-issue").description(
25262
25268
  "Mint a card from a findings/gap/issue artifact: opens a card on the workspace's BUGS board (else the task's own board), links the artifact to it, and relates it back to the source task."
25263
- ).requiredOption("--artifact <id>", "the findings/gap/issue artifact to link").requiredOption(
25269
+ ).requiredOption(
25270
+ "--artifact <id>",
25271
+ "the findings/gap/issue artifact to link"
25272
+ ).requiredOption(
25264
25273
  "--from-task <id>",
25265
25274
  "the task the artifact was pushed on (the card RELATES_TO it)"
25266
25275
  ).option(
@@ -25338,7 +25347,9 @@ function statusToExit(status) {
25338
25347
  }
25339
25348
  async function runArtifactAttach(filePath, flags, deps2) {
25340
25349
  if (!flags.workspace && !flags.task) {
25341
- deps2.writeErr("--workspace <id> is required (or --task <id> to resolve it)");
25350
+ deps2.writeErr(
25351
+ "--workspace <id> is required (or --task <id> to resolve it)"
25352
+ );
25342
25353
  return EXIT_CODES.INVALID_INPUT;
25343
25354
  }
25344
25355
  if (flags.kind && !PUSH_KINDS.includes(flags.kind)) {
@@ -25458,7 +25469,12 @@ async function runArtifactAttach(filePath, flags, deps2) {
25458
25469
  });
25459
25470
  const artifact = result.artifact ?? {};
25460
25471
  deps2.writeOut(
25461
- flags.json ? JSON.stringify({ artifactId: artifact.id ?? null, ...result }) : `Attached artifact ${String(artifact.id ?? "?")} \xB7 ${String(artifact.title ?? filename)} (${type}, ${body.byteLength} bytes)`
25472
+ flags.json ? (
25473
+ // The id at the top level: every other artifact-producing command
25474
+ // answers `artifactId`, and a caller should not have to know which
25475
+ // envelope this one came in.
25476
+ JSON.stringify({ artifactId: artifact.id ?? null, ...result })
25477
+ ) : `Attached artifact ${String(artifact.id ?? "?")} \xB7 ${String(artifact.title ?? filename)} (${type}, ${body.byteLength} bytes)`
25462
25478
  );
25463
25479
  return 0;
25464
25480
  } finally {
@@ -25483,7 +25499,10 @@ function registerArtifactCommand(program3, deps2, onExit2) {
25483
25499
  ).option("--project <id>", "link to a project").option("--task <id>", "link to a task").option("--run <id>", "link to a run").option("--work-order <id>", "link to a Work Order").option("--decision <id>", "link to a decision").option(
25484
25500
  "--kind <kind>",
25485
25501
  `push kind (${PUSH_KINDS.join("|")}) \u2014 files under the same ArtifactType, hence the same work layer, as \`jentrix push\``
25486
- ).option("--session <id>", "attribute the artifact to a connected session").option("--type <type>", "ArtifactType (default inferred from --kind, else the extension, else DOC)").option("--title <title>", "artifact title (default: the file name)").option("--json", "stable JSON output").action(
25502
+ ).option("--session <id>", "attribute the artifact to a connected session").option(
25503
+ "--type <type>",
25504
+ "ArtifactType (default inferred from --kind, else the extension, else DOC)"
25505
+ ).option("--title <title>", "artifact title (default: the file name)").option("--json", "stable JSON output").action(
25487
25506
  async (file, flags) => onExit2(await runArtifactAttach(file, flags, deps2))
25488
25507
  );
25489
25508
  return artifact;
@@ -26785,7 +26804,8 @@ function mountCommandTree(program3, plan, runtime) {
26785
26804
  var WHOAMI_SCOPE_NOTE = "scope/workspace restrictions are enforced server-side and not introspectable; a FORBIDDEN (exit 3) on a write means the token lacks write/admin scope.";
26786
26805
  function redactToken(token) {
26787
26806
  const hint = token.length > 8 ? `\u2026${token.slice(-4)}` : "\u2026";
26788
- if (token.startsWith("tmo_")) return { type: "oauth", display: `tmo_${hint}` };
26807
+ if (token.startsWith("tmo_"))
26808
+ return { type: "oauth", display: `tmo_${hint}` };
26789
26809
  if (token.startsWith("tm_")) return { type: "pat", display: `tm_${hint}` };
26790
26810
  return { type: "unknown", display: hint };
26791
26811
  }
@@ -26874,7 +26894,8 @@ async function runWhoamiCommand(flags, deps2) {
26874
26894
  const scrub = (text) => {
26875
26895
  let out = text;
26876
26896
  for (const secret of secrets) {
26877
- if (out.includes(secret)) out = out.split(secret).join("<redacted token>");
26897
+ if (out.includes(secret))
26898
+ out = out.split(secret).join("<redacted token>");
26878
26899
  }
26879
26900
  return out;
26880
26901
  };
@@ -26969,7 +26990,9 @@ async function runWhoamiCommand(flags, deps2) {
26969
26990
  note: WHOAMI_SCOPE_NOTE
26970
26991
  };
26971
26992
  deps2.writeOut(
26972
- redact(flags.json === true ? stableStringify(report) : renderHuman(report))
26993
+ redact(
26994
+ flags.json === true ? stableStringify(report) : renderHuman(report)
26995
+ )
26973
26996
  );
26974
26997
  return EXIT_CODES.OK;
26975
26998
  } finally {
@@ -28750,7 +28773,8 @@ function loadSurface(json) {
28750
28773
  const seen = /* @__PURE__ */ new Set();
28751
28774
  const validated = tools.map((tool, index) => {
28752
28775
  const where = (field) => isRecord9(tool) && typeof tool.name === "string" ? `tool "${tool.name}": ${field}` : `tools[${index}]: ${field}`;
28753
- if (!isRecord9(tool)) throw new SurfaceError(`tools[${index}] is not an object`);
28776
+ if (!isRecord9(tool))
28777
+ throw new SurfaceError(`tools[${index}] is not an object`);
28754
28778
  if (typeof tool.name !== "string" || tool.name.length === 0) {
28755
28779
  throw new SurfaceError(where("missing or empty `name`"));
28756
28780
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jentrix/cli",
3
- "version": "0.5.14",
3
+ "version": "0.5.15",
4
4
  "description": "Command-line client for the Jentrix MCP surface (jentrix tool <name>, generated noun-verb commands).",
5
5
  "keywords": [
6
6
  "jentrix",