@qodalis/cli-server-node 1.0.0-beta.5 → 1.0.0-beta.7

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.
Files changed (119) hide show
  1. package/README.md +397 -19
  2. package/dist/package.json +77 -0
  3. package/dist/src/auth/cli-server-auth-gate.d.ts +33 -0
  4. package/dist/src/auth/cli-server-auth-gate.d.ts.map +1 -0
  5. package/dist/src/auth/cli-server-auth-gate.js +121 -0
  6. package/dist/src/auth/cli-server-auth-gate.js.map +1 -0
  7. package/dist/src/auth/cli-server-auth.d.ts +149 -0
  8. package/dist/src/auth/cli-server-auth.d.ts.map +1 -0
  9. package/dist/src/auth/cli-server-auth.js +143 -0
  10. package/dist/src/auth/cli-server-auth.js.map +1 -0
  11. package/dist/src/auth/index.d.ts +4 -0
  12. package/dist/src/auth/index.d.ts.map +1 -0
  13. package/dist/src/auth/index.js +19 -0
  14. package/dist/src/auth/index.js.map +1 -0
  15. package/dist/src/auth/ws-upgrade-auth.d.ts +30 -0
  16. package/dist/src/auth/ws-upgrade-auth.d.ts.map +1 -0
  17. package/dist/src/auth/ws-upgrade-auth.js +49 -0
  18. package/dist/src/auth/ws-upgrade-auth.js.map +1 -0
  19. package/dist/src/controllers/cli-controller.d.ts +2 -1
  20. package/dist/src/controllers/cli-controller.d.ts.map +1 -1
  21. package/dist/src/controllers/cli-controller.js +7 -2
  22. package/dist/src/controllers/cli-controller.js.map +1 -1
  23. package/dist/src/controllers/cli-version-controller.d.ts +12 -1
  24. package/dist/src/controllers/cli-version-controller.d.ts.map +1 -1
  25. package/dist/src/controllers/cli-version-controller.js +7 -2
  26. package/dist/src/controllers/cli-version-controller.js.map +1 -1
  27. package/dist/src/create-cli-server.d.ts +71 -1
  28. package/dist/src/create-cli-server.d.ts.map +1 -1
  29. package/dist/src/create-cli-server.js +146 -10
  30. package/dist/src/create-cli-server.js.map +1 -1
  31. package/dist/src/extensions/cli-builder.d.ts +10 -0
  32. package/dist/src/extensions/cli-builder.d.ts.map +1 -1
  33. package/dist/src/extensions/cli-builder.js +14 -0
  34. package/dist/src/extensions/cli-builder.js.map +1 -1
  35. package/dist/src/index.d.ts +4 -2
  36. package/dist/src/index.d.ts.map +1 -1
  37. package/dist/src/index.js +36 -1
  38. package/dist/src/index.js.map +1 -1
  39. package/dist/src/mcp/index.d.ts +10 -0
  40. package/dist/src/mcp/index.d.ts.map +1 -0
  41. package/dist/src/mcp/index.js +34 -0
  42. package/dist/src/mcp/index.js.map +1 -0
  43. package/dist/src/mcp/mcp-controller.d.ts +54 -0
  44. package/dist/src/mcp/mcp-controller.d.ts.map +1 -0
  45. package/dist/src/mcp/mcp-controller.js +358 -0
  46. package/dist/src/mcp/mcp-controller.js.map +1 -0
  47. package/dist/src/mcp/mcp-render.d.ts +10 -0
  48. package/dist/src/mcp/mcp-render.d.ts.map +1 -0
  49. package/dist/src/mcp/mcp-render.js +66 -0
  50. package/dist/src/mcp/mcp-render.js.map +1 -0
  51. package/dist/src/mcp/mcp-schema.d.ts +8 -0
  52. package/dist/src/mcp/mcp-schema.d.ts.map +1 -0
  53. package/dist/src/mcp/mcp-schema.js +64 -0
  54. package/dist/src/mcp/mcp-schema.js.map +1 -0
  55. package/dist/src/mcp/mcp-types.d.ts +70 -0
  56. package/dist/src/mcp/mcp-types.d.ts.map +1 -0
  57. package/dist/src/mcp/mcp-types.js +21 -0
  58. package/dist/src/mcp/mcp-types.js.map +1 -0
  59. package/dist/src/mcp/providers/commands-provider.d.ts +26 -0
  60. package/dist/src/mcp/providers/commands-provider.d.ts.map +1 -0
  61. package/dist/src/mcp/providers/commands-provider.js +142 -0
  62. package/dist/src/mcp/providers/commands-provider.js.map +1 -0
  63. package/dist/src/mcp/providers/data-explorer-provider.d.ts +15 -0
  64. package/dist/src/mcp/providers/data-explorer-provider.d.ts.map +1 -0
  65. package/dist/src/mcp/providers/data-explorer-provider.js +83 -0
  66. package/dist/src/mcp/providers/data-explorer-provider.js.map +1 -0
  67. package/dist/src/mcp/providers/filesystem-provider.d.ts +15 -0
  68. package/dist/src/mcp/providers/filesystem-provider.d.ts.map +1 -0
  69. package/dist/src/mcp/providers/filesystem-provider.js +166 -0
  70. package/dist/src/mcp/providers/filesystem-provider.js.map +1 -0
  71. package/dist/src/mcp/providers/jobs-provider.d.ts +23 -0
  72. package/dist/src/mcp/providers/jobs-provider.d.ts.map +1 -0
  73. package/dist/src/mcp/providers/jobs-provider.js +121 -0
  74. package/dist/src/mcp/providers/jobs-provider.js.map +1 -0
  75. package/dist/src/mcp/providers/server-provider.d.ts +19 -0
  76. package/dist/src/mcp/providers/server-provider.d.ts.map +1 -0
  77. package/dist/src/mcp/providers/server-provider.js +73 -0
  78. package/dist/src/mcp/providers/server-provider.js.map +1 -0
  79. package/dist/src/processors/cli-base64-command-processor.d.ts.map +1 -1
  80. package/dist/src/processors/cli-base64-command-processor.js +2 -0
  81. package/dist/src/processors/cli-base64-command-processor.js.map +1 -1
  82. package/dist/src/processors/cli-hash-command-processor.d.ts +1 -0
  83. package/dist/src/processors/cli-hash-command-processor.d.ts.map +1 -1
  84. package/dist/src/processors/cli-hash-command-processor.js +1 -0
  85. package/dist/src/processors/cli-hash-command-processor.js.map +1 -1
  86. package/dist/src/processors/cli-http-command-processor.d.ts.map +1 -1
  87. package/dist/src/processors/cli-http-command-processor.js +2 -0
  88. package/dist/src/processors/cli-http-command-processor.js.map +1 -1
  89. package/dist/src/server.js +12 -0
  90. package/dist/src/server.js.map +1 -1
  91. package/dist/src/services/cli-event-socket-manager.d.ts +37 -1
  92. package/dist/src/services/cli-event-socket-manager.d.ts.map +1 -1
  93. package/dist/src/services/cli-event-socket-manager.js +126 -28
  94. package/dist/src/services/cli-event-socket-manager.js.map +1 -1
  95. package/dist/src/services/cli-log-socket-manager.d.ts +15 -1
  96. package/dist/src/services/cli-log-socket-manager.d.ts.map +1 -1
  97. package/dist/src/services/cli-log-socket-manager.js +53 -13
  98. package/dist/src/services/cli-log-socket-manager.js.map +1 -1
  99. package/dist/src/services/cli-shell-options.d.ts +107 -0
  100. package/dist/src/services/cli-shell-options.d.ts.map +1 -0
  101. package/dist/src/services/cli-shell-options.js +139 -0
  102. package/dist/src/services/cli-shell-options.js.map +1 -0
  103. package/dist/src/services/cli-shell-session-manager.d.ts +83 -10
  104. package/dist/src/services/cli-shell-session-manager.d.ts.map +1 -1
  105. package/dist/src/services/cli-shell-session-manager.js +474 -85
  106. package/dist/src/services/cli-shell-session-manager.js.map +1 -1
  107. package/dist/src/services/index.d.ts +2 -1
  108. package/dist/src/services/index.d.ts.map +1 -1
  109. package/dist/src/services/index.js +5 -1
  110. package/dist/src/services/index.js.map +1 -1
  111. package/dist/src/services/process-tree.d.ts +38 -0
  112. package/dist/src/services/process-tree.d.ts.map +1 -0
  113. package/dist/src/services/process-tree.js +249 -0
  114. package/dist/src/services/process-tree.js.map +1 -0
  115. package/dist/src/services/session-bound-sockets.d.ts +45 -0
  116. package/dist/src/services/session-bound-sockets.d.ts.map +1 -0
  117. package/dist/src/services/session-bound-sockets.js +129 -0
  118. package/dist/src/services/session-bound-sockets.js.map +1 -0
  119. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Qodalis CLI Server (Node.js)
2
2
 
3
+ [![npm @qodalis/cli-server-node](https://img.shields.io/npm/v/@qodalis/cli-server-node?label=%40qodalis%2Fcli-server-node)](https://www.npmjs.com/package/@qodalis/cli-server-node)
4
+ [![npm @qodalis/cli-server-abstractions](https://img.shields.io/npm/v/@qodalis/cli-server-abstractions?label=%40qodalis%2Fcli-server-abstractions)](https://www.npmjs.com/package/@qodalis/cli-server-abstractions)
5
+
3
6
  A Node.js CLI server framework for the [Qodalis CLI](https://github.com/qodalis-solutions/web-cli) ecosystem. Build custom server-side commands that integrate with the Qodalis web terminal.
4
7
 
5
8
  ## Installation
@@ -303,11 +306,319 @@ The server exposes versioned endpoints:
303
306
 
304
307
  | Method | Path | Description |
305
308
  |--------|------|-------------|
306
- | GET | `/api/cli/versions` | Version discovery (supported versions, preferred version) |
307
- | GET | `/api/v1/cli/version` | V1 server version |
308
- | GET | `/api/v1/cli/commands` | V1 commands (all processors) |
309
- | POST | `/api/v1/cli/execute` | V1 execute |
310
- | WS | `/ws/cli/events` | WebSocket events (also `/ws/v1/cli/events`) |
309
+ | GET | `/api/qcli/versions` | Version discovery (supported versions, preferred version) |
310
+ | GET | `/api/v1/qcli/version` | V1 server version |
311
+ | GET | `/api/v1/qcli/commands` | V1 commands (all processors) |
312
+ | POST | `/api/v1/qcli/execute` | V1 execute |
313
+ | POST | `/api/v1/qcli/execute/stream` | V1 SSE streaming execute |
314
+ | POST | `/api/qcli/mcp` | MCP endpoint (when the `mcp` option is on, see [MCP Endpoint](#mcp-endpoint)) |
315
+ | WS | `/ws/qcli/events` | WebSocket events (also `/ws/v1/qcli/events`) |
316
+ | WS | `/ws/v1/qcli/shell` | Interactive PTY shell session |
317
+
318
+ `GET /capabilities` reports `shell: true|false` and, when the shell is enabled, `shellAuth: 'users' | 'token' | 'custom' | 'anonymous' | 'locked'`.
319
+
320
+ ## MCP Endpoint
321
+
322
+ The server can expose **the whole system** to AI clients such as Claude Code over the [Model Context Protocol](https://modelcontextprotocol.io): CLI commands, the filesystem API, the data explorer, jobs and tools contributed by plugins, each with the same permissions as a CLI user. It works with or without the users plugin.
323
+
324
+ ### Enabling
325
+
326
+ MCP is off by default. Enable it with the `mcp` option:
327
+
328
+ ```typescript
329
+ const { app, mcp } = createCliServer({
330
+ mcp: true, // or an options object:
331
+ // mcp: {
332
+ // tools: ['weather_*', 'echo'], // allow-list (trailing * = prefix); default: every tool
333
+ // exclude: ['fs_delete'], // deny-list, applied after `tools`
334
+ // token: process.env.MCP_TOKEN, // only WITHOUT an auth provider: required bearer token
335
+ // instructions: 'Each tool runs one command of this server.', // returned by initialize
336
+ // enabled: true, // default true when the object is given
337
+ // },
338
+ configure: (builder) => builder.addProcessor(new CliEchoCommandProcessor()),
339
+ });
340
+ ```
341
+
342
+ ### Route and transport
343
+
344
+ - `POST /api/qcli/mcp` (`{basePath}/mcp`): MCP *Streamable HTTP*, **stateless** (no `Mcp-Session-Id`) and **JSON responses only** (no SSE). `GET`/`DELETE` → `405` with `Allow: POST`.
345
+ - JSON-RPC 2.0: notifications → `202` with an empty body; malformed JSON → `-32700`, invalid request (including batches) → `-32600`, unknown method → `-32601`, bad params → `-32602` (JSON-RPC errors use HTTP 200).
346
+ - Methods: `initialize` (protocol versions `2025-11-25`, `2025-06-18`, `2025-03-26`; the client's version is echoed when supported, otherwise the newest), `ping`, `tools/list`, `tools/call`.
347
+ - An `Origin` header, when present, must be in `allowedOrigins` or same-origin, otherwise `403` (DNS-rebinding protection; checked before authentication).
348
+
349
+ ### Authentication
350
+
351
+ | Server | Access |
352
+ |---|---|
353
+ | users plugin mounted (`protectServer`) | a session or an [API token](plugins/users/README.md#api-tokens) (`Authorization: Bearer qcli_pat_…`) holding `commands:execute`; `401`/`403` as plain `{ "error": … }` responses. `tools/list` and `tools/call` only show the tools whose permissions the caller holds. `mcp.token` is ignored. |
354
+ | no auth provider, `mcp.token` set | `Authorization: Bearer <token>` (constant-time compare), else `401` |
355
+ | no auth provider, no token | open, like `/execute` |
356
+
357
+ ### Tools
358
+
359
+ Tools are collected from **tool providers** on every request, so new registrations, enabled features and the caller's permissions are always current. The `tools`/`exclude` lists apply to every tool.
360
+
361
+ | Provider | Tools | Permissions | Annotations |
362
+ |---|---|---|---|
363
+ | server | `server_info` (version, capabilities, enabled features, command names) | — | read-only |
364
+ | commands | one tool per command (below) | `commands:execute` + `requiredPermissions` along the chain | — |
365
+ | filesystem (when a file storage provider is configured) | `fs_list {path}`, `fs_stat {path}`, `fs_read {path, maxBytes?=262144}` | `files:read` | read-only |
366
+ | | `fs_write {path, content}`, `fs_mkdir {path}` | `files:write` | — |
367
+ | | `fs_delete {path, recursive?=false}` | `files:write` | destructive |
368
+ | data explorer (when sources are registered) | `data_sources`, `data_schema {source}` | `data:query` | read-only |
369
+ | | `data_query {source, query, parameters?}` | `data:query` | — |
370
+ | jobs (when the jobs plugin is mounted) | `jobs_list`, `jobs_get {id}`, `jobs_history {id, limit?=20}` | `jobs:read` | read-only |
371
+ | | `jobs_trigger`, `jobs_pause`, `jobs_resume`, `jobs_stop`, `jobs_cancel` `{id}` | `jobs:manage` | `jobs_stop`/`jobs_cancel` destructive |
372
+
373
+ The jobs tools' `id` accepts a job id or a unique job name (exact, then case-insensitive; an ambiguous name is an error).
374
+
375
+ Feature tools use the same services as the HTTP routes (the storage provider and its path checks, the data explorer executor with its timeouts and row limits, the job scheduler); their errors become `isError: true` results. `fs_read` returns UTF-8 text (binary files → `isError` `binary file, N bytes`). Paths outside the provider's allow-list are rejected exactly as by `/api/qcli/fs` (symlinks are resolved first, so on macOS allow `/private/tmp` rather than `/tmp`).
376
+
377
+ **Command tools**: every root command and sub-command that has no sub-commands, declares parameters or takes a value (`valueRequired`, or `acceptsValue` for an optional one) becomes a tool named after its path joined with `_` (`weather`, `weather_current`), lower-cased, other characters than `[a-z0-9_-]` replaced by `-`, at most 64 characters. The input schema has one property per parameter (`string`, `number`, `boolean`, `array` of strings; anything else is a string) plus `value` (the positional input) when the command takes one: required with `valueRequired`, optional with `acceptsValue` (set it on processors that read `command.value` without requiring it; the built-in `hash`, `base64 encode|decode`, `http get|post` and the weather commands do). A call builds the same command the CLI would send and runs it through the **same executor as `/execute`**; the outputs are rendered as text (tables as `a | b` rows, lists one item per line, JSON pretty-printed, key-value as `key: value`), `isError` is `exitCode != 0`, and a single JSON-object output is also returned as `structuredContent`. Unknown tools and invalid arguments (wrong type, unknown or missing property) are JSON-RPC `-32602` errors.
378
+
379
+ Name collisions (a warning is logged): built-in feature tools win, then plugin/host provider tools in registration order, then command tools. The interactive shell is never a tool.
380
+
381
+ ### Adding tools (providers)
382
+
383
+ ```typescript
384
+ import { McpToolProvider, mcpObjectSchema } from '@qodalis/cli-server-node';
385
+
386
+ const provider: McpToolProvider = {
387
+ name: 'inventory',
388
+ getTools: () => [
389
+ {
390
+ name: 'inventory_count',
391
+ description: 'Count the items of a warehouse',
392
+ inputSchema: mcpObjectSchema({ warehouse: { type: 'string' } }, ['warehouse']),
393
+ requiredPermissions: ['inventory:read'], // checked for list and call (with an auth provider)
394
+ annotations: { readOnlyHint: true },
395
+ call: async (args, { auth, signal }) => ({ text: `42 items in ${args.warehouse}`, isError: false }),
396
+ },
397
+ ],
398
+ };
399
+
400
+ const server = createCliServer({ mcp: true });
401
+ server.mcp.addProvider(provider); // or builder.addMcpToolProvider(provider) in `configure`
402
+ // plugins can also return `mcpProviders: [provider]` from their build result (picked up by mountPlugin)
403
+ ```
404
+
405
+ Arguments are validated against `inputSchema` before `call` runs. Return `structured` to also send `structuredContent`.
406
+
407
+ ### Using the MCP endpoint from an MCP client
408
+
409
+ This guide connects [Claude Code](https://docs.claude.com/en/docs/claude-code) to a server. The commands use the demo server (`cd demo && npm start`: port 8047, users plugin, user `root` / password `admin`); replace the URL and credentials with yours.
410
+
411
+ #### 1. Enable MCP on the server
412
+
413
+ MCP is enabled with the `mcp` option of `createCliServer` (`mcp: true`, or an options object).
414
+
415
+ With the users plugin (sessions, permissions, API tokens):
416
+
417
+ ```typescript
418
+ import { createCliServer, CliEchoCommandProcessor } from '@qodalis/cli-server-node';
419
+ import { CliUsersBuilder } from '@qodalis/cli-server-plugin-users';
420
+
421
+ const { app, eventSocketManager, mountPlugin } = createCliServer({
422
+ mcp: { instructions: 'Each tool runs one command of this server.' },
423
+ configure: (builder) => builder.addProcessor(new CliEchoCommandProcessor()),
424
+ });
425
+
426
+ const usersPlugin = new CliUsersBuilder()
427
+ .setDefaultAdminPassword('admin')
428
+ .build((msg) => eventSocketManager.broadcastMessage(msg));
429
+ mountPlugin(usersPlugin);
430
+
431
+ app.listen(8047);
432
+ ```
433
+
434
+ Without the users plugin, protect the endpoint with a static bearer token (without `token` it is as open as `/execute`):
435
+
436
+ ```typescript
437
+ const { app } = createCliServer({
438
+ mcp: { token: process.env.QCLI_MCP_TOKEN },
439
+ configure: (builder) => builder.addProcessor(new CliEchoCommandProcessor()),
440
+ });
441
+ app.listen(8047);
442
+ ```
443
+
444
+ The endpoint is `http://localhost:8047/api/qcli/mcp` (`{basePath}/mcp`).
445
+
446
+ #### 2. Get a token
447
+
448
+ With the users plugin, sign in and create an API token with the session token (requires `jq`; otherwise copy the `token` field of each response):
449
+
450
+ ```bash
451
+ SESSION=$(curl -s -X POST http://localhost:8047/api/v1/qcli/sessions \
452
+ -H "Content-Type: application/json" \
453
+ -d '{"username":"root","password":"admin"}' | jq -r .token)
454
+
455
+ TOKEN=$(curl -s -X POST http://localhost:8047/api/v1/qcli/tokens \
456
+ -H "Authorization: Bearer $SESSION" \
457
+ -H "Content-Type: application/json" \
458
+ -d '{"name":"claude","expiresInDays":30}' | jq -r .token)
459
+
460
+ echo "$TOKEN"
461
+ ```
462
+
463
+ The API token (`qcli_pat_…`) is shown only once. In the web CLI connected to the server, `token create claude` does the same. Add `"permissions": ["commands:execute", "files:read"]` to the body to give the token only part of your permissions; tokens are listed with `GET /api/v1/qcli/tokens` and revoked with `DELETE /api/v1/qcli/tokens/<id>` (see [API tokens](plugins/users/README.md#api-tokens)).
464
+
465
+ Without the users plugin, the token is the `mcp.token` value.
466
+
467
+ #### 3. Connect Claude Code
468
+
469
+ **(a) Directly over HTTP** with the token:
470
+
471
+ ```bash
472
+ claude mcp add --transport http qcli http://localhost:8047/api/qcli/mcp --header "Authorization: Bearer <token>"
473
+ ```
474
+
475
+ **(b) Through the [`@qodalis/mcp-cli`](https://github.com/qodalis-solutions/mcp-cli) bridge**, a local MCP server configured with environment variables:
476
+
477
+ ```bash
478
+ claude mcp add qcli -e QCLI_URL=http://localhost:8047 -e QCLI_TOKEN=<token> -- npx -y @qodalis/mcp-cli
479
+ ```
480
+
481
+ Use `QCLI_USER` / `QCLI_PASSWORD` instead of `QCLI_TOKEN` to sign in with a user, and `QCLI_READ_ONLY=true` to expose only read-only tools.
482
+
483
+ Check the connection with `claude mcp list` (or `/mcp` inside Claude Code); `qcli` should be listed as connected, with the tools of step 4.
484
+
485
+ #### 4. Check the endpoint with curl
486
+
487
+ Every request is one JSON-RPC message sent with `POST`; the server is stateless, so no session header is needed between calls.
488
+
489
+ ```bash
490
+ # initialize
491
+ curl -s http://localhost:8047/api/qcli/mcp \
492
+ -H "Authorization: Bearer $TOKEN" \
493
+ -H "Content-Type: application/json" \
494
+ -H "Accept: application/json, text/event-stream" \
495
+ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
496
+
497
+ # list the tools
498
+ curl -s http://localhost:8047/api/qcli/mcp \
499
+ -H "Authorization: Bearer $TOKEN" \
500
+ -H "Content-Type: application/json" \
501
+ -H "Accept: application/json, text/event-stream" \
502
+ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
503
+
504
+ # call a tool
505
+ curl -s http://localhost:8047/api/qcli/mcp \
506
+ -H "Authorization: Bearer $TOKEN" \
507
+ -H "Content-Type: application/json" \
508
+ -H "Accept: application/json, text/event-stream" \
509
+ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"value":"hello from MCP"}}}'
510
+ ```
511
+
512
+ Responses (shortened):
513
+
514
+ ```json
515
+ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{"listChanged":false}},"serverInfo":{"name":"qodalis-cli-server","version":"1.0.0-beta.7"},"instructions":"…"}}
516
+ {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"server_info","description":"…","inputSchema":{"type":"object","properties":{},"required":[],"additionalProperties":false},"annotations":{"readOnlyHint":true}},{"name":"echo", …}, …]}}
517
+ {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"hello from MCP"}],"isError":false}}
518
+ ```
519
+
520
+ A failed command is a normal result with `"isError": true`; an unknown tool or invalid arguments is a JSON-RPC error (`-32602`).
521
+
522
+ #### 5. Troubleshooting
523
+
524
+ | Symptom | Cause and fix |
525
+ |---|---|
526
+ | `claude mcp list` / `/mcp` shows `qcli` as failed | Run the curl checks of step 4 with the same URL and token to see the HTTP error below. |
527
+ | `401 {"error":"unauthorized"}` or `{"error":"invalid or expired token"}` | No `Authorization: Bearer …` header, or the token is wrong, expired or revoked, or its user is disabled or deleted: create a new token (step 2). Without the users plugin, check `mcp.token`. |
528
+ | `403 {"error":"forbidden: origin not allowed"}` | The request has an `Origin` header that is neither in `allowedOrigins` nor the server's own origin (DNS-rebinding protection). Claude Code sends no `Origin`; for browser-based clients add the origin to `allowedOrigins`. |
529
+ | `403 {"error":"forbidden: missing permission commands:execute"}` | The user, or the token's scope, lacks `commands:execute`, which the endpoint requires. Add it to one of the user's groups, or create a token whose scope includes it. |
530
+ | `403 {"error":"password change required"}` | The user must change their password first. |
531
+ | `404` | MCP is not enabled (`mcp` option) or the URL is not `{basePath}/mcp`. |
532
+ | `405` with `Allow: POST` | The endpoint only accepts `POST` (MCP clients that probe with `GET` handle this). |
533
+ | A tool is missing from `tools/list`, or `-32602 Unknown tool` | The user lacks one of the tool's permissions (`commands:execute` plus the command's `requiredPermissions`, `files:read`/`files:write`, `data:query`, `jobs:read`/`jobs:manage`); the token's scope narrows them further; `mcp.tools` / `mcp.exclude` filter it out; or the feature is not enabled on the server (file storage, data explorer sources, jobs plugin). A command whose parameters all live on its sub-commands appears only as `<command>_<sub>` tools. |
534
+
535
+ ## Remote Shell Security
536
+
537
+ The PTY shell (`/ws/v1/qcli/shell`, `/ws/qcli/shell`) gives OS-level access to the server. It is **off** unless `enableShell: true`. When enabled, exactly one access mode applies:
538
+
539
+ | Mode (`shellAuth`) | When | Client must |
540
+ |---|---|---|
541
+ | `users` | the users plugin is mounted | connect with a WS ticket (`?ticket=`) of a session holding `shell:execute` |
542
+ | `token` | no users plugin, `shell.token` set | send `Authorization: Bearer <token>` (header, or the `_header_Authorization` query parameter) |
543
+ | `custom` | no users plugin, `shell.authorize` set | pass the host's `authorize(request, { url, remoteAddress, mode })` check |
544
+ | `anonymous` | no users plugin, `shell.allowAnonymous: true` | nothing (a startup WARNING is logged) |
545
+ | `locked` | none of the above | — every connection is closed with 4401 and a startup ERROR explains the options |
546
+
547
+ Precedence: users plugin mounted (even with `protectServer: false`) ⇒ `users`; otherwise `token`, then `custom`, then `anonymous`. Whatever the mode, **every configured check among `token` and `authorize` must pass** (so with both set, both are required and `shellAuth` reports `token`); `allowAnonymous` is ignored once either is set. Tokens are compared in constant time (SHA-256 of both, then `timingSafeEqual`) and never logged; an `authorize` that throws or rejects denies.
548
+
549
+ ### With the users plugin
550
+
551
+ ```typescript
552
+ const { mountPlugin, eventSocketManager } = createCliServer({ enableShell: true });
553
+ mountPlugin(new CliUsersBuilder().build((m) => eventSocketManager.broadcastMessage(m)));
554
+ // Grant shell:execute to a group (root/admin has '*'):
555
+ // PUT /api/v1/qcli/groups/ops { "permissions": ["commands:execute", "shell:execute"] }
556
+ ```
557
+
558
+ An open shell is bound to the session that opened it: logout, idle/absolute session expiry, admin revoke (kick), user disabled or deleted, or losing `shell:execute` (checked once more right after the shell is registered and before it is spawned, so a logout racing the open is not missed; then immediately on session/user/group changes and every `sessionCheckIntervalMs`, default 30 s, max 60 s), or a password change becoming required closes it with **4401** `session ended` and kills its process tree. Users authenticated by an external auth adapter have no stored session: their shells are re-checked by calling the adapter again with the shell's original upgrade request (see the users plugin README). Re-checks never extend the session by themselves; stdin received since the previous check counts as session activity (at most one touch per 30 s), so an active shell user is not idle-logged-out. Opens and closes are audited as `shell_opened` / `shell_closed` (`userId`, `actorId`, `metadata: { mode: 'interactive' | 'command', sessionId, durationMs, reason, exitCode? }`), never with the command text or stdin. `reason` is one of `exited` (then `exitCode` is set), `closed`, `session ended`, `permission removed`, `shell timed out`, `server shutdown`, `error`.
559
+
560
+ ### Without the users plugin (token mode)
561
+
562
+ ```typescript
563
+ createCliServer({
564
+ enableShell: true,
565
+ shell: { token: process.env.QCLI_SHELL_TOKEN }, // long random secret
566
+ });
567
+ ```
568
+
569
+ Browser clients cannot set WebSocket headers: configure the server in web-cli with `headers: { Authorization: 'Bearer <token>' }` and the client forwards it as `?_header_Authorization=Bearer%20<token>`. The standalone server (`src/server.ts`) enables token mode when `QCLI_SHELL_TOKEN` is set. Use HTTPS/WSS so the token is not sent in clear text.
570
+
571
+ For your own scheme (reverse-proxy identity, IP allow-list, …) use `authorize`:
572
+
573
+ ```typescript
574
+ createCliServer({
575
+ enableShell: true,
576
+ shell: { authorize: async (req, { remoteAddress }) => remoteAddress === '127.0.0.1' && req.headers['x-ops'] === 'yes' },
577
+ });
578
+ ```
579
+
580
+ Non-users modes log one info line per open/close (remote address, mode, reason) without the command text.
581
+
582
+ ### Options
583
+
584
+ ```typescript
585
+ shell: {
586
+ token?: string; // token mode (or extra requirement with the users plugin)
587
+ authorize?: (req, ctx) => boolean | Promise<boolean>; // custom mode (or extra requirement)
588
+ allowAnonymous?: boolean; // open to anyone who can reach the server (not recommended)
589
+ maxSessionsPerUser?: number; // per user (users mode) or per remote address (default 3; X-Forwarded-For only with trustProxy)
590
+ maxSessions?: number; // total concurrent shells (default 20)
591
+ idleTimeoutMs?: number; // close after no stdin (resize does not count; default 15 min, 0 = off)
592
+ maxDurationMs?: number; // absolute limit (default none)
593
+ sessionCheckIntervalMs?: number; // users-mode re-check (default 30 000, max 60 000)
594
+ cwd?: string; // working directory (default: home directory)
595
+ shell?: string; // shell binary (default: detected)
596
+ env?: { allow?: string[]; set?: Record<string, string> };
597
+ }
598
+ ```
599
+
600
+ The shell does **not** inherit the server environment: it gets only `PATH`, `HOME`, `USER`, `LANG`, `TERM`, `COLUMNS`, `LINES`, `SHELL` (plus the variables Windows needs to start PowerShell), the names in `env.allow`, and `env.set`. Secrets such as database passwords stay out of the shell.
601
+
602
+ ### Process cleanup
603
+
604
+ Whenever a shell ends (exit, client close, session ended or permission removed, timeout, server shutdown), the server first collects everything the shell started — its process group, its descendants by parent PID (including background jobs that interactive shells move to their own process groups, and `setsid`/`nohup` children), on Linux every process of its session, and every process tagged with the shell's marker (below) — and then sends SIGHUP, SIGTERM and, after a 1 s grace period, SIGKILL to all of them (re-collecting before the SIGKILL). The server's own process and process group are never targeted. Linux reads `/proc` for everything (no `ps` needed); macOS and other Unixes run `ps`. On Windows only the shell process is killed.
605
+
606
+ Every shell gets a unique marker in its environment, `QCLI_SHELL_ID=<random 128-bit hex>`, added after the `env.allow`/`env.set` filtering so configuration can neither override nor remove it. Children inherit it, so processes that were re-parented to init *and* left the shell's process group and session — e.g. `setsid sleep 100 &` from an interactive shell, double-forking daemons — are still found: on Linux by reading `/proc/<pid>/environ`, on macOS through `ps -A -E -ww` (which shows the environment of the server user's processes).
607
+
608
+ Limitations: a process escapes only if it deliberately unsets or changes `QCLI_SHELL_ID` before detaching (`env -u QCLI_SHELL_ID setsid …`), or runs as a different user whose environment the server cannot read (e.g. via `sudo`). On macOS the environment of Apple platform binaries (`/bin/sleep`, `/usr/bin/perl`, …) is hidden from `ps -E`, so such a binary that left the shell's tree and session is not found there either. Run the server in a container or under a service manager (cgroup) if every process must be contained.
609
+
610
+ ### Close codes
611
+
612
+ Refusals are sent as *accept, then close* so browsers can read them (a foreign `Origin` is still rejected with HTTP 403):
613
+
614
+ | Code | Reason | When |
615
+ |---|---|---|
616
+ | 4401 | `login required` / `session ended` / `shell access is not configured` | missing ticket or (Bearer) token; session ended (logout, expiry, kick, user disabled/deleted, `shell:execute` removed, password change now required) — shell, events and logs sockets; shell locked |
617
+ | 4403 | `forbidden` | wrong token, `authorize` false or threw, no `shell:execute`, password change required |
618
+ | 4404 | `shell disabled` | `enableShell` is false |
619
+ | 4408 | `shell timed out` | idle timeout or `maxDurationMs` |
620
+ | 4429 | `too many shell sessions` | `maxSessionsPerUser` or `maxSessions` reached |
621
+ | 1001 | `server shutdown` | the server is shutting down |
311
622
 
312
623
  ## Processor Base Class Reference
313
624
 
@@ -322,6 +633,7 @@ The server exposes versioned endpoints:
322
633
  | `processors` | `ICliCommandProcessor[]` | `undefined` | Sub-commands |
323
634
  | `allowUnlistedCommands` | `boolean` | `undefined` | Accept sub-commands not in `processors` |
324
635
  | `valueRequired` | `boolean` | `undefined` | Require a positional value |
636
+ | `acceptsValue` | `boolean` | `undefined` | Reads an optional positional value (`command.value`); MCP exposes it as an optional `value` argument |
325
637
  | `version` | `string` | `'1.0.0'` | Processor version string |
326
638
  | `author` | `ICliCommandAuthor` | default author | Author metadata (name, email) |
327
639
 
@@ -329,12 +641,26 @@ The server exposes versioned endpoints:
329
641
 
330
642
  ```typescript
331
643
  interface CliServerOptions {
332
- basePath?: string; // API base path (default: '/api/cli')
333
- cors?: boolean | cors.CorsOptions; // CORS config (default: false)
644
+ basePath?: string; // API base path (default: '/api/qcli')
645
+ cors?: boolean | cors.CorsOptions; // CORS config (default: only allowedOrigins; off when none)
646
+ allowedOrigins?: string[]; // CORS + WebSocket Origin allow-list ('http://host:*' = any port)
647
+ enableShell?: boolean; // Interactive shell WebSocket (default: false)
648
+ shell?: CliShellOptions; // Shell access mode, limits, timeouts, env (see Remote Shell Security)
649
+ trustProxy?: boolean | number; // Trusted proxy hops for X-Forwarded-* (true = 1; default false = none)
650
+ auth?: ICliServerAuth; // Protect the whole server (e.g. usersPlugin.serverAuth)
651
+ mcp?: boolean | CliMcpOptions; // MCP endpoint at {basePath}/mcp (default off, see MCP Endpoint)
334
652
  configure?: (builder: CliBuilder) => void; // Processor registration
335
653
  }
336
654
  ```
337
655
 
656
+ CORS is disabled unless `allowedOrigins` (or an explicit `cors` option) is given. The shell socket is off unless `enableShell: true`, and even then it is locked until an access mode applies (see [Remote Shell Security](#remote-shell-security)).
657
+
658
+ Same-origin requests (e.g. the bundled admin dashboard at `/qcli/admin`) are always allowed by the Origin, CORS and CSRF checks, even when `allowedOrigins` does not list the server's own origin (`X-Forwarded-Proto`/`X-Forwarded-Host` are honoured only with `trustProxy`, and read like `X-Forwarded-For`: the entry at index `len - hops`, the leftmost when the list is shorter, an empty entry ignored).
659
+
660
+ `trustProxy` is the number of trusted reverse-proxy hops in front of the server (`true` = 1, `false`/`0`/absent = none). With n hops the client IP (Express `req.ip`, the shell's per-address limits) is the `X-Forwarded-For` entry at index `len - n` of the comma-separated list, i.e. with one hop the rightmost entry, appended by your proxy; the leftmost entries are client-controlled and never used unless the list is shorter than n. Without the header (or when the selected entry is empty) the socket address is used. Set it to the exact number of proxies you run, otherwise clients can spoof their IP.
661
+
662
+ When the users plugin is mounted (`mountPlugin(usersPlugin)`) or passed as `auth`, every `/api/...` route and every WebSocket requires a users session (see [Users Plugin](#users-plugin)); without it the server behaves as before.
663
+
338
664
  `createCliServer()` returns:
339
665
 
340
666
  ```typescript
@@ -342,7 +668,12 @@ interface CliServerOptions {
342
668
  app: Express; // Configured Express app
343
669
  registry: CliCommandRegistry; // Processor registry
344
670
  builder: CliBuilder; // Registration builder
345
- eventSocketManager: CliEventSocketManager; // WebSocket manager
671
+ executor: CliCommandExecutorService; // Command executor
672
+ eventSocketManager: CliEventSocketManager; // WebSocket event manager
673
+ logSocketManager: CliLogSocketManager; // WebSocket log manager
674
+ mountPlugin(plugin: MountablePlugin): void; // Mount a plugin with its routes (installs plugin.serverAuth)
675
+ setAuth(auth?: ICliServerAuth): void; // Install/remove the server auth provider
676
+ mcp: CliServerMcp; // MCP endpoint: { enabled, path, addProvider(provider) }
346
677
  }
347
678
  ```
348
679
 
@@ -391,19 +722,19 @@ import {
391
722
 
392
723
  ## File Storage
393
724
 
394
- The server includes a pluggable file storage system exposed at `/api/cli/fs/*`. Enable it with `setFileStorageProvider()` and choose a storage backend.
725
+ The server includes a pluggable file storage system exposed at `/api/qcli/fs/*`. Enable it with `setFileStorageProvider()` and choose a storage backend.
395
726
 
396
727
  ### Filesystem API Endpoints
397
728
 
398
729
  | Method | Path | Description |
399
730
  |--------|------|-------------|
400
- | GET | `/api/cli/fs/ls?path=/` | List directory contents |
401
- | GET | `/api/cli/fs/cat?path=/file.txt` | Read file content |
402
- | GET | `/api/cli/fs/stat?path=/file.txt` | File/directory metadata |
403
- | GET | `/api/cli/fs/download?path=/file.txt` | Download file |
404
- | POST | `/api/cli/fs/upload` | Upload file (multipart) |
405
- | POST | `/api/cli/fs/mkdir` | Create directory |
406
- | DELETE | `/api/cli/fs/rm?path=/file.txt` | Delete file or directory |
731
+ | GET | `/api/qcli/fs/ls?path=/` | List directory contents |
732
+ | GET | `/api/qcli/fs/cat?path=/file.txt` | Read file content |
733
+ | GET | `/api/qcli/fs/stat?path=/file.txt` | File/directory metadata |
734
+ | GET | `/api/qcli/fs/download?path=/file.txt` | Download file |
735
+ | POST | `/api/qcli/fs/upload` | Upload file (multipart) |
736
+ | POST | `/api/qcli/fs/mkdir` | Create directory |
737
+ | DELETE | `/api/qcli/fs/rm?path=/file.txt` | Delete file or directory |
407
738
 
408
739
  ### Storage Providers
409
740
 
@@ -615,6 +946,40 @@ All commands support `--region` (`-r`) for region override and `--output` (`-o`)
615
946
 
616
947
  See [`plugins/aws/README.md`](plugins/aws/README.md) for the full command reference.
617
948
 
949
+ ### Users Plugin
950
+
951
+ The users plugin (`@qodalis/cli-server-plugin-users`) adds REST endpoints under `/api/v1/qcli` for authentication, users, groups with permissions, sessions and an audit log, with swappable storage (in-memory by default). It is the server side of the `@qodalis/cli-users` browser plugin's *server mode*.
952
+
953
+ ```typescript
954
+ import { CliUsersBuilder } from '@qodalis/cli-server-plugin-users';
955
+
956
+ const usersPlugin = new CliUsersBuilder()
957
+ .build((msg) => eventSocketManager.broadcastMessage(msg));
958
+ mountPlugin(usersPlugin); // protects the whole server (protectServer: true)
959
+ ```
960
+
961
+ Once mounted, every server route and socket requires a users session (login, version discovery and capabilities stay public):
962
+
963
+ | Surface | Permission |
964
+ |---|---|
965
+ | `GET /commands` | authenticated |
966
+ | `POST /execute`, `/execute/stream` | `commands:execute` + the processor's `requiredPermissions` |
967
+ | `POST /api/qcli/mcp` | `commands:execute` (each tool then checks its own permissions) |
968
+ | fs `ls`/`cat`/`stat`/`download` | `files:read` |
969
+ | fs `upload`/`mkdir`/`rm` | `files:write` |
970
+ | data explorer | `data:query` |
971
+ | jobs read / manage | `jobs:read` / `jobs:manage` |
972
+ | events / logs / shell WebSocket | authenticated / `logs:read` / `shell:execute` (see [Remote Shell Security](#remote-shell-security)) |
973
+ | admin dashboard | `admin:access` |
974
+
975
+ WebSockets authenticate with a single-use ticket: `POST /api/v1/qcli/sessions/ws-ticket` → `{ ticket, expiresIn: 30 }`, then connect with `?ticket=<ticket>`. Host routes can use `usersPlugin.requireAuth()` / `usersPlugin.requirePermission('perm')`. Processors declare `requiredPermissions: ['deploy:run']` to be enforced server-side.
976
+
977
+ Events and logs sockets authenticated with a stored session are bound to it: when the session ends (logout, revoke/kick, user disabled or deleted, a password change becoming required, idle/absolute expiry) the socket is closed with **4401** `session ended`; for a revoke the events socket first receives `session-kicked`. Bound sockets are re-checked on every session/user/group change and after each expired-session sweep (`setCleanupInterval`, default 5 minutes). Sockets authenticated by an external auth adapter have no stored session and are not re-checked.
978
+
979
+ API tokens (`qcli_pat_…`) are long-lived, revocable bearer tokens for MCP clients, CI and scripts; they are accepted wherever a session token is (see [API tokens](plugins/users/README.md#api-tokens)). Sockets opened with a token identity close with 4401 when the token is revoked or expires, or the user is disabled.
980
+
981
+ See [`plugins/users/README.md`](plugins/users/README.md) for options, endpoints, API tokens, custom storage and external auth adapters.
982
+
618
983
  ## Built-in Processors
619
984
 
620
985
  These processors ship with the library and are included in the standalone server:
@@ -627,7 +992,6 @@ These processors ship with the library and are included in the standalone server
627
992
  | `http` | HTTP request operations |
628
993
  | `hash` | Hash computation (MD5, SHA1, SHA256, SHA512) |
629
994
  | `base64` | Base64 encode/decode (sub-commands) |
630
- | `uuid` | UUID generation |
631
995
 
632
996
  ## Docker
633
997
 
@@ -663,10 +1027,22 @@ packages/
663
1027
  cli-command-parameter-descriptor.ts # Parameter declaration
664
1028
  cli-command-author.ts # Author metadata
665
1029
  plugins/
1030
+ admin/ # Admin dashboard plugin (auth, status, config, logs)
1031
+ aws/ # AWS cloud services (S3, EC2, Lambda, CloudWatch, etc.)
1032
+ data-explorer/ # Core data explorer abstraction
1033
+ data-explorer-sql/ # SQL data explorer provider (SQLite)
1034
+ data-explorer-mongo/ # MongoDB data explorer provider
1035
+ data-explorer-postgres/ # PostgreSQL data explorer provider
1036
+ data-explorer-mysql/ # MySQL data explorer provider
1037
+ data-explorer-mssql/ # MS SQL data explorer provider
1038
+ data-explorer-redis/ # Redis data explorer provider
1039
+ data-explorer-elasticsearch/ # Elasticsearch data explorer provider
666
1040
  filesystem/ # Core file storage abstraction (IFileStorageProvider, InMemory, OS)
667
1041
  filesystem-json/ # JSON file persistence provider
668
1042
  filesystem-sqlite/ # SQLite persistence provider (better-sqlite3)
669
1043
  filesystem-s3/ # Amazon S3 storage provider (@aws-sdk/client-s3)
1044
+ jobs/ # Job scheduling plugin
1045
+ users/ # Users plugin (auth, sessions, groups, audit)
670
1046
  weather/ # Weather module (example plugin)
671
1047
  src/
672
1048
  abstractions/ # Re-exports from @qodalis/cli-server-abstractions
@@ -679,9 +1055,11 @@ src/
679
1055
  cli-command-executor-service.ts # Command execution pipeline
680
1056
  cli-response-builder.ts # Structured output builder
681
1057
  cli-event-socket-manager.ts # WebSocket event broadcasting
1058
+ cli-log-socket-manager.ts # WebSocket log streaming
1059
+ cli-shell-session-manager.ts # PTY shell session management
682
1060
  controllers/
683
- cli-controller.ts # V1 REST API (/api/v1/cli)
684
- cli-version-controller.ts # Version discovery (/api/cli/versions)
1061
+ cli-controller.ts # V1 REST API (/api/v1/qcli)
1062
+ cli-version-controller.ts # Version discovery (/api/qcli/versions)
685
1063
  extensions/
686
1064
  cli-builder.ts # Fluent registration API (addProcessor, addModule)
687
1065
  processors/ # Built-in processors
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@qodalis/cli-server-node",
3
+ "version": "1.0.0-beta.7",
4
+ "description": "Node.js CLI server for Qodalis CLI ecosystem — use as a library in existing Express apps or run standalone",
5
+ "main": "dist/src/index.js",
6
+ "types": "dist/src/index.d.ts",
7
+ "bin": {
8
+ "qodalis-cli-server": "dist/src/server.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "!dist/src/__tests__",
13
+ "!dist/plugins",
14
+ "LICENSE",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "start": "tsx src/server.ts",
20
+ "dev": "tsx watch src/server.ts",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest"
23
+ },
24
+ "keywords": [
25
+ "cli",
26
+ "qodalis",
27
+ "terminal",
28
+ "commands",
29
+ "cli-server",
30
+ "command-processor"
31
+ ],
32
+ "author": "Nicolae Lupei <nicolae.lupei@qodalis.com>",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/qodalis-solutions/cli-server-node"
37
+ },
38
+ "homepage": "https://qodalis.com/",
39
+ "dependencies": {
40
+ "@qodalis/cli-server-abstractions": "file:packages/abstractions",
41
+ "@qodalis/cli-server-plugin-data-explorer": "file:plugins/data-explorer",
42
+ "@qodalis/cli-server-plugin-filesystem": "file:plugins/filesystem",
43
+ "@qodalis/cli-server-plugin-jobs": "file:plugins/jobs",
44
+ "cors": "^2.8.5",
45
+ "express": "^4.21.0",
46
+ "multer": "^2.1.0",
47
+ "node-pty": "^1.1.0",
48
+ "ws": "^8.18.0"
49
+ },
50
+ "optionalDependencies": {
51
+ "@elastic/elasticsearch": "^8.16.0",
52
+ "@qodalis/cli-server-plugin-filesystem-json": "file:plugins/filesystem-json",
53
+ "@qodalis/cli-server-plugin-filesystem-s3": "file:plugins/filesystem-s3",
54
+ "@qodalis/cli-server-plugin-filesystem-sqlite": "file:plugins/filesystem-sqlite",
55
+ "better-sqlite3": "^11.10.0",
56
+ "ioredis": "^5.4.0",
57
+ "mongodb": "^6.0.0",
58
+ "mssql": "^11.0.0",
59
+ "mysql2": "^3.11.0",
60
+ "pg": "^8.13.0"
61
+ },
62
+ "devDependencies": {
63
+ "@types/better-sqlite3": "^7.6.13",
64
+ "@types/cors": "^2.8.17",
65
+ "@types/express": "^4.17.21",
66
+ "@types/mssql": "^9.1.9",
67
+ "@types/multer": "^2.0.0",
68
+ "@types/node": "^22.0.0",
69
+ "@types/pg": "^8.0.0",
70
+ "@types/supertest": "^7.2.0",
71
+ "@types/ws": "^8.5.13",
72
+ "supertest": "^7.2.2",
73
+ "tsx": "^4.19.0",
74
+ "typescript": "^5.6.0",
75
+ "vitest": "^4.0.18"
76
+ }
77
+ }
@@ -0,0 +1,33 @@
1
+ import { RequestHandler } from 'express';
2
+ import { ICliCommandRegistry } from '../services/cli-command-registry';
3
+ import { CliServerAuthContext, ICliServerAuth } from './cli-server-auth';
4
+ declare global {
5
+ namespace Express {
6
+ interface Request {
7
+ /** Set by the server auth gate when the users plugin protects the server. */
8
+ qcliServerAuth?: CliServerAuthContext;
9
+ }
10
+ }
11
+ }
12
+ /** Normalizes a request path for classification: lower-case, single slashes, no trailing slash. */
13
+ export declare function normalizePath(path: string): string;
14
+ export interface ServerAuthGateOptions {
15
+ registry: ICliCommandRegistry;
16
+ /** Prefixes serving the CLI controller (`/api/v1/qcli`, custom basePath). */
17
+ cliPrefixes: string[];
18
+ /** Extra public routes as `"METHOD /path"` (e.g. a plugin's own login). */
19
+ publicRoutes: Set<string>;
20
+ /** MCP endpoint paths (normalized), requiring `commands:execute`. */
21
+ mcpPaths?: string[];
22
+ }
23
+ /** Collects `requiredPermissions` along the resolved processor chain. */
24
+ export declare function processorPermissions(registry: ICliCommandRegistry, command: unknown, chain: unknown): string[];
25
+ /** Permissions required for a (protected) server route. */
26
+ export declare function requiredServerPermissions(method: string, path: string, body: any, options: ServerAuthGateOptions): string[];
27
+ /**
28
+ * Express middleware protecting every `/api/...` route of the CLI server when
29
+ * an auth provider with `protectServer` is installed. Without a provider it is
30
+ * a no-op, so servers without the users plugin behave as before.
31
+ */
32
+ export declare function createServerAuthGate(getAuth: () => ICliServerAuth | undefined, options: ServerAuthGateOptions): RequestHandler;
33
+ //# sourceMappingURL=cli-server-auth-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-server-auth-gate.d.ts","sourceRoot":"","sources":["../../../src/auth/cli-server-auth-gate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,cAAc,EAAE,MAAM,SAAS,CAAC;AAE1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAA6B,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAKpG,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,OAAO,CAAC;QACd,UAAU,OAAO;YACb,6EAA6E;YAC7E,cAAc,CAAC,EAAE,oBAAoB,CAAC;SACzC;KACJ;CACJ;AAED,mGAAmG;AACnG,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGlD;AAgBD,MAAM,WAAW,qBAAqB;IAClC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,6EAA6E;IAC7E,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,2EAA2E;IAC3E,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1B,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,yEAAyE;AACzE,wBAAgB,oBAAoB,CAChC,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,OAAO,GACf,MAAM,EAAE,CAkBV;AAED,2DAA2D;AAC3D,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,qBAAqB,GAAG,MAAM,EAAE,CAoB3H;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAChC,OAAO,EAAE,MAAM,cAAc,GAAG,SAAS,EACzC,OAAO,EAAE,qBAAqB,GAC/B,cAAc,CAkDhB"}