t3code-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/bin.js +17842 -0
- package/dist/index.js +2 -0
- package/dist/runtime-DU_hs0MM.js +61631 -0
- package/package.json +59 -0
- package/src/application/error.ts +4 -0
- package/src/application/layer.ts +20 -0
- package/src/application/model-selection.ts +109 -0
- package/src/application/models.ts +26 -0
- package/src/application/project-commands.ts +28 -0
- package/src/application/projects.ts +50 -0
- package/src/application/service.ts +93 -0
- package/src/application/shell-sequence.ts +29 -0
- package/src/application/thread-commands.ts +89 -0
- package/src/application/thread-wait.ts +86 -0
- package/src/application/threads.ts +172 -0
- package/src/auth/error.ts +42 -0
- package/src/auth/layer.ts +114 -0
- package/src/auth/local.ts +241 -0
- package/src/auth/pairing.ts +54 -0
- package/src/auth/schema.ts +55 -0
- package/src/auth/service.ts +16 -0
- package/src/auth/transport.ts +132 -0
- package/src/auth/type.ts +31 -0
- package/src/bin.ts +47 -0
- package/src/cli/app.ts +18 -0
- package/src/cli/auth-format.ts +43 -0
- package/src/cli/auth.ts +99 -0
- package/src/cli/error.ts +16 -0
- package/src/cli/input/error.ts +11 -0
- package/src/cli/input/layer.ts +31 -0
- package/src/cli/input/service.ts +11 -0
- package/src/cli/message-input.ts +25 -0
- package/src/cli/model-format.ts +18 -0
- package/src/cli/model-options.ts +56 -0
- package/src/cli/models.ts +45 -0
- package/src/cli/output/error.ts +11 -0
- package/src/cli/output/layer.ts +53 -0
- package/src/cli/output/service.ts +15 -0
- package/src/cli/output-format.ts +41 -0
- package/src/cli/project-format.ts +11 -0
- package/src/cli/projects.ts +62 -0
- package/src/cli/thread-format.ts +74 -0
- package/src/cli/threads/archive.ts +28 -0
- package/src/cli/threads/list.ts +29 -0
- package/src/cli/threads/messages.ts +36 -0
- package/src/cli/threads/send.ts +91 -0
- package/src/cli/threads/start.ts +136 -0
- package/src/cli/threads/wait.ts +35 -0
- package/src/cli/threads.ts +22 -0
- package/src/cli/wait-events.ts +112 -0
- package/src/config/error.ts +21 -0
- package/src/config/layer.ts +103 -0
- package/src/config/service.ts +24 -0
- package/src/config/url.ts +55 -0
- package/src/domain/command-schema.ts +82 -0
- package/src/domain/error.ts +46 -0
- package/src/domain/helpers.ts +23 -0
- package/src/domain/model-config.ts +40 -0
- package/src/domain/schema.ts +162 -0
- package/src/domain/thread-lifecycle.ts +97 -0
- package/src/environment/layer.ts +12 -0
- package/src/environment/service.ts +13 -0
- package/src/index.ts +18 -0
- package/src/orchestration/layer.ts +193 -0
- package/src/orchestration/service.ts +39 -0
- package/src/protocol/schema.ts +105 -0
- package/src/rpc/error.ts +31 -0
- package/src/rpc/layer.ts +99 -0
- package/src/rpc/service.ts +16 -0
- package/src/runtime.ts +28 -0
- package/src/version/layer.ts +25 -0
- package/src/version/service.ts +8 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as Cause from "effect/Cause";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import { PlatformError } from "effect/PlatformError";
|
|
4
|
+
|
|
5
|
+
const ConfigErrorCauseSchema = Schema.Union([
|
|
6
|
+
Schema.instanceOf(PlatformError),
|
|
7
|
+
Schema.instanceOf(Schema.SchemaError),
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
export class UrlError extends Schema.TaggedErrorClass<UrlError>()("UrlError", {
|
|
11
|
+
message: Schema.String,
|
|
12
|
+
protocol: Schema.optionalKey(Schema.String),
|
|
13
|
+
cause: Schema.optionalKey(Schema.instanceOf(Cause.IllegalArgumentError)),
|
|
14
|
+
}) {}
|
|
15
|
+
|
|
16
|
+
export class ConfigError extends Schema.TaggedErrorClass<ConfigError>()("ConfigError", {
|
|
17
|
+
message: Schema.String,
|
|
18
|
+
cause: Schema.optionalKey(ConfigErrorCauseSchema),
|
|
19
|
+
}) {}
|
|
20
|
+
|
|
21
|
+
export type ConfigServiceError = ConfigError | UrlError;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as FileSystem from "effect/FileSystem";
|
|
3
|
+
import * as Filter from "effect/Filter";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
import * as Path from "effect/Path";
|
|
6
|
+
import * as Schema from "effect/Schema";
|
|
7
|
+
|
|
8
|
+
import { Environment, type EnvironmentShape } from "../environment/service.ts";
|
|
9
|
+
import { ConfigError } from "./error.ts";
|
|
10
|
+
import { T3Config, type StoredConfig } from "./service.ts";
|
|
11
|
+
import { normalizeHttpBaseUrl } from "./url.ts";
|
|
12
|
+
|
|
13
|
+
export const makeT3Config = Effect.fn("makeT3Config")(function* () {
|
|
14
|
+
const fs = yield* FileSystem.FileSystem;
|
|
15
|
+
const path = yield* Path.Path;
|
|
16
|
+
const environment = yield* Environment;
|
|
17
|
+
const configFilePath = resolveConfigPath(path, environment);
|
|
18
|
+
const readStored = Effect.fn("T3ConfigLive.readStored")(function* () {
|
|
19
|
+
const raw = yield* fs.readFileString(configFilePath).pipe(
|
|
20
|
+
Effect.catchFilter(Filter.reason("PlatformError", "NotFound"), () =>
|
|
21
|
+
Effect.succeed(undefined),
|
|
22
|
+
),
|
|
23
|
+
Effect.mapError(
|
|
24
|
+
(error) => new ConfigError({ message: "failed to read config", cause: error }),
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
if (raw === undefined) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
return yield* parseStoredConfig(raw);
|
|
31
|
+
});
|
|
32
|
+
const writeStored = Effect.fn("T3ConfigLive.writeStored")(function* (config: StoredConfig) {
|
|
33
|
+
yield* fs
|
|
34
|
+
.makeDirectory(path.dirname(configFilePath), { recursive: true, mode: 0o700 })
|
|
35
|
+
.pipe(
|
|
36
|
+
Effect.mapError(
|
|
37
|
+
(error) => new ConfigError({ message: "failed to write config", cause: error }),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
yield* fs
|
|
41
|
+
.writeFileString(configFilePath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 })
|
|
42
|
+
.pipe(
|
|
43
|
+
Effect.mapError(
|
|
44
|
+
(error) => new ConfigError({ message: "failed to write config", cause: error }),
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
const resolve = Effect.fn("T3ConfigLive.resolve")(function* () {
|
|
49
|
+
const stored = yield* readStored();
|
|
50
|
+
const envUrl = environment.env["T3CODE_URL"];
|
|
51
|
+
const envToken = environment.env["T3CODE_TOKEN"];
|
|
52
|
+
const envUrlValue = envUrl?.trim();
|
|
53
|
+
const envTokenValue = envToken?.trim();
|
|
54
|
+
const url = envUrlValue !== undefined && envUrlValue.length > 0 ? envUrlValue : stored.url;
|
|
55
|
+
const token =
|
|
56
|
+
envTokenValue !== undefined && envTokenValue.length > 0 ? envTokenValue : stored.token;
|
|
57
|
+
if (url === undefined || url.length === 0 || token === undefined || token.length === 0) {
|
|
58
|
+
return yield* Effect.fail(
|
|
59
|
+
new ConfigError({ message: "not authenticated. run: t3cli auth pair <pairing-url>" }),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const source: "env" | "config" =
|
|
63
|
+
(envUrlValue !== undefined && envUrlValue.length > 0) ||
|
|
64
|
+
(envTokenValue !== undefined && envTokenValue.length > 0)
|
|
65
|
+
? "env"
|
|
66
|
+
: "config";
|
|
67
|
+
const normalizedUrl = yield* normalizeHttpBaseUrl(url);
|
|
68
|
+
return {
|
|
69
|
+
url: normalizedUrl,
|
|
70
|
+
token,
|
|
71
|
+
source,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
readStored,
|
|
77
|
+
writeStored,
|
|
78
|
+
resolve,
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const T3ConfigLive = Layer.effect(T3Config, makeT3Config());
|
|
83
|
+
|
|
84
|
+
const StoredConfigSchema = Schema.Struct({
|
|
85
|
+
url: Schema.optionalKey(Schema.String),
|
|
86
|
+
token: Schema.optionalKey(Schema.String),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
function parseStoredConfig(raw: string) {
|
|
90
|
+
return Schema.decodeUnknownEffect(Schema.fromJsonString(StoredConfigSchema))(raw).pipe(
|
|
91
|
+
Effect.mapError((error) => new ConfigError({ message: "failed to read config", cause: error })),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolveConfigPath(path: Path.Path, environment: EnvironmentShape) {
|
|
96
|
+
const xdgConfigHome = environment.env["XDG_CONFIG_HOME"];
|
|
97
|
+
const xdgConfigHomeValue = xdgConfigHome?.trim();
|
|
98
|
+
const root =
|
|
99
|
+
xdgConfigHomeValue !== undefined && xdgConfigHomeValue.length > 0
|
|
100
|
+
? xdgConfigHomeValue
|
|
101
|
+
: path.join(environment.homeDir, ".config");
|
|
102
|
+
return path.join(root, "t3cli", "config.json");
|
|
103
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import type * as Effect from "effect/Effect";
|
|
3
|
+
|
|
4
|
+
import type { ConfigError, ConfigServiceError } from "./error.ts";
|
|
5
|
+
|
|
6
|
+
export type StoredConfig = {
|
|
7
|
+
readonly url?: string;
|
|
8
|
+
readonly token?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ResolvedConfig = {
|
|
12
|
+
readonly url: string;
|
|
13
|
+
readonly token: string;
|
|
14
|
+
readonly source: "env" | "config";
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export class T3Config extends Context.Service<
|
|
18
|
+
T3Config,
|
|
19
|
+
{
|
|
20
|
+
readonly readStored: () => Effect.Effect<StoredConfig, ConfigError>;
|
|
21
|
+
readonly writeStored: (config: StoredConfig) => Effect.Effect<void, ConfigError>;
|
|
22
|
+
readonly resolve: () => Effect.Effect<ResolvedConfig, ConfigServiceError>;
|
|
23
|
+
}
|
|
24
|
+
>()("t3cli/T3Config") {}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { Url } from "effect/unstable/http";
|
|
3
|
+
|
|
4
|
+
import { UrlError } from "./error.ts";
|
|
5
|
+
|
|
6
|
+
export function normalizeHttpBaseUrl(value: string) {
|
|
7
|
+
return parseUrl(value).pipe(Effect.flatMap((url) => Effect.succeed(normalizeBaseUrl(url))));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function toWebSocketBaseUrl(httpBaseUrl: string) {
|
|
11
|
+
return parseUrl(httpBaseUrl).pipe(
|
|
12
|
+
Effect.flatMap((url) => {
|
|
13
|
+
if (url.protocol === "http:") {
|
|
14
|
+
return Effect.succeed(makeWebSocketUrl(url, "ws:"));
|
|
15
|
+
}
|
|
16
|
+
if (url.protocol === "https:") {
|
|
17
|
+
return Effect.succeed(makeWebSocketUrl(url, "wss:"));
|
|
18
|
+
}
|
|
19
|
+
return Effect.fail(
|
|
20
|
+
new UrlError({
|
|
21
|
+
message: `unsupported server url protocol: ${url.protocol}`,
|
|
22
|
+
protocol: url.protocol,
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseUrl(value: string): Effect.Effect<URL, UrlError> {
|
|
30
|
+
return Effect.fromResult(Url.fromString(value)).pipe(
|
|
31
|
+
Effect.catchTags({
|
|
32
|
+
IllegalArgumentError: (error) =>
|
|
33
|
+
Effect.fail(new UrlError({ message: "invalid url", cause: error })),
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeBaseUrl(url: URL) {
|
|
39
|
+
return Url.mutate(url, (current) => {
|
|
40
|
+
current.hash = "";
|
|
41
|
+
current.search = "";
|
|
42
|
+
current.pathname = "";
|
|
43
|
+
})
|
|
44
|
+
.toString()
|
|
45
|
+
.replace(/\/$/, "");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function makeWebSocketUrl(url: URL, protocol: "ws:" | "wss:") {
|
|
49
|
+
return Url.mutate(url, (current) => {
|
|
50
|
+
current.protocol = protocol;
|
|
51
|
+
current.pathname = "/ws";
|
|
52
|
+
current.search = "";
|
|
53
|
+
current.hash = "";
|
|
54
|
+
}).toString();
|
|
55
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
|
|
3
|
+
import { ModelSelectionSchema } from "./schema.ts";
|
|
4
|
+
|
|
5
|
+
export const DispatchResultSchema = Schema.Struct({
|
|
6
|
+
sequence: Schema.Number,
|
|
7
|
+
});
|
|
8
|
+
export type DispatchResult = typeof DispatchResultSchema.Type;
|
|
9
|
+
|
|
10
|
+
export const ProjectCreateCommandSchema = Schema.Struct({
|
|
11
|
+
type: Schema.Literal("project.create"),
|
|
12
|
+
commandId: Schema.String,
|
|
13
|
+
projectId: Schema.String,
|
|
14
|
+
title: Schema.String,
|
|
15
|
+
workspaceRoot: Schema.String,
|
|
16
|
+
createdAt: Schema.String,
|
|
17
|
+
});
|
|
18
|
+
export type ProjectCreateCommand = typeof ProjectCreateCommandSchema.Type;
|
|
19
|
+
|
|
20
|
+
export const ThreadCreateCommandSchema = Schema.Struct({
|
|
21
|
+
type: Schema.Literal("thread.create"),
|
|
22
|
+
commandId: Schema.String,
|
|
23
|
+
threadId: Schema.String,
|
|
24
|
+
projectId: Schema.String,
|
|
25
|
+
title: Schema.String,
|
|
26
|
+
modelSelection: ModelSelectionSchema,
|
|
27
|
+
runtimeMode: Schema.Literal("full-access"),
|
|
28
|
+
interactionMode: Schema.Literal("default"),
|
|
29
|
+
branch: Schema.Null,
|
|
30
|
+
worktreePath: Schema.NullOr(Schema.String),
|
|
31
|
+
createdAt: Schema.String,
|
|
32
|
+
});
|
|
33
|
+
export type ThreadCreateCommand = typeof ThreadCreateCommandSchema.Type;
|
|
34
|
+
|
|
35
|
+
export const ThreadTurnStartCommandSchema = Schema.Struct({
|
|
36
|
+
type: Schema.Literal("thread.turn.start"),
|
|
37
|
+
commandId: Schema.String,
|
|
38
|
+
threadId: Schema.String,
|
|
39
|
+
message: Schema.Struct({
|
|
40
|
+
messageId: Schema.String,
|
|
41
|
+
role: Schema.Literal("user"),
|
|
42
|
+
text: Schema.String,
|
|
43
|
+
attachments: Schema.Array(Schema.Never),
|
|
44
|
+
}),
|
|
45
|
+
modelSelection: Schema.optionalKey(ModelSelectionSchema),
|
|
46
|
+
titleSeed: Schema.optionalKey(Schema.String),
|
|
47
|
+
runtimeMode: Schema.Literal("full-access"),
|
|
48
|
+
interactionMode: Schema.Literal("default"),
|
|
49
|
+
bootstrap: Schema.optionalKey(
|
|
50
|
+
Schema.Struct({
|
|
51
|
+
createThread: Schema.optionalKey(
|
|
52
|
+
Schema.Struct({
|
|
53
|
+
projectId: Schema.String,
|
|
54
|
+
title: Schema.String,
|
|
55
|
+
modelSelection: ModelSelectionSchema,
|
|
56
|
+
runtimeMode: Schema.Literal("full-access"),
|
|
57
|
+
interactionMode: Schema.Literal("default"),
|
|
58
|
+
branch: Schema.Null,
|
|
59
|
+
worktreePath: Schema.NullOr(Schema.String),
|
|
60
|
+
createdAt: Schema.String,
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
}),
|
|
64
|
+
),
|
|
65
|
+
createdAt: Schema.String,
|
|
66
|
+
});
|
|
67
|
+
export type ThreadTurnStartCommand = typeof ThreadTurnStartCommandSchema.Type;
|
|
68
|
+
|
|
69
|
+
export const ThreadArchiveCommandSchema = Schema.Struct({
|
|
70
|
+
type: Schema.Literal("thread.archive"),
|
|
71
|
+
commandId: Schema.String,
|
|
72
|
+
threadId: Schema.String,
|
|
73
|
+
});
|
|
74
|
+
export type ThreadArchiveCommand = typeof ThreadArchiveCommandSchema.Type;
|
|
75
|
+
|
|
76
|
+
export const ClientOrchestrationCommandSchema = Schema.Union([
|
|
77
|
+
ProjectCreateCommandSchema,
|
|
78
|
+
ThreadArchiveCommandSchema,
|
|
79
|
+
ThreadCreateCommandSchema,
|
|
80
|
+
ThreadTurnStartCommandSchema,
|
|
81
|
+
]);
|
|
82
|
+
export type ClientOrchestrationCommand = typeof ClientOrchestrationCommandSchema.Type;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
|
|
3
|
+
export class ProjectLookupError extends Schema.TaggedErrorClass<ProjectLookupError>()(
|
|
4
|
+
"ProjectLookupError",
|
|
5
|
+
{
|
|
6
|
+
message: Schema.String,
|
|
7
|
+
ref: Schema.String,
|
|
8
|
+
},
|
|
9
|
+
) {}
|
|
10
|
+
|
|
11
|
+
export class ModelSelectionError extends Schema.TaggedErrorClass<ModelSelectionError>()(
|
|
12
|
+
"ModelSelectionError",
|
|
13
|
+
{
|
|
14
|
+
message: Schema.String,
|
|
15
|
+
},
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
export class ThreadEventError extends Schema.TaggedErrorClass<ThreadEventError>()(
|
|
19
|
+
"ThreadEventError",
|
|
20
|
+
{
|
|
21
|
+
message: Schema.String,
|
|
22
|
+
},
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
export class ThreadSessionError extends Schema.TaggedErrorClass<ThreadSessionError>()(
|
|
26
|
+
"ThreadSessionError",
|
|
27
|
+
{
|
|
28
|
+
message: Schema.String,
|
|
29
|
+
threadId: Schema.String,
|
|
30
|
+
},
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
export class ProjectCreateVisibilityError extends Schema.TaggedErrorClass<ProjectCreateVisibilityError>()(
|
|
34
|
+
"ProjectCreateVisibilityError",
|
|
35
|
+
{
|
|
36
|
+
message: Schema.String,
|
|
37
|
+
projectId: Schema.String,
|
|
38
|
+
},
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
export type DomainError =
|
|
42
|
+
| ProjectLookupError
|
|
43
|
+
| ModelSelectionError
|
|
44
|
+
| ThreadEventError
|
|
45
|
+
| ThreadSessionError
|
|
46
|
+
| ProjectCreateVisibilityError;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type * as Path from "effect/Path";
|
|
2
|
+
|
|
3
|
+
import { ProjectLookupError } from "./error.ts";
|
|
4
|
+
import type { ProjectShell, ShellSnapshot } from "./schema.ts";
|
|
5
|
+
|
|
6
|
+
export function resolveProject(snapshot: ShellSnapshot, ref: string, path: Path.Path, cwd: string) {
|
|
7
|
+
const byId = findProjectById(snapshot, ref);
|
|
8
|
+
if (byId !== null) {
|
|
9
|
+
return byId;
|
|
10
|
+
}
|
|
11
|
+
const absolute = path.resolve(cwd, ref);
|
|
12
|
+
const byPath = snapshot.projects.find(
|
|
13
|
+
(project) => path.resolve(cwd, project.workspaceRoot) === absolute,
|
|
14
|
+
);
|
|
15
|
+
if (byPath !== undefined) {
|
|
16
|
+
return byPath;
|
|
17
|
+
}
|
|
18
|
+
throw new ProjectLookupError({ message: `project not found: ${ref}`, ref });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function findProjectById(snapshot: ShellSnapshot, projectId: string): ProjectShell | null {
|
|
22
|
+
return snapshot.projects.find((project) => project.id === projectId) ?? null;
|
|
23
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ServerProvider } from "./schema.ts";
|
|
2
|
+
|
|
3
|
+
export function isSelectableProvider(provider: ServerProvider) {
|
|
4
|
+
return provider.status === "ready" && provider.models.length > 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function firstSelectableProvider(providers: ReadonlyArray<ServerProvider>) {
|
|
8
|
+
return providers.find(isSelectableProvider);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function firstSelectableModel(provider: ServerProvider) {
|
|
12
|
+
return provider.models[0];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function findSelectableProvider(
|
|
16
|
+
providers: ReadonlyArray<ServerProvider>,
|
|
17
|
+
instanceId: string,
|
|
18
|
+
) {
|
|
19
|
+
return providers.find(
|
|
20
|
+
(provider) => provider.instanceId === instanceId && isSelectableProvider(provider),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function filterProvidersForModelListing(input: {
|
|
25
|
+
readonly providers: ReadonlyArray<ServerProvider>;
|
|
26
|
+
readonly all: boolean;
|
|
27
|
+
readonly provider?: string;
|
|
28
|
+
}) {
|
|
29
|
+
return input.providers
|
|
30
|
+
.filter((provider) => input.provider === undefined || provider.instanceId === input.provider)
|
|
31
|
+
.flatMap((provider) => {
|
|
32
|
+
if (input.all) {
|
|
33
|
+
return [provider];
|
|
34
|
+
}
|
|
35
|
+
if (!isSelectableProvider(provider)) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
return [provider];
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
|
|
3
|
+
export type Format = "human" | "json" | "ndjson";
|
|
4
|
+
|
|
5
|
+
export const FormatSchema = Schema.Literals(["human", "json", "ndjson"]);
|
|
6
|
+
|
|
7
|
+
export const ModelSelectionSchema = Schema.Struct({
|
|
8
|
+
instanceId: Schema.String,
|
|
9
|
+
model: Schema.String,
|
|
10
|
+
options: Schema.optionalKey(
|
|
11
|
+
Schema.Array(
|
|
12
|
+
Schema.Struct({
|
|
13
|
+
id: Schema.String,
|
|
14
|
+
value: Schema.Unknown,
|
|
15
|
+
}),
|
|
16
|
+
),
|
|
17
|
+
),
|
|
18
|
+
});
|
|
19
|
+
export type ModelSelection = typeof ModelSelectionSchema.Type;
|
|
20
|
+
|
|
21
|
+
export const ProjectShellSchema = Schema.Struct({
|
|
22
|
+
id: Schema.String,
|
|
23
|
+
title: Schema.String,
|
|
24
|
+
workspaceRoot: Schema.String,
|
|
25
|
+
defaultModelSelection: Schema.NullOr(ModelSelectionSchema),
|
|
26
|
+
createdAt: Schema.String,
|
|
27
|
+
updatedAt: Schema.String,
|
|
28
|
+
});
|
|
29
|
+
export type ProjectShell = typeof ProjectShellSchema.Type;
|
|
30
|
+
|
|
31
|
+
export const ThreadMessageSchema = Schema.Struct({
|
|
32
|
+
id: Schema.optionalKey(Schema.String),
|
|
33
|
+
messageId: Schema.optionalKey(Schema.String),
|
|
34
|
+
role: Schema.Literals(["user", "assistant", "system"]),
|
|
35
|
+
text: Schema.String,
|
|
36
|
+
streaming: Schema.optionalKey(Schema.Boolean),
|
|
37
|
+
turnId: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
38
|
+
createdAt: Schema.String,
|
|
39
|
+
updatedAt: Schema.String,
|
|
40
|
+
});
|
|
41
|
+
export type ThreadMessage = typeof ThreadMessageSchema.Type;
|
|
42
|
+
|
|
43
|
+
export const ThreadSessionSchema = Schema.Struct({
|
|
44
|
+
threadId: Schema.String,
|
|
45
|
+
status: Schema.Literals([
|
|
46
|
+
"idle",
|
|
47
|
+
"starting",
|
|
48
|
+
"running",
|
|
49
|
+
"ready",
|
|
50
|
+
"interrupted",
|
|
51
|
+
"stopped",
|
|
52
|
+
"error",
|
|
53
|
+
]),
|
|
54
|
+
lastError: Schema.NullOr(Schema.String),
|
|
55
|
+
updatedAt: Schema.String,
|
|
56
|
+
});
|
|
57
|
+
export type ThreadSession = typeof ThreadSessionSchema.Type;
|
|
58
|
+
|
|
59
|
+
export const ThreadShellSchema = Schema.Struct({
|
|
60
|
+
id: Schema.String,
|
|
61
|
+
projectId: Schema.String,
|
|
62
|
+
title: Schema.String,
|
|
63
|
+
modelSelection: ModelSelectionSchema,
|
|
64
|
+
runtimeMode: Schema.String,
|
|
65
|
+
interactionMode: Schema.String,
|
|
66
|
+
branch: Schema.NullOr(Schema.String),
|
|
67
|
+
worktreePath: Schema.NullOr(Schema.String),
|
|
68
|
+
latestTurn: Schema.NullOr(Schema.Struct({ state: Schema.String })),
|
|
69
|
+
createdAt: Schema.String,
|
|
70
|
+
updatedAt: Schema.String,
|
|
71
|
+
session: Schema.NullOr(ThreadSessionSchema),
|
|
72
|
+
latestUserMessageAt: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
73
|
+
});
|
|
74
|
+
export type ThreadShell = typeof ThreadShellSchema.Type;
|
|
75
|
+
|
|
76
|
+
export const ThreadDetailSchema = Schema.Struct({
|
|
77
|
+
id: Schema.String,
|
|
78
|
+
projectId: Schema.String,
|
|
79
|
+
title: Schema.String,
|
|
80
|
+
modelSelection: ModelSelectionSchema,
|
|
81
|
+
runtimeMode: Schema.String,
|
|
82
|
+
interactionMode: Schema.String,
|
|
83
|
+
branch: Schema.NullOr(Schema.String),
|
|
84
|
+
worktreePath: Schema.NullOr(Schema.String),
|
|
85
|
+
latestTurn: Schema.NullOr(Schema.Struct({ state: Schema.String })),
|
|
86
|
+
createdAt: Schema.String,
|
|
87
|
+
updatedAt: Schema.String,
|
|
88
|
+
session: Schema.NullOr(ThreadSessionSchema),
|
|
89
|
+
latestUserMessageAt: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
90
|
+
messages: Schema.Array(ThreadMessageSchema),
|
|
91
|
+
activities: Schema.optionalKey(Schema.Array(Schema.Unknown)),
|
|
92
|
+
});
|
|
93
|
+
export type ThreadDetail = typeof ThreadDetailSchema.Type;
|
|
94
|
+
|
|
95
|
+
export const ShellSnapshotSchema = Schema.Struct({
|
|
96
|
+
snapshotSequence: Schema.Number,
|
|
97
|
+
projects: Schema.Array(ProjectShellSchema),
|
|
98
|
+
threads: Schema.Array(ThreadShellSchema),
|
|
99
|
+
updatedAt: Schema.String,
|
|
100
|
+
});
|
|
101
|
+
export type ShellSnapshot = typeof ShellSnapshotSchema.Type;
|
|
102
|
+
|
|
103
|
+
export const ServerProviderSchema = Schema.Struct({
|
|
104
|
+
instanceId: Schema.String,
|
|
105
|
+
displayName: Schema.optionalKey(Schema.String),
|
|
106
|
+
enabled: Schema.Boolean,
|
|
107
|
+
installed: Schema.Boolean,
|
|
108
|
+
status: Schema.Literals(["ready", "warning", "error", "disabled"]),
|
|
109
|
+
auth: Schema.Struct({
|
|
110
|
+
status: Schema.Literals(["authenticated", "unauthenticated", "unknown"]),
|
|
111
|
+
}),
|
|
112
|
+
models: Schema.Array(
|
|
113
|
+
Schema.Struct({
|
|
114
|
+
slug: Schema.String,
|
|
115
|
+
name: Schema.String,
|
|
116
|
+
}),
|
|
117
|
+
),
|
|
118
|
+
});
|
|
119
|
+
export type ServerProvider = typeof ServerProviderSchema.Type;
|
|
120
|
+
export type ServerProviderModel = NonNullable<ServerProvider["models"]>[number];
|
|
121
|
+
|
|
122
|
+
export const ServerConfigSchema = Schema.Struct({
|
|
123
|
+
providers: Schema.optionalKey(Schema.Array(ServerProviderSchema)),
|
|
124
|
+
});
|
|
125
|
+
export type ServerConfig = typeof ServerConfigSchema.Type;
|
|
126
|
+
|
|
127
|
+
export const ThreadMessageSentEventSchema = Schema.Struct({
|
|
128
|
+
type: Schema.Literal("thread.message-sent"),
|
|
129
|
+
payload: Schema.Struct({
|
|
130
|
+
messageId: Schema.String,
|
|
131
|
+
role: Schema.Literals(["user", "assistant", "system"]),
|
|
132
|
+
text: Schema.String,
|
|
133
|
+
turnId: Schema.NullOr(Schema.String),
|
|
134
|
+
streaming: Schema.optionalKey(Schema.Boolean),
|
|
135
|
+
createdAt: Schema.String,
|
|
136
|
+
updatedAt: Schema.String,
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
export type ThreadMessageSentEvent = typeof ThreadMessageSentEventSchema.Type;
|
|
140
|
+
|
|
141
|
+
export const ThreadSessionSetEventSchema = Schema.Struct({
|
|
142
|
+
type: Schema.Literal("thread.session-set"),
|
|
143
|
+
payload: Schema.Struct({
|
|
144
|
+
session: Schema.NullOr(ThreadSessionSchema),
|
|
145
|
+
}),
|
|
146
|
+
});
|
|
147
|
+
export type ThreadSessionSetEvent = typeof ThreadSessionSetEventSchema.Type;
|
|
148
|
+
|
|
149
|
+
export const UnknownThreadEventSchema = Schema.Struct({
|
|
150
|
+
type: Schema.String,
|
|
151
|
+
payload: Schema.Record(Schema.String, Schema.Unknown),
|
|
152
|
+
});
|
|
153
|
+
export type UnknownThreadEvent = typeof UnknownThreadEventSchema.Type;
|
|
154
|
+
|
|
155
|
+
export const ThreadEventSchema = Schema.Union([
|
|
156
|
+
ThreadMessageSentEventSchema,
|
|
157
|
+
ThreadSessionSetEventSchema,
|
|
158
|
+
UnknownThreadEventSchema,
|
|
159
|
+
]);
|
|
160
|
+
export type ThreadEvent = typeof ThreadEventSchema.Type;
|
|
161
|
+
|
|
162
|
+
export const decodeModelSelection = Schema.decodeUnknownSync(ModelSelectionSchema);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type ThreadDetail,
|
|
5
|
+
type ThreadEvent,
|
|
6
|
+
type ThreadMessage,
|
|
7
|
+
type ThreadShell,
|
|
8
|
+
ThreadMessageSchema,
|
|
9
|
+
ThreadMessageSentEventSchema,
|
|
10
|
+
ThreadSessionSetEventSchema,
|
|
11
|
+
} from "./schema.ts";
|
|
12
|
+
|
|
13
|
+
const isThreadMessageSentEvent = Schema.is(ThreadMessageSentEventSchema);
|
|
14
|
+
const isThreadSessionSetEvent = Schema.is(ThreadSessionSetEventSchema);
|
|
15
|
+
|
|
16
|
+
export function isThreadActive(thread: ThreadShell | ThreadDetail) {
|
|
17
|
+
return (
|
|
18
|
+
thread.session?.status === "starting" ||
|
|
19
|
+
thread.session?.status === "running" ||
|
|
20
|
+
thread.latestTurn?.state === "running" ||
|
|
21
|
+
isPendingStart(thread)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function threadStatus(thread: ThreadShell | ThreadDetail) {
|
|
26
|
+
if (isPendingStart(thread)) {
|
|
27
|
+
return "pending";
|
|
28
|
+
}
|
|
29
|
+
return thread.session?.status ?? thread.latestTurn?.state ?? "unknown";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function latestAssistantMessage(thread: ThreadDetail) {
|
|
33
|
+
return thread.messages.toReversed().find((message) => message.role === "assistant");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isThreadCompleteEnough(thread: ThreadDetail) {
|
|
37
|
+
if (thread.session?.status === "error" || thread.session?.status === "interrupted") {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
const lastUserIndex = thread.messages.findLastIndex((message) => message.role === "user");
|
|
41
|
+
if (lastUserIndex === -1) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
return thread.messages.slice(lastUserIndex + 1).some((message) => message.role === "assistant");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function applyThreadEvent(
|
|
48
|
+
current: ThreadDetail,
|
|
49
|
+
event: ThreadEvent,
|
|
50
|
+
messages: Map<string, ThreadMessage>,
|
|
51
|
+
) {
|
|
52
|
+
const message = messageFromEvent(event, messages);
|
|
53
|
+
if (message !== null) {
|
|
54
|
+
messages.set(messageKey(message), message);
|
|
55
|
+
return { ...current, messages: [...messages.values()] };
|
|
56
|
+
}
|
|
57
|
+
if (isThreadSessionSetEvent(event)) {
|
|
58
|
+
return {
|
|
59
|
+
...current,
|
|
60
|
+
session: event.payload.session,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return current;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function messageFromEvent(
|
|
67
|
+
event: ThreadEvent,
|
|
68
|
+
existingMessages: Map<string, ThreadMessage> = new Map(),
|
|
69
|
+
): ThreadMessage | null {
|
|
70
|
+
if (!isThreadMessageSentEvent(event)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const payload = event.payload;
|
|
74
|
+
const id = payload.messageId;
|
|
75
|
+
const previous = existingMessages.get(id);
|
|
76
|
+
const text = payload.text;
|
|
77
|
+
return Schema.decodeUnknownSync(ThreadMessageSchema)({
|
|
78
|
+
id,
|
|
79
|
+
role: payload.role,
|
|
80
|
+
text: text.length > 0 || previous === undefined ? text : previous.text,
|
|
81
|
+
turnId: payload.turnId,
|
|
82
|
+
...(payload.streaming !== undefined ? { streaming: payload.streaming } : {}),
|
|
83
|
+
createdAt: payload.createdAt,
|
|
84
|
+
updatedAt: payload.updatedAt,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function messageKey(message: ThreadMessage) {
|
|
89
|
+
return message.id ?? message.messageId ?? `${message.role}:${message.createdAt}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isPendingStart(thread: ThreadShell | ThreadDetail) {
|
|
93
|
+
if (thread.session !== null || thread.latestTurn !== null || !("messages" in thread)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return thread.messages.at(-1)?.role === "user";
|
|
97
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import * as Layer from "effect/Layer";
|
|
3
|
+
|
|
4
|
+
import { Environment } from "./service.ts";
|
|
5
|
+
|
|
6
|
+
export const NodeEnvironmentLive = Layer.succeed(Environment)({
|
|
7
|
+
cwd: process.cwd(),
|
|
8
|
+
homeDir: homedir(),
|
|
9
|
+
env: process.env,
|
|
10
|
+
stdoutIsTTY: process.stdout.isTTY ?? false,
|
|
11
|
+
stderrIsTTY: process.stderr.isTTY ?? false,
|
|
12
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
|
|
3
|
+
export type EnvironmentShape = {
|
|
4
|
+
readonly cwd: string;
|
|
5
|
+
readonly homeDir: string;
|
|
6
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
7
|
+
readonly stdoutIsTTY: boolean;
|
|
8
|
+
readonly stderrIsTTY: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export class Environment extends Context.Service<Environment, EnvironmentShape>()(
|
|
12
|
+
"t3cli/Environment",
|
|
13
|
+
) {}
|