@llm4ts/shell 2.45.0 → 2.46.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.
Files changed (64) hide show
  1. package/dist/Cli.d.ts +3 -7
  2. package/dist/Cli.d.ts.map +1 -1
  3. package/dist/Cli.js +20 -4
  4. package/dist/Cli.js.map +1 -1
  5. package/dist/FlowLaunch.d.ts +2 -16
  6. package/dist/FlowLaunch.d.ts.map +1 -1
  7. package/dist/FlowLaunch.js +20 -66
  8. package/dist/FlowLaunch.js.map +1 -1
  9. package/dist/FlowProcess.d.ts +67 -0
  10. package/dist/FlowProcess.d.ts.map +1 -0
  11. package/dist/FlowProcess.js +175 -0
  12. package/dist/FlowProcess.js.map +1 -0
  13. package/dist/Menu.d.ts +1 -1
  14. package/dist/Menu.d.ts.map +1 -1
  15. package/dist/OperatorActTools.d.ts +170 -0
  16. package/dist/OperatorActTools.d.ts.map +1 -0
  17. package/dist/OperatorActTools.js +332 -0
  18. package/dist/OperatorActTools.js.map +1 -0
  19. package/dist/OperatorConfig.d.ts +44 -0
  20. package/dist/OperatorConfig.d.ts.map +1 -0
  21. package/dist/OperatorConfig.js +88 -0
  22. package/dist/OperatorConfig.js.map +1 -0
  23. package/dist/OperatorNode.d.ts +45 -0
  24. package/dist/OperatorNode.d.ts.map +1 -0
  25. package/dist/OperatorNode.js +103 -0
  26. package/dist/OperatorNode.js.map +1 -0
  27. package/dist/OperatorRuns.d.ts +76 -0
  28. package/dist/OperatorRuns.d.ts.map +1 -0
  29. package/dist/OperatorRuns.js +146 -0
  30. package/dist/OperatorRuns.js.map +1 -0
  31. package/dist/OperatorServer.d.ts +160 -0
  32. package/dist/OperatorServer.d.ts.map +1 -0
  33. package/dist/OperatorServer.js +123 -0
  34. package/dist/OperatorServer.js.map +1 -0
  35. package/dist/OperatorTools.d.ts +558 -0
  36. package/dist/OperatorTools.d.ts.map +1 -0
  37. package/dist/OperatorTools.js +398 -0
  38. package/dist/OperatorTools.js.map +1 -0
  39. package/dist/Refine.d.ts +2 -2
  40. package/dist/Refine.d.ts.map +1 -1
  41. package/dist/Refine.js +3 -5
  42. package/dist/Refine.js.map +1 -1
  43. package/dist/RunSupervisor.d.ts +51 -0
  44. package/dist/RunSupervisor.d.ts.map +1 -0
  45. package/dist/RunSupervisor.js +282 -0
  46. package/dist/RunSupervisor.js.map +1 -0
  47. package/dist/ShellErrors.d.ts +9 -0
  48. package/dist/ShellErrors.d.ts.map +1 -0
  49. package/dist/ShellErrors.js +7 -0
  50. package/dist/ShellErrors.js.map +1 -0
  51. package/flows/port-guide.js +4 -1
  52. package/package.json +13 -4
  53. package/src/Cli.ts +64 -3
  54. package/src/FlowLaunch.ts +20 -66
  55. package/src/FlowProcess.ts +239 -0
  56. package/src/OperatorActTools.ts +407 -0
  57. package/src/OperatorConfig.ts +148 -0
  58. package/src/OperatorNode.ts +147 -0
  59. package/src/OperatorRuns.ts +270 -0
  60. package/src/OperatorServer.ts +192 -0
  61. package/src/OperatorTools.ts +610 -0
  62. package/src/Refine.ts +4 -7
  63. package/src/RunSupervisor.ts +403 -0
  64. package/src/ShellErrors.ts +6 -0
