@neurocode-ai/server 1.18.8
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/package.json +24 -0
- package/src/api.ts +8 -0
- package/src/auth.ts +63 -0
- package/src/cors.ts +34 -0
- package/src/handlers/agent.ts +13 -0
- package/src/handlers/command.ts +8 -0
- package/src/handlers/credential.ts +22 -0
- package/src/handlers/event.ts +52 -0
- package/src/handlers/fs.ts +39 -0
- package/src/handlers/health.ts +7 -0
- package/src/handlers/integration.ts +104 -0
- package/src/handlers/location.ts +18 -0
- package/src/handlers/message.ts +81 -0
- package/src/handlers/model.ts +17 -0
- package/src/handlers/permission.ts +98 -0
- package/src/handlers/project-copy.ts +68 -0
- package/src/handlers/provider.ts +32 -0
- package/src/handlers/pty.ts +223 -0
- package/src/handlers/question.ts +62 -0
- package/src/handlers/reference.ts +8 -0
- package/src/handlers/session.ts +385 -0
- package/src/handlers/skill.ts +8 -0
- package/src/handlers.ts +40 -0
- package/src/location.ts +60 -0
- package/src/middleware/authorization.ts +58 -0
- package/src/middleware/schema-error.ts +20 -0
- package/src/middleware/session-location.ts +67 -0
- package/src/pty-environment.ts +19 -0
- package/src/routes.ts +68 -0
- package/sst-env.d.ts +10 -0
- package/tsconfig.json +8 -0
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
+
"name": "@neurocode-ai/server",
|
|
4
|
+
"version": "1.18.8",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./*": "./src/*.ts"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"typecheck": "tsgo --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@neurocode-ai/core": "1.18.8",
|
|
15
|
+
"@neurocode-ai/protocol": "1.18.8",
|
|
16
|
+
"drizzle-orm": "1.0.0-rc.2",
|
|
17
|
+
"effect": "4.0.0-beta.83"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@tsconfig/bun": "1.0.9",
|
|
21
|
+
"@types/bun": "1.3.13",
|
|
22
|
+
"@typescript/native-preview": "7.0.0-dev.20251207.1"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { makeDefaultApi } from "@neurocode-ai/protocol/api"
|
|
2
|
+
import { LocationMiddleware } from "./location"
|
|
3
|
+
import { SessionLocationMiddleware } from "./middleware/session-location"
|
|
4
|
+
|
|
5
|
+
export const Api = makeDefaultApi({
|
|
6
|
+
locationMiddleware: LocationMiddleware,
|
|
7
|
+
sessionLocationMiddleware: SessionLocationMiddleware,
|
|
8
|
+
})
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export * as ServerAuth from "./auth"
|
|
2
|
+
|
|
3
|
+
import { Config as EffectConfig, Context, Effect, Layer, Option, Redacted } from "effect"
|
|
4
|
+
|
|
5
|
+
export type Credentials = {
|
|
6
|
+
password?: string
|
|
7
|
+
username?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type DecodedCredentials = {
|
|
11
|
+
readonly username: string
|
|
12
|
+
readonly password: Redacted.Redacted
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type Info = {
|
|
16
|
+
readonly password: Option.Option<string>
|
|
17
|
+
readonly username: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class Config extends Context.Service<Config, Info>()("@neurocode/ServerAuthConfig") {
|
|
21
|
+
static configLayer(input: Info) {
|
|
22
|
+
return Layer.succeed(this, this.of(input))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static get layer() {
|
|
26
|
+
return Layer.effect(
|
|
27
|
+
this,
|
|
28
|
+
Effect.gen(function* () {
|
|
29
|
+
return Config.of(
|
|
30
|
+
yield* EffectConfig.all({
|
|
31
|
+
password: EffectConfig.string("NEUROCODE_SERVER_PASSWORD").pipe(EffectConfig.option),
|
|
32
|
+
username: EffectConfig.string("NEUROCODE_SERVER_USERNAME").pipe(EffectConfig.withDefault("opencode")),
|
|
33
|
+
}),
|
|
34
|
+
)
|
|
35
|
+
}),
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function required(config: Info) {
|
|
41
|
+
return Option.isSome(config.password) && config.password.value !== ""
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function authorized(credentials: DecodedCredentials, config: Info) {
|
|
45
|
+
return (
|
|
46
|
+
Option.isSome(config.password) &&
|
|
47
|
+
credentials.username === config.username &&
|
|
48
|
+
Redacted.value(credentials.password) === config.password.value
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function header(credentials?: Credentials) {
|
|
53
|
+
const password = credentials?.password ?? process.env.NEUROCODE_SERVER_PASSWORD
|
|
54
|
+
if (!password) return undefined
|
|
55
|
+
|
|
56
|
+
return `Basic ${Buffer.from(`${credentials?.username ?? process.env.NEUROCODE_SERVER_USERNAME ?? "opencode"}:${password}`).toString("base64")}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function headers(credentials?: Credentials) {
|
|
60
|
+
const authorization = header(credentials)
|
|
61
|
+
if (!authorization) return undefined
|
|
62
|
+
return { Authorization: authorization }
|
|
63
|
+
}
|
package/src/cors.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Context } from "effect"
|
|
2
|
+
|
|
3
|
+
const opencodeOrigin = /^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/
|
|
4
|
+
|
|
5
|
+
export type CorsOptions = { readonly cors?: ReadonlyArray<string> }
|
|
6
|
+
|
|
7
|
+
export const CorsConfig = Context.Reference<CorsOptions | undefined>("@neurocode/ServerCorsConfig", {
|
|
8
|
+
defaultValue: () => undefined,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export function isAllowedCorsOrigin(input: string | undefined, opts?: CorsOptions) {
|
|
12
|
+
if (!input) return true
|
|
13
|
+
if (input.startsWith("http://localhost:")) return true
|
|
14
|
+
if (input.startsWith("http://127.0.0.1:")) return true
|
|
15
|
+
if (input.startsWith("oc://renderer")) return true
|
|
16
|
+
if (input === "tauri://localhost" || input === "http://tauri.localhost" || input === "https://tauri.localhost")
|
|
17
|
+
return true
|
|
18
|
+
if (opencodeOrigin.test(input)) return true
|
|
19
|
+
return opts?.cors?.includes(input) ?? false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isAllowedRequestOrigin(input: string | undefined, host: string | undefined, opts?: CorsOptions) {
|
|
23
|
+
if (!input) return true
|
|
24
|
+
if (host && sameHost(input, host)) return true
|
|
25
|
+
return isAllowedCorsOrigin(input, opts)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sameHost(origin: string, host: string) {
|
|
29
|
+
try {
|
|
30
|
+
return new URL(origin).host === host
|
|
31
|
+
} catch {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AgentV2 } from "@neurocode-ai/core/agent"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
|
4
|
+
import { Api } from "../api"
|
|
5
|
+
import { response } from "../location"
|
|
6
|
+
|
|
7
|
+
export const AgentHandler = HttpApiBuilder.group(Api, "server.agent", (handlers) =>
|
|
8
|
+
handlers.handle("agent.list", () =>
|
|
9
|
+
Effect.gen(function* () {
|
|
10
|
+
return yield* response(AgentV2.Service.use((agent) => agent.all()))
|
|
11
|
+
}),
|
|
12
|
+
),
|
|
13
|
+
)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { CommandV2 } from "@neurocode-ai/core/command"
|
|
2
|
+
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
|
3
|
+
import { Api } from "../api"
|
|
4
|
+
import { response } from "../location"
|
|
5
|
+
|
|
6
|
+
export const CommandHandler = HttpApiBuilder.group(Api, "server.command", (handlers) =>
|
|
7
|
+
handlers.handle("command.list", () => response(CommandV2.Service.use((command) => command.list()))),
|
|
8
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Integration } from "@neurocode-ai/core/integration"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
|
4
|
+
import { Api } from "../api"
|
|
5
|
+
|
|
6
|
+
export const CredentialHandler = HttpApiBuilder.group(Api, "server.credential", (handlers) =>
|
|
7
|
+
handlers
|
|
8
|
+
.handle(
|
|
9
|
+
"credential.update",
|
|
10
|
+
Effect.fn(function* (ctx) {
|
|
11
|
+
yield* (yield* Integration.Service).connection.update(ctx.params.credentialID, { label: ctx.payload.label })
|
|
12
|
+
return HttpApiSchema.NoContent.make()
|
|
13
|
+
}),
|
|
14
|
+
)
|
|
15
|
+
.handle(
|
|
16
|
+
"credential.remove",
|
|
17
|
+
Effect.fn(function* (ctx) {
|
|
18
|
+
yield* (yield* Integration.Service).connection.remove(ctx.params.credentialID)
|
|
19
|
+
return HttpApiSchema.NoContent.make()
|
|
20
|
+
}),
|
|
21
|
+
),
|
|
22
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { EventV2 } from "@neurocode-ai/core/event"
|
|
2
|
+
import { OpenCodeEvent } from "@neurocode-ai/protocol/groups/event"
|
|
3
|
+
import { Effect, Schema, Stream } from "effect"
|
|
4
|
+
import { HttpServerResponse } from "effect/unstable/http"
|
|
5
|
+
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
|
6
|
+
import * as Sse from "effect/unstable/encoding/Sse"
|
|
7
|
+
import { Api } from "../api"
|
|
8
|
+
|
|
9
|
+
const subscriberCapacity = 256
|
|
10
|
+
|
|
11
|
+
function eventData(data: unknown): Sse.Event {
|
|
12
|
+
return {
|
|
13
|
+
_tag: "Event",
|
|
14
|
+
event: "message",
|
|
15
|
+
id: undefined,
|
|
16
|
+
data: JSON.stringify(Schema.encodeUnknownSync(OpenCodeEvent)(data)),
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const EventHandler = HttpApiBuilder.group(Api, "server.event", (handlers) =>
|
|
21
|
+
Effect.gen(function* () {
|
|
22
|
+
const events = yield* EventV2.Service
|
|
23
|
+
return handlers.handleRaw("event.subscribe", () =>
|
|
24
|
+
Effect.gen(function* () {
|
|
25
|
+
const connected = {
|
|
26
|
+
id: EventV2.ID.create(),
|
|
27
|
+
type: "server.connected",
|
|
28
|
+
data: {},
|
|
29
|
+
}
|
|
30
|
+
const output = Stream.unwrap(
|
|
31
|
+
Effect.gen(function* () {
|
|
32
|
+
// Acquiring the bounded stream installs its listener before readiness is observable.
|
|
33
|
+
const live = yield* EventV2.allBounded(events, subscriberCapacity)
|
|
34
|
+
return Stream.make(connected).pipe(Stream.concat(live))
|
|
35
|
+
}),
|
|
36
|
+
).pipe(Stream.map(eventData), Stream.pipeThroughChannel(Sse.encode()))
|
|
37
|
+
const heartbeat = Stream.tick("15 seconds").pipe(Stream.map(() => ": heartbeat\n\n"))
|
|
38
|
+
return HttpServerResponse.stream(
|
|
39
|
+
output.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }), Stream.encodeText),
|
|
40
|
+
{
|
|
41
|
+
contentType: "text/event-stream",
|
|
42
|
+
headers: {
|
|
43
|
+
"Cache-Control": "no-cache, no-transform",
|
|
44
|
+
"X-Accel-Buffering": "no",
|
|
45
|
+
"X-Content-Type-Options": "nosniff",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
}),
|
|
50
|
+
)
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { FileSystem } from "@neurocode-ai/core/filesystem"
|
|
2
|
+
import { RelativePath } from "@neurocode-ai/core/schema"
|
|
3
|
+
import { Effect } from "effect"
|
|
4
|
+
import { HttpServerResponse } from "effect/unstable/http"
|
|
5
|
+
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
|
6
|
+
import { Api } from "../api"
|
|
7
|
+
import { response } from "../location"
|
|
8
|
+
|
|
9
|
+
export const FileSystemHandler = HttpApiBuilder.group(Api, "server.fs", (handlers) =>
|
|
10
|
+
Effect.gen(function* () {
|
|
11
|
+
return handlers
|
|
12
|
+
.handleRaw("fs.read", (ctx) =>
|
|
13
|
+
Effect.gen(function* () {
|
|
14
|
+
const file = yield* (yield* FileSystem.Service).read({
|
|
15
|
+
path: RelativePath.make(
|
|
16
|
+
decodeURIComponent(new URL(ctx.request.url, "http://localhost").pathname.slice(13)),
|
|
17
|
+
),
|
|
18
|
+
})
|
|
19
|
+
return HttpServerResponse.uint8Array(file.content, { contentType: file.mime })
|
|
20
|
+
}),
|
|
21
|
+
)
|
|
22
|
+
.handle("fs.list", (ctx) =>
|
|
23
|
+
response(
|
|
24
|
+
Effect.gen(function* () {
|
|
25
|
+
const fs = yield* FileSystem.Service
|
|
26
|
+
return yield* fs.list(ctx.query)
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
.handle("fs.find", (ctx) =>
|
|
31
|
+
response(
|
|
32
|
+
Effect.gen(function* () {
|
|
33
|
+
const fs = yield* FileSystem.Service
|
|
34
|
+
return yield* fs.find(ctx.query)
|
|
35
|
+
}),
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
}),
|
|
39
|
+
)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
|
3
|
+
import { Api } from "../api"
|
|
4
|
+
|
|
5
|
+
export const HealthHandler = HttpApiBuilder.group(Api, "server.health", (handlers) =>
|
|
6
|
+
handlers.handle("health.get", () => Effect.succeed({ healthy: true as const })),
|
|
7
|
+
)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Integration } from "@neurocode-ai/core/integration"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
|
4
|
+
import { Api } from "../api"
|
|
5
|
+
import { InvalidRequestError } from "@neurocode-ai/protocol/errors"
|
|
6
|
+
import { response } from "../location"
|
|
7
|
+
|
|
8
|
+
const authorize = <A, R>(effect: Effect.Effect<A, Integration.AuthorizationError, R>) =>
|
|
9
|
+
effect.pipe(
|
|
10
|
+
Effect.mapError(
|
|
11
|
+
() =>
|
|
12
|
+
new InvalidRequestError({
|
|
13
|
+
message: "Authentication failed",
|
|
14
|
+
kind: "integration_authorization",
|
|
15
|
+
}),
|
|
16
|
+
),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration", (handlers) =>
|
|
20
|
+
Effect.gen(function* () {
|
|
21
|
+
return handlers
|
|
22
|
+
.handle(
|
|
23
|
+
"integration.list",
|
|
24
|
+
Effect.fn(function* () {
|
|
25
|
+
const service = yield* Integration.Service
|
|
26
|
+
return yield* response(service.list())
|
|
27
|
+
}),
|
|
28
|
+
)
|
|
29
|
+
.handle(
|
|
30
|
+
"integration.get",
|
|
31
|
+
Effect.fn(function* (ctx) {
|
|
32
|
+
const service = yield* Integration.Service
|
|
33
|
+
return yield* response(service.get(ctx.params.integrationID))
|
|
34
|
+
}),
|
|
35
|
+
)
|
|
36
|
+
.handle(
|
|
37
|
+
"integration.connect.key",
|
|
38
|
+
Effect.fn(function* (ctx) {
|
|
39
|
+
const service = yield* Integration.Service
|
|
40
|
+
yield* authorize(
|
|
41
|
+
service.connection.key({
|
|
42
|
+
integrationID: ctx.params.integrationID,
|
|
43
|
+
key: ctx.payload.key,
|
|
44
|
+
label: ctx.payload.label,
|
|
45
|
+
}),
|
|
46
|
+
)
|
|
47
|
+
return HttpApiSchema.NoContent.make()
|
|
48
|
+
}),
|
|
49
|
+
)
|
|
50
|
+
.handle(
|
|
51
|
+
"integration.connect.oauth",
|
|
52
|
+
Effect.fn(function* (ctx) {
|
|
53
|
+
const service = yield* Integration.Service
|
|
54
|
+
return yield* response(
|
|
55
|
+
authorize(
|
|
56
|
+
service.connection.oauth({
|
|
57
|
+
integrationID: ctx.params.integrationID,
|
|
58
|
+
methodID: ctx.payload.methodID,
|
|
59
|
+
inputs: ctx.payload.inputs,
|
|
60
|
+
label: ctx.payload.label,
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
}),
|
|
65
|
+
)
|
|
66
|
+
.handle(
|
|
67
|
+
"integration.attempt.status",
|
|
68
|
+
Effect.fn(function* (ctx) {
|
|
69
|
+
const service = yield* Integration.Service
|
|
70
|
+
return yield* response(service.attempt.status(ctx.params.attemptID))
|
|
71
|
+
}),
|
|
72
|
+
)
|
|
73
|
+
.handle(
|
|
74
|
+
"integration.attempt.complete",
|
|
75
|
+
Effect.fn(function* (ctx) {
|
|
76
|
+
const service = yield* Integration.Service
|
|
77
|
+
yield* service.attempt.complete({ attemptID: ctx.params.attemptID, code: ctx.payload.code }).pipe(
|
|
78
|
+
Effect.mapError(
|
|
79
|
+
(error) =>
|
|
80
|
+
new InvalidRequestError({
|
|
81
|
+
message:
|
|
82
|
+
error._tag === "Integration.CodeRequired"
|
|
83
|
+
? "Authorization code is required"
|
|
84
|
+
: "Authentication failed",
|
|
85
|
+
kind:
|
|
86
|
+
error._tag === "Integration.CodeRequired"
|
|
87
|
+
? "integration_code_required"
|
|
88
|
+
: "integration_authorization",
|
|
89
|
+
}),
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
return HttpApiSchema.NoContent.make()
|
|
93
|
+
}),
|
|
94
|
+
)
|
|
95
|
+
.handle(
|
|
96
|
+
"integration.attempt.cancel",
|
|
97
|
+
Effect.fn(function* (ctx) {
|
|
98
|
+
const service = yield* Integration.Service
|
|
99
|
+
yield* service.attempt.cancel(ctx.params.attemptID)
|
|
100
|
+
return HttpApiSchema.NoContent.make()
|
|
101
|
+
}),
|
|
102
|
+
)
|
|
103
|
+
}),
|
|
104
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Location } from "@neurocode-ai/core/location"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
|
4
|
+
import { Api } from "../api"
|
|
5
|
+
|
|
6
|
+
export const LocationHandler = HttpApiBuilder.group(Api, "server.location", (handlers) =>
|
|
7
|
+
handlers.handle(
|
|
8
|
+
"location.get",
|
|
9
|
+
Effect.fn(function* () {
|
|
10
|
+
const location = yield* Location.Service
|
|
11
|
+
return new Location.Info({
|
|
12
|
+
directory: location.directory,
|
|
13
|
+
workspaceID: location.workspaceID,
|
|
14
|
+
project: location.project,
|
|
15
|
+
})
|
|
16
|
+
}),
|
|
17
|
+
),
|
|
18
|
+
)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { SessionMessage } from "@neurocode-ai/core/session/message"
|
|
2
|
+
import { SessionV2 } from "@neurocode-ai/core/session"
|
|
3
|
+
import { Effect, Schema } from "effect"
|
|
4
|
+
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
|
5
|
+
import { Api } from "../api"
|
|
6
|
+
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "@neurocode-ai/protocol/errors"
|
|
7
|
+
|
|
8
|
+
const DefaultMessagesLimit = 50
|
|
9
|
+
|
|
10
|
+
const Cursor = Schema.Struct({
|
|
11
|
+
id: SessionMessage.ID,
|
|
12
|
+
order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]),
|
|
13
|
+
direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const decodeCursor = Schema.decodeUnknownSync(Cursor)
|
|
17
|
+
|
|
18
|
+
const cursor = {
|
|
19
|
+
encode(message: SessionMessage.Message, order: "asc" | "desc", direction: "previous" | "next") {
|
|
20
|
+
return Buffer.from(JSON.stringify({ id: message.id, order, direction })).toString("base64url")
|
|
21
|
+
},
|
|
22
|
+
decode(input: string) {
|
|
23
|
+
return decodeCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8")))
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const MessageHandler = HttpApiBuilder.group(Api, "server.message", (handlers) =>
|
|
28
|
+
Effect.gen(function* () {
|
|
29
|
+
const session = yield* SessionV2.Service
|
|
30
|
+
|
|
31
|
+
return handlers.handle(
|
|
32
|
+
"session.messages",
|
|
33
|
+
Effect.fn(function* (ctx) {
|
|
34
|
+
if (ctx.query.cursor && ctx.query.order !== undefined)
|
|
35
|
+
return yield* new InvalidCursorError({ message: "Cursor cannot be combined with order" })
|
|
36
|
+
const decoded = yield* Effect.try({
|
|
37
|
+
try: () => (ctx.query.cursor ? cursor.decode(ctx.query.cursor) : undefined),
|
|
38
|
+
catch: () => new InvalidCursorError({ message: "Invalid cursor" }),
|
|
39
|
+
})
|
|
40
|
+
const order = decoded?.order ?? ctx.query.order ?? "desc"
|
|
41
|
+
const messages = yield* session
|
|
42
|
+
.messages({
|
|
43
|
+
sessionID: ctx.params.sessionID,
|
|
44
|
+
limit: ctx.query.limit ?? DefaultMessagesLimit,
|
|
45
|
+
order,
|
|
46
|
+
cursor: decoded ? { id: decoded.id, direction: decoded.direction } : undefined,
|
|
47
|
+
})
|
|
48
|
+
.pipe(
|
|
49
|
+
Effect.catchTag("Session.NotFoundError", (error) =>
|
|
50
|
+
Effect.fail(
|
|
51
|
+
new SessionNotFoundError({
|
|
52
|
+
sessionID: error.sessionID,
|
|
53
|
+
message: `Session not found: ${error.sessionID}`,
|
|
54
|
+
}),
|
|
55
|
+
),
|
|
56
|
+
),
|
|
57
|
+
Effect.catchTag("Session.MessageDecodeError", (error) => {
|
|
58
|
+
const ref = `err_${crypto.randomUUID().slice(0, 8)}`
|
|
59
|
+
return Effect.logError("failed to decode session message").pipe(
|
|
60
|
+
Effect.annotateLogs({ ref, sessionID: error.sessionID, messageID: error.messageID }),
|
|
61
|
+
Effect.andThen(
|
|
62
|
+
Effect.fail(
|
|
63
|
+
new UnknownError({ message: "Unexpected server error. Check server logs for details.", ref }),
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
const first = messages[0]
|
|
70
|
+
const last = messages.at(-1)
|
|
71
|
+
return {
|
|
72
|
+
data: messages,
|
|
73
|
+
cursor: {
|
|
74
|
+
previous: first ? cursor.encode(first, order, "previous") : undefined,
|
|
75
|
+
next: last ? cursor.encode(last, order, "next") : undefined,
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
}),
|
|
79
|
+
)
|
|
80
|
+
}),
|
|
81
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Catalog } from "@neurocode-ai/core/catalog"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
|
4
|
+
import { Api } from "../api"
|
|
5
|
+
import { response } from "../location"
|
|
6
|
+
|
|
7
|
+
export const ModelHandler = HttpApiBuilder.group(Api, "server.model", (handlers) =>
|
|
8
|
+
Effect.gen(function* () {
|
|
9
|
+
return handlers.handle(
|
|
10
|
+
"model.list",
|
|
11
|
+
Effect.fn(function* () {
|
|
12
|
+
const catalog = yield* Catalog.Service
|
|
13
|
+
return yield* response(catalog.model.available())
|
|
14
|
+
}),
|
|
15
|
+
)
|
|
16
|
+
}),
|
|
17
|
+
)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Location } from "@neurocode-ai/core/location"
|
|
2
|
+
import { PermissionV2 } from "@neurocode-ai/core/permission"
|
|
3
|
+
import { PermissionSaved } from "@neurocode-ai/core/permission/saved"
|
|
4
|
+
import { Effect } from "effect"
|
|
5
|
+
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
|
6
|
+
import { Api } from "../api"
|
|
7
|
+
import { PermissionNotFoundError, SessionNotFoundError } from "@neurocode-ai/protocol/errors"
|
|
8
|
+
import { response } from "../location"
|
|
9
|
+
|
|
10
|
+
function missingRequest(id: PermissionV2.ID) {
|
|
11
|
+
return new PermissionNotFoundError({ requestID: id, message: `Permission request not found: ${id}` })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission", (handlers) =>
|
|
15
|
+
Effect.gen(function* () {
|
|
16
|
+
return handlers
|
|
17
|
+
.handle(
|
|
18
|
+
"permission.request.list",
|
|
19
|
+
Effect.fn(function* () {
|
|
20
|
+
return yield* response((yield* PermissionV2.Service).list())
|
|
21
|
+
}),
|
|
22
|
+
)
|
|
23
|
+
.handle(
|
|
24
|
+
"session.permission.create",
|
|
25
|
+
Effect.fn(function* (ctx) {
|
|
26
|
+
const permission = yield* PermissionV2.Service
|
|
27
|
+
return {
|
|
28
|
+
data: yield* permission
|
|
29
|
+
.ask({
|
|
30
|
+
id: ctx.payload.id,
|
|
31
|
+
sessionID: ctx.params.sessionID,
|
|
32
|
+
action: ctx.payload.action,
|
|
33
|
+
resources: ctx.payload.resources,
|
|
34
|
+
save: ctx.payload.save,
|
|
35
|
+
metadata: ctx.payload.metadata,
|
|
36
|
+
source: ctx.payload.source,
|
|
37
|
+
agent: ctx.payload.agent,
|
|
38
|
+
})
|
|
39
|
+
.pipe(
|
|
40
|
+
Effect.catchTag(
|
|
41
|
+
"Session.NotFoundError",
|
|
42
|
+
(error) =>
|
|
43
|
+
new SessionNotFoundError({
|
|
44
|
+
sessionID: error.sessionID,
|
|
45
|
+
message: `Session not found: ${error.sessionID}`,
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
),
|
|
49
|
+
}
|
|
50
|
+
}),
|
|
51
|
+
)
|
|
52
|
+
.handle(
|
|
53
|
+
"session.permission.list",
|
|
54
|
+
Effect.fn(function* (ctx) {
|
|
55
|
+
const permission = yield* PermissionV2.Service
|
|
56
|
+
return { data: yield* permission.forSession(ctx.params.sessionID) }
|
|
57
|
+
}),
|
|
58
|
+
)
|
|
59
|
+
.handle(
|
|
60
|
+
"session.permission.get",
|
|
61
|
+
Effect.fn(function* (ctx) {
|
|
62
|
+
const request = yield* (yield* PermissionV2.Service).get(ctx.params.requestID)
|
|
63
|
+
if (!request || request.sessionID !== ctx.params.sessionID) return yield* missingRequest(ctx.params.requestID)
|
|
64
|
+
return { data: request }
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
67
|
+
.handle(
|
|
68
|
+
"session.permission.reply",
|
|
69
|
+
Effect.fn(function* (ctx) {
|
|
70
|
+
const permission = yield* PermissionV2.Service
|
|
71
|
+
const request = yield* permission.get(ctx.params.requestID)
|
|
72
|
+
if (!request || request.sessionID !== ctx.params.sessionID) return yield* missingRequest(ctx.params.requestID)
|
|
73
|
+
yield* permission
|
|
74
|
+
.reply({ requestID: ctx.params.requestID, reply: ctx.payload.reply, message: ctx.payload.message })
|
|
75
|
+
.pipe(Effect.catchTag("PermissionV2.NotFoundError", () => missingRequest(ctx.params.requestID)))
|
|
76
|
+
return HttpApiSchema.NoContent.make()
|
|
77
|
+
}),
|
|
78
|
+
)
|
|
79
|
+
.handle(
|
|
80
|
+
"permission.saved.list",
|
|
81
|
+
Effect.fn(function* (ctx) {
|
|
82
|
+
const location = yield* Location.Service
|
|
83
|
+
return {
|
|
84
|
+
data: yield* (yield* PermissionSaved.Service).list({
|
|
85
|
+
projectID: ctx.query.projectID ?? location.project.id,
|
|
86
|
+
}),
|
|
87
|
+
}
|
|
88
|
+
}),
|
|
89
|
+
)
|
|
90
|
+
.handle(
|
|
91
|
+
"permission.saved.remove",
|
|
92
|
+
Effect.fn(function* (ctx) {
|
|
93
|
+
yield* (yield* PermissionSaved.Service).remove(ctx.params.id)
|
|
94
|
+
return HttpApiSchema.NoContent.make()
|
|
95
|
+
}),
|
|
96
|
+
)
|
|
97
|
+
}),
|
|
98
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Location } from "@neurocode-ai/core/location"
|
|
2
|
+
import { ProjectCopy } from "@neurocode-ai/core/project/copy"
|
|
3
|
+
import { Git } from "@neurocode-ai/core/git"
|
|
4
|
+
import { Effect } from "effect"
|
|
5
|
+
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
|
6
|
+
import { Api } from "../api"
|
|
7
|
+
import { ProjectCopyError } from "@neurocode-ai/protocol/groups/project-copy"
|
|
8
|
+
|
|
9
|
+
export const ProjectCopyHandler = HttpApiBuilder.group(Api, "server.projectCopy", (handlers) =>
|
|
10
|
+
Effect.succeed(
|
|
11
|
+
handlers
|
|
12
|
+
.handle("projectCopy.create", (ctx) =>
|
|
13
|
+
Effect.gen(function* () {
|
|
14
|
+
const copies = yield* ProjectCopy.Service
|
|
15
|
+
const location = yield* Location.Service
|
|
16
|
+
return yield* badRequest(
|
|
17
|
+
copies.create({
|
|
18
|
+
...ctx.payload,
|
|
19
|
+
projectID: ctx.params.projectID,
|
|
20
|
+
sourceDirectory: location.project.directory,
|
|
21
|
+
}),
|
|
22
|
+
)
|
|
23
|
+
}),
|
|
24
|
+
)
|
|
25
|
+
.handle("projectCopy.remove", (ctx) =>
|
|
26
|
+
ProjectCopy.Service.use((copies) =>
|
|
27
|
+
badRequest(copies.remove({ ...ctx.payload, projectID: ctx.params.projectID })).pipe(
|
|
28
|
+
Effect.as(HttpApiSchema.NoContent.make()),
|
|
29
|
+
),
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
.handle("projectCopy.refresh", (ctx) =>
|
|
33
|
+
ProjectCopy.Service.use((copies) =>
|
|
34
|
+
badRequest(copies.refresh({ projectID: ctx.params.projectID })).pipe(
|
|
35
|
+
Effect.as(HttpApiSchema.NoContent.make()),
|
|
36
|
+
),
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
function badRequest<A, R>(effect: Effect.Effect<A, ProjectCopy.Error, R>) {
|
|
43
|
+
return effect.pipe(
|
|
44
|
+
Effect.mapError(
|
|
45
|
+
(error) =>
|
|
46
|
+
new ProjectCopyError({
|
|
47
|
+
name: "ProjectCopyError",
|
|
48
|
+
data: {
|
|
49
|
+
message: message(error),
|
|
50
|
+
forceRequired: error instanceof Git.WorktreeError ? error.forceRequired : undefined,
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function message(error: ProjectCopy.Error) {
|
|
58
|
+
if (error instanceof ProjectCopy.SourceDirectoryNotFoundError)
|
|
59
|
+
return `Project copy source not found: ${error.directory}`
|
|
60
|
+
if (error instanceof ProjectCopy.DestinationExistsError)
|
|
61
|
+
return `Project copy destination already exists: ${error.directory}`
|
|
62
|
+
if (error instanceof ProjectCopy.DirectoryUnavailableError)
|
|
63
|
+
return `Project copy directory unavailable: ${error.directory}`
|
|
64
|
+
if (error instanceof ProjectCopy.InvalidDirectoryError) return `Invalid project copy directory: ${error.directory}`
|
|
65
|
+
if (error instanceof ProjectCopy.StrategyUnavailableError)
|
|
66
|
+
return `Project copy strategy unavailable: ${error.strategy}`
|
|
67
|
+
return error.message
|
|
68
|
+
}
|