@@ -0,0 +1,192 @@
1
+ import { timingSafeEqual } from "node:crypto"
2
+ import { createServer } from "node:http"
3
+ import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"
4
+ import * as NodeStdio from "@effect/platform-node/NodeStdio"
5
+ import type * as Cause from "effect/Cause"
6
+ import * as Clock from "effect/Clock"
7
+ import * as Effect from "effect/Effect"
8
+ import * as Layer from "effect/Layer"
9
+ import * as Logger from "effect/Logger"
10
+ import * as Option from "effect/Option"
11
+ import * as Schema from "effect/Schema"
12
+ import * as McpProtocol from "effect/ai/McpProtocol"
13
+ import * as McpServer from "effect/ai/McpServer"
14
+ import * as Toolkit from "effect/ai/Toolkit"
15
+ import * as HttpRouter from "effect/http/HttpRouter"
16
+ import type { ServeError } from "effect/http/HttpServerError"
17
+ import * as HttpServerRequest from "effect/http/HttpServerRequest"
18
+ import * as HttpServerResponse from "effect/http/HttpServerResponse"
19
+ import {
20
+ actHandlers,
21
+ actToolkit,
22
+ approveTool,
23
+ retroStartTool,
24
+ startRunTool,
25
+ stopRunTool,
26
+ waitHandlers,
27
+ waitToolkit
28
+ } from "./OperatorActTools.ts"
29
+ import type { OperatorConfig } from "./OperatorConfig.ts"
30
+ import { readHandlers, readToolkit, type OperatorDeps } from "./OperatorTools.ts"
31
+ import { packageVersion } from "./Package.ts"
32
+
33
+ /**
34
+ * The operator MCP server (`llm4ts mcp`, ADR 0041): the read, wait and allowed
35
+ * act tools served over stdio, or over HTTP at `/mcp` behind a bearer token.
36
+ * A tool that the configuration does not allow is never registered, so it is
37
+ * absent from `tools/list` rather than refused when called.
38
+ */
39
+
40
+ export const serverName = "llm4ts"
41
+ export const serverPath = "/mcp"
42
+
43
+ type HttpTransport = Extract<OperatorConfig["transport"], { readonly _tag: "http" }>
44
+ export type HttpOperatorConfig = OperatorConfig & { readonly transport: HttpTransport }
45
+
46
+ const serverInfo = {
47
+ name: serverName,
48
+ version: packageVersion,
49
+ protocols: [McpProtocol.v2025_06_18]
50
+ } as const
51
+
52
+ const runToolkit = Toolkit.make(startRunTool, stopRunTool, retroStartTool)
53
+ const approveToolkit = Toolkit.make(approveTool)
54
+
55
+ /** The act tools a configuration allows: `--allow-run` and `--allow-approve` each add their own. */
56
+ export const actToolkitFor = (config: OperatorConfig) =>
57
+ config.allowRun
58
+ ? config.allowApprove
59
+ ? actToolkit
60
+ : runToolkit
61
+ : config.allowApprove
62
+ ? approveToolkit
63
+ : Toolkit.empty
64
+
65
+ const actLayer = (config: OperatorConfig, deps: OperatorDeps): Layer.Layer<never> => {
66
+ const act = actHandlers(deps)
67
+ if (config.allowRun && config.allowApprove) {
68
+ return McpServer.toolkit(actToolkit).pipe(Layer.provide(actToolkit.toLayer(act)))
69
+ }
70
+ if (config.allowRun) {
71
+ const handlers = runToolkit.of({
72
+ start_run: act.start_run,
73
+ stop_run: act.stop_run,
74
+ retro_start: act.retro_start
75
+ })
76
+ return McpServer.toolkit(runToolkit).pipe(Layer.provide(runToolkit.toLayer(handlers)))
77
+ }
78
+ if (config.allowApprove) {
79
+ const handlers = approveToolkit.of({ approve: act.approve })
80
+ return McpServer.toolkit(approveToolkit).pipe(Layer.provide(approveToolkit.toLayer(handlers)))
81
+ }
82
+ return Layer.empty
83
+ }
84
+
85
+ /** Every tool this configuration serves, registered on the server it is provided. */
86
+ export const toolLayer = (
87
+ config: OperatorConfig,
88
+ deps: OperatorDeps
89
+ ): Layer.Layer<never, never, McpServer.McpServer> =>
90
+ Layer.mergeAll(
91
+ McpServer.toolkit(readToolkit).pipe(Layer.provide(readToolkit.toLayer(readHandlers(deps)))),
92
+ McpServer.toolkit(waitToolkit).pipe(Layer.provide(waitToolkit.toLayer(waitHandlers(deps)))),
93
+ actLayer(config, deps)
94
+ )
95
+
96
+ const unauthorized = HttpServerResponse.empty({ status: 401 })
97
+
98
+ /**
99
+ * Lets a request through only with `Authorization: Bearer <token>`, compared in
100
+ * constant time; anything else is a bare 401. The token is never logged.
101
+ */
102
+ export const bearerMiddleware = (token: string): Layer.Layer<never> => {
103
+ const expected = Buffer.from(`Bearer ${token}`, "utf8")
104
+ const matches = (header: string | undefined): boolean => {
105
+ if (header === undefined) return false
106
+ const actual = Buffer.from(header, "utf8")
107
+ return actual.length === expected.length && timingSafeEqual(actual, expected)
108
+ }
109
+ return HttpRouter.middleware((httpEffect) =>
110
+ Effect.gen(function* () {
111
+ const request = yield* HttpServerRequest.HttpServerRequest
112
+ return matches(request.headers.authorization) ? yield* httpEffect : unauthorized
113
+ })
114
+ ).layer
115
+ }
116
+
117
+ /** The HTTP application: the tools on an MCP endpoint at `/mcp`, behind the bearer token. */
118
+ export const httpAppLayer = (
119
+ config: HttpOperatorConfig,
120
+ deps: OperatorDeps
121
+ ): Layer.Layer<never, Cause.IllegalArgumentError, HttpRouter.HttpRouter> =>
122
+ toolLayer(config, deps).pipe(
123
+ Layer.provide(McpServer.layerHttp({ ...serverInfo, path: serverPath })),
124
+ Layer.provide(bearerMiddleware(config.transport.token))
125
+ )
126
+
127
+ const RpcCall = Schema.Struct({
128
+ method: Schema.String,
129
+ params: Schema.optional(Schema.Struct({ name: Schema.optional(Schema.String) }))
130
+ })
131
+ const decodeRpcCall = Schema.decodeUnknownOption(Schema.fromJsonString(RpcCall))
132
+
133
+ /** What a request asked for: its JSON-RPC method and, for `tools/call`, the tool. Never its arguments. */
134
+ const requestLabel = (body: string): string =>
135
+ Option.match(decodeRpcCall(body), {
136
+ onNone: () => "request",
137
+ onSome: (call) =>
138
+ call.method === "tools/call" && call.params?.name !== undefined
139
+ ? `tools/call ${call.params.name}`
140
+ : call.method
141
+ })
142
+
143
+ /**
144
+ * One log line per HTTP request: method, tool, duration and status. The body
145
+ * is read only after the endpoint has read it (the read is cached), and never
146
+ * for a request the token check turned away.
147
+ */
148
+ const requestLog = HttpRouter.middleware(
149
+ (httpEffect) =>
150
+ Effect.gen(function* () {
151
+ const request = yield* HttpServerRequest.HttpServerRequest
152
+ const startedAt = yield* Clock.currentTimeMillis
153
+ const response = yield* httpEffect
154
+ const elapsed = (yield* Clock.currentTimeMillis) - startedAt
155
+ const label =
156
+ response.status === 401 || request.method !== "POST"
157
+ ? request.method
158
+ : yield* request.text.pipe(
159
+ Effect.map(requestLabel),
160
+ Effect.orElseSucceed(() => "request")
161
+ )
162
+ yield* Effect.logInfo(`mcp ${label} ${elapsed}ms status=${response.status}`)
163
+ return response
164
+ }),
165
+ { global: true }
166
+ )
167
+
168
+ /** Serves the operator tools on the configured transport until interrupted. */
169
+ export const runOperatorServer = (
170
+ config: OperatorConfig,
171
+ deps: OperatorDeps
172
+ ): Effect.Effect<never, ServeError | Cause.IllegalArgumentError> => {
173
+ const transport = config.transport
174
+ if (transport._tag === "stdio") {
175
+ // stdout carries the protocol: every log line goes to stderr.
176
+ return Layer.launch(
177
+ toolLayer(config, deps).pipe(
178
+ Layer.provide(McpServer.layerStdio(serverInfo)),
179
+ Layer.provide(NodeStdio.layer)
180
+ )
181
+ ).pipe(Effect.provideService(Logger.LogToStderr, true))
182
+ }
183
+ return Layer.launch(
184
+ HttpRouter.serve(Layer.merge(httpAppLayer({ ...config, transport }, deps), requestLog), {
185
+ disableLogger: true
186
+ }).pipe(
187
+ Layer.provide(
188
+ NodeHttpServer.layer(createServer, { host: transport.host, port: transport.port })
189
+ )
190
+ )
191
+ )
192
+ }