t3code-cli 0.1.2 → 0.2.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/dist/bin.js +2 -2
- package/dist/index.js +1 -1
- package/dist/{runtime-DU_hs0MM.js → runtime-CMPZpQaG.js} +9930 -2845
- package/package.json +4 -2
- package/src/application/model-selection.ts +13 -15
- package/src/application/models.ts +1 -1
- package/src/application/project-commands.ts +6 -4
- package/src/application/service.ts +26 -22
- package/src/application/shell-sequence.ts +1 -1
- package/src/application/thread-commands.ts +32 -22
- package/src/application/thread-wait.ts +4 -4
- package/src/auth/error.ts +5 -1
- package/src/auth/pairing.ts +6 -2
- package/src/auth/transport.ts +21 -10
- package/src/cli/model-format.ts +1 -1
- package/src/cli/project-format.ts +3 -3
- package/src/cli/thread-format.ts +6 -6
- package/src/config/url.ts +46 -2
- package/src/domain/helpers.ts +11 -3
- package/src/domain/model-config.ts +1 -1
- package/src/domain/thread-lifecycle.ts +24 -30
- package/src/index.ts +6 -6
- package/src/orchestration/layer.ts +41 -48
- package/src/orchestration/service.ts +20 -11
- package/src/rpc/error.ts +12 -0
- package/src/rpc/layer.ts +4 -4
- package/src/rpc/service.ts +2 -3
- package/src/rpc/ws-group.ts +43 -0
- package/src/domain/command-schema.ts +0 -82
- package/src/domain/schema.ts +0 -162
- package/src/protocol/schema.ts +0 -105
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "t3code-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CLI for t3code",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -32,13 +32,15 @@
|
|
|
32
32
|
"types": "./src/index.ts"
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
|
+
"dependencies": {},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@changesets/cli": "^2.31.0",
|
|
37
38
|
"@effect/platform-node": "4.0.0-beta.74",
|
|
38
39
|
"@types/node": "^25.9.1",
|
|
39
40
|
"effect": "4.0.0-beta.74",
|
|
40
41
|
"typescript": "^6.0.3",
|
|
41
|
-
"vite-plus": "^0.1.24"
|
|
42
|
+
"vite-plus": "^0.1.24",
|
|
43
|
+
"@t3tools/contracts": "0.0.24"
|
|
42
44
|
},
|
|
43
45
|
"engines": {
|
|
44
46
|
"node": "^24.0.0"
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
|
+
import { ModelSelection, ProviderInstanceId } from "@t3tools/contracts";
|
|
3
|
+
import type { OrchestrationProjectShell } from "@t3tools/contracts";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
2
5
|
|
|
3
6
|
import { ModelSelectionError } from "../domain/error.ts";
|
|
4
7
|
import {
|
|
@@ -6,23 +9,18 @@ import {
|
|
|
6
9
|
firstSelectableModel,
|
|
7
10
|
firstSelectableProvider,
|
|
8
11
|
} from "../domain/model-config.ts";
|
|
9
|
-
import {
|
|
10
|
-
decodeModelSelection,
|
|
11
|
-
type ModelSelection,
|
|
12
|
-
type ProjectShell,
|
|
13
|
-
type ServerConfig,
|
|
14
|
-
} from "../domain/schema.ts";
|
|
12
|
+
import type { ServerConfigForCli } from "../orchestration/service.ts";
|
|
15
13
|
import type { StartThreadInput } from "./service.ts";
|
|
16
14
|
|
|
17
15
|
export function resolveModelSelection(input: {
|
|
18
16
|
readonly start: StartThreadInput;
|
|
19
|
-
readonly project:
|
|
20
|
-
readonly serverConfig:
|
|
17
|
+
readonly project: OrchestrationProjectShell;
|
|
18
|
+
readonly serverConfig: ServerConfigForCli;
|
|
21
19
|
}) {
|
|
22
20
|
return Effect.gen(function* () {
|
|
23
21
|
if (input.start.provider !== undefined && input.start.model !== undefined) {
|
|
24
22
|
return withModelOptions(input.start, {
|
|
25
|
-
instanceId: input.start.provider,
|
|
23
|
+
instanceId: ProviderInstanceId.make(input.start.provider),
|
|
26
24
|
model: input.start.model,
|
|
27
25
|
});
|
|
28
26
|
}
|
|
@@ -33,7 +31,7 @@ export function resolveModelSelection(input: {
|
|
|
33
31
|
return yield* failNoAvailableModel();
|
|
34
32
|
}
|
|
35
33
|
return withModelOptions(input.start, {
|
|
36
|
-
instanceId: input.start.provider,
|
|
34
|
+
instanceId: ProviderInstanceId.make(input.start.provider),
|
|
37
35
|
model: model.slug,
|
|
38
36
|
});
|
|
39
37
|
}
|
|
@@ -70,7 +68,7 @@ export function mergeModelOptions(
|
|
|
70
68
|
for (const option of options) {
|
|
71
69
|
optionsById.set(option.id, option);
|
|
72
70
|
}
|
|
73
|
-
return
|
|
71
|
+
return Schema.decodeUnknownSync(ModelSelection)({
|
|
74
72
|
...selection,
|
|
75
73
|
options: [...optionsById.values()],
|
|
76
74
|
});
|
|
@@ -83,8 +81,8 @@ function withModelOptions(input: StartThreadInput, selection: ModelSelection): M
|
|
|
83
81
|
return mergeModelOptions(selection, input.options);
|
|
84
82
|
}
|
|
85
83
|
|
|
86
|
-
function firstAvailableModel(serverConfig:
|
|
87
|
-
const providers = serverConfig.providers
|
|
84
|
+
function firstAvailableModel(serverConfig: ServerConfigForCli) {
|
|
85
|
+
const providers = serverConfig.providers;
|
|
88
86
|
const provider = firstSelectableProvider(providers);
|
|
89
87
|
if (provider === undefined) {
|
|
90
88
|
return failNoAvailableModel();
|
|
@@ -92,8 +90,8 @@ function firstAvailableModel(serverConfig: ServerConfig) {
|
|
|
92
90
|
return Effect.succeed(provider);
|
|
93
91
|
}
|
|
94
92
|
|
|
95
|
-
function findProvider(serverConfig:
|
|
96
|
-
const provider = findSelectableProvider(serverConfig.providers
|
|
93
|
+
function findProvider(serverConfig: ServerConfigForCli, instanceId: string) {
|
|
94
|
+
const provider = findSelectableProvider(serverConfig.providers, instanceId);
|
|
97
95
|
if (provider === undefined) {
|
|
98
96
|
return failNoAvailableModel();
|
|
99
97
|
}
|
|
@@ -12,7 +12,7 @@ export const makeModelsApplication = Effect.fn("makeModelsApplication")(function
|
|
|
12
12
|
}) {
|
|
13
13
|
const config = yield* orchestration.getServerConfig();
|
|
14
14
|
return filterProvidersForModelListing({
|
|
15
|
-
providers: config.providers
|
|
15
|
+
providers: config.providers,
|
|
16
16
|
all: input.all === true,
|
|
17
17
|
...(input.provider !== undefined && input.provider.length > 0
|
|
18
18
|
? { provider: input.provider }
|
|
@@ -2,9 +2,9 @@ import * as Crypto from "effect/Crypto";
|
|
|
2
2
|
import * as DateTime from "effect/DateTime";
|
|
3
3
|
import * as Effect from "effect/Effect";
|
|
4
4
|
import * as Path from "effect/Path";
|
|
5
|
+
import { CommandId, ProjectId, type ClientOrchestrationCommand } from "@t3tools/contracts";
|
|
5
6
|
|
|
6
7
|
import { Environment } from "../environment/service.ts";
|
|
7
|
-
import type { ProjectCreateCommand } from "../domain/command-schema.ts";
|
|
8
8
|
|
|
9
9
|
export const makeProjectCreateCommand = Effect.fn("makeProjectCreateCommand")(function* (input: {
|
|
10
10
|
readonly path: string;
|
|
@@ -14,15 +14,17 @@ export const makeProjectCreateCommand = Effect.fn("makeProjectCreateCommand")(fu
|
|
|
14
14
|
const crypto = yield* Crypto.Crypto;
|
|
15
15
|
const environment = yield* Environment;
|
|
16
16
|
const workspaceRoot = path.resolve(environment.cwd, input.path);
|
|
17
|
-
const projectId = yield* crypto.randomUUIDv4.pipe(Effect.orDie);
|
|
17
|
+
const projectId = ProjectId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
|
|
18
18
|
const title = input.title?.trim();
|
|
19
19
|
const createdAt = DateTime.formatIso(yield* DateTime.now);
|
|
20
20
|
return {
|
|
21
21
|
type: "project.create",
|
|
22
|
-
commandId:
|
|
22
|
+
commandId: CommandId.make(
|
|
23
|
+
`t3cli:project-create:${yield* crypto.randomUUIDv4.pipe(Effect.orDie)}`,
|
|
24
|
+
),
|
|
23
25
|
projectId,
|
|
24
26
|
title: title !== undefined && title.length > 0 ? title : path.basename(workspaceRoot),
|
|
25
27
|
workspaceRoot,
|
|
26
28
|
createdAt,
|
|
27
|
-
} satisfies
|
|
29
|
+
} satisfies Extract<ClientOrchestrationCommand, { readonly type: "project.create" }>;
|
|
28
30
|
});
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import * as Context from "effect/Context";
|
|
2
2
|
import type * as Effect from "effect/Effect";
|
|
3
3
|
import type * as Stream from "effect/Stream";
|
|
4
|
-
|
|
5
|
-
import type { ApplicationError } from "./error.ts";
|
|
6
|
-
import type { DispatchResult } from "../domain/command-schema.ts";
|
|
7
4
|
import type {
|
|
8
|
-
|
|
9
|
-
ServerProvider,
|
|
10
|
-
ShellSnapshot,
|
|
11
|
-
ThreadDetail,
|
|
12
|
-
ThreadMessage,
|
|
13
|
-
ThreadShell,
|
|
5
|
+
DispatchResult,
|
|
14
6
|
ModelSelection,
|
|
15
|
-
|
|
7
|
+
OrchestrationMessage,
|
|
8
|
+
OrchestrationProjectShell,
|
|
9
|
+
OrchestrationShellSnapshot,
|
|
10
|
+
OrchestrationThread,
|
|
11
|
+
OrchestrationThreadShell,
|
|
12
|
+
ServerProvider,
|
|
13
|
+
} from "@t3tools/contracts";
|
|
14
|
+
|
|
15
|
+
import type { ApplicationError } from "./error.ts";
|
|
16
16
|
|
|
17
17
|
export type StartThreadInput = {
|
|
18
18
|
readonly projectRef: string;
|
|
@@ -35,15 +35,15 @@ export type StartThreadPolicy = {
|
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
export type WaitEvent =
|
|
38
|
-
| { readonly type: "thread"; readonly thread:
|
|
39
|
-
| { readonly type: "message"; readonly message:
|
|
38
|
+
| { readonly type: "thread"; readonly thread: OrchestrationThread }
|
|
39
|
+
| { readonly type: "message"; readonly message: OrchestrationMessage }
|
|
40
40
|
| { readonly type: "status"; readonly status: string; readonly threadId: string }
|
|
41
|
-
| { readonly type: "done"; readonly thread:
|
|
41
|
+
| { readonly type: "done"; readonly thread: OrchestrationThread };
|
|
42
42
|
|
|
43
43
|
export class T3Application extends Context.Service<
|
|
44
44
|
T3Application,
|
|
45
45
|
{
|
|
46
|
-
readonly loadShell: () => Effect.Effect<
|
|
46
|
+
readonly loadShell: () => Effect.Effect<OrchestrationShellSnapshot, ApplicationError>;
|
|
47
47
|
readonly listModels: (input: {
|
|
48
48
|
readonly all?: boolean;
|
|
49
49
|
readonly provider?: string;
|
|
@@ -52,17 +52,19 @@ export class T3Application extends Context.Service<
|
|
|
52
52
|
readonly path: string;
|
|
53
53
|
readonly title?: string;
|
|
54
54
|
}) => Effect.Effect<
|
|
55
|
-
{ readonly dispatch: DispatchResult; readonly project:
|
|
55
|
+
{ readonly dispatch: DispatchResult; readonly project: OrchestrationProjectShell },
|
|
56
56
|
ApplicationError
|
|
57
57
|
>;
|
|
58
58
|
readonly listThreads: (projectRef: string) => Effect.Effect<
|
|
59
59
|
{
|
|
60
|
-
readonly project:
|
|
61
|
-
readonly threads: ReadonlyArray<
|
|
60
|
+
readonly project: OrchestrationProjectShell;
|
|
61
|
+
readonly threads: ReadonlyArray<OrchestrationThreadShell>;
|
|
62
62
|
},
|
|
63
63
|
ApplicationError
|
|
64
64
|
>;
|
|
65
|
-
readonly getThreadMessages: (
|
|
65
|
+
readonly getThreadMessages: (
|
|
66
|
+
threadId: string,
|
|
67
|
+
) => Effect.Effect<OrchestrationThread, ApplicationError>;
|
|
66
68
|
readonly archiveThread: (threadId: string) => Effect.Effect<DispatchResult, ApplicationError>;
|
|
67
69
|
readonly startThread: (
|
|
68
70
|
input: StartThreadInput,
|
|
@@ -70,9 +72,9 @@ export class T3Application extends Context.Service<
|
|
|
70
72
|
) => Effect.Effect<
|
|
71
73
|
{
|
|
72
74
|
readonly dispatch: DispatchResult;
|
|
73
|
-
readonly project:
|
|
75
|
+
readonly project: OrchestrationProjectShell;
|
|
74
76
|
readonly threadId: string;
|
|
75
|
-
readonly thread?:
|
|
77
|
+
readonly thread?: OrchestrationThread;
|
|
76
78
|
},
|
|
77
79
|
ApplicationError
|
|
78
80
|
>;
|
|
@@ -83,11 +85,13 @@ export class T3Application extends Context.Service<
|
|
|
83
85
|
{
|
|
84
86
|
readonly dispatch: DispatchResult;
|
|
85
87
|
readonly threadId: string;
|
|
86
|
-
readonly thread?:
|
|
88
|
+
readonly thread?: OrchestrationThread;
|
|
87
89
|
},
|
|
88
90
|
ApplicationError
|
|
89
91
|
>;
|
|
90
92
|
readonly watchThread: (threadId: string) => Stream.Stream<WaitEvent, ApplicationError>;
|
|
91
|
-
readonly waitForThread: (
|
|
93
|
+
readonly waitForThread: (
|
|
94
|
+
threadId: string,
|
|
95
|
+
) => Effect.Effect<OrchestrationThread, ApplicationError>;
|
|
92
96
|
}
|
|
93
97
|
>()("t3cli/T3Application") {}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
2
|
import * as Option from "effect/Option";
|
|
3
3
|
import * as Stream from "effect/Stream";
|
|
4
|
+
import { ORCHESTRATION_WS_METHODS } from "@t3tools/contracts";
|
|
4
5
|
|
|
5
6
|
import type { Orchestration } from "../orchestration/service.ts";
|
|
6
7
|
import { RpcError } from "../rpc/error.ts";
|
|
7
|
-
import { ORCHESTRATION_WS_METHODS } from "../protocol/schema.ts";
|
|
8
8
|
|
|
9
9
|
export function waitForShellSequence(input: {
|
|
10
10
|
readonly orchestration: Orchestration;
|
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import * as Crypto from "effect/Crypto";
|
|
2
2
|
import * as DateTime from "effect/DateTime";
|
|
3
3
|
import * as Effect from "effect/Effect";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
import {
|
|
5
|
+
CommandId,
|
|
6
|
+
MessageId,
|
|
7
|
+
ThreadId,
|
|
8
|
+
type ClientOrchestrationCommand,
|
|
9
|
+
type ModelSelection,
|
|
10
|
+
type OrchestrationProjectShell,
|
|
11
|
+
} from "@t3tools/contracts";
|
|
12
|
+
import type { ServerConfigForCli } from "../orchestration/service.ts";
|
|
11
13
|
import { resolveModelSelection } from "./model-selection.ts";
|
|
12
14
|
import type { SendThreadInput, StartThreadInput } from "./service.ts";
|
|
13
15
|
|
|
14
16
|
export const makeThreadStartCommands = Effect.fn("makeThreadStartCommands")(function* (input: {
|
|
15
17
|
readonly start: StartThreadInput;
|
|
16
|
-
readonly project:
|
|
17
|
-
readonly serverConfig:
|
|
18
|
+
readonly project: OrchestrationProjectShell;
|
|
19
|
+
readonly serverConfig: ServerConfigForCli;
|
|
18
20
|
}) {
|
|
19
21
|
const crypto = yield* Crypto.Crypto;
|
|
20
|
-
const threadId = yield* crypto.randomUUIDv4.pipe(Effect.orDie);
|
|
22
|
+
const threadId = ThreadId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
|
|
21
23
|
const createdAt = DateTime.formatIso(yield* DateTime.now);
|
|
22
24
|
const modelSelection = yield* resolveModelSelection(input);
|
|
23
25
|
const inputTitle = input.start.title?.trim();
|
|
@@ -25,7 +27,9 @@ export const makeThreadStartCommands = Effect.fn("makeThreadStartCommands")(func
|
|
|
25
27
|
const title = inputTitle !== undefined && inputTitle.length > 0 ? inputTitle : messageTitle;
|
|
26
28
|
const createCommand = {
|
|
27
29
|
type: "thread.create",
|
|
28
|
-
commandId:
|
|
30
|
+
commandId: CommandId.make(
|
|
31
|
+
`t3cli:thread-create:${yield* crypto.randomUUIDv4.pipe(Effect.orDie)}`,
|
|
32
|
+
),
|
|
29
33
|
threadId,
|
|
30
34
|
projectId: input.project.id,
|
|
31
35
|
title: title.length > 0 ? title : "New thread",
|
|
@@ -35,13 +39,15 @@ export const makeThreadStartCommands = Effect.fn("makeThreadStartCommands")(func
|
|
|
35
39
|
branch: null,
|
|
36
40
|
worktreePath: input.start.worktreePath ?? null,
|
|
37
41
|
createdAt,
|
|
38
|
-
} satisfies
|
|
42
|
+
} satisfies Extract<ClientOrchestrationCommand, { readonly type: "thread.create" }>;
|
|
39
43
|
const turnCommand = {
|
|
40
44
|
type: "thread.turn.start",
|
|
41
|
-
commandId:
|
|
45
|
+
commandId: CommandId.make(
|
|
46
|
+
`t3cli:thread-start:${yield* crypto.randomUUIDv4.pipe(Effect.orDie)}`,
|
|
47
|
+
),
|
|
42
48
|
threadId,
|
|
43
49
|
message: {
|
|
44
|
-
messageId: yield* crypto.randomUUIDv4.pipe(Effect.orDie),
|
|
50
|
+
messageId: MessageId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie)),
|
|
45
51
|
role: "user",
|
|
46
52
|
text: input.start.message,
|
|
47
53
|
attachments: [],
|
|
@@ -51,7 +57,7 @@ export const makeThreadStartCommands = Effect.fn("makeThreadStartCommands")(func
|
|
|
51
57
|
runtimeMode: "full-access",
|
|
52
58
|
interactionMode: "default",
|
|
53
59
|
createdAt,
|
|
54
|
-
} satisfies
|
|
60
|
+
} satisfies Extract<ClientOrchestrationCommand, { readonly type: "thread.turn.start" }>;
|
|
55
61
|
return { createCommand, turnCommand, threadId };
|
|
56
62
|
});
|
|
57
63
|
|
|
@@ -62,10 +68,12 @@ export const makeThreadTurnContinueCommand = Effect.fn("makeThreadTurnContinueCo
|
|
|
62
68
|
const createdAt = DateTime.formatIso(yield* DateTime.now);
|
|
63
69
|
return {
|
|
64
70
|
type: "thread.turn.start",
|
|
65
|
-
commandId:
|
|
66
|
-
|
|
71
|
+
commandId: CommandId.make(
|
|
72
|
+
`t3cli:thread-start:${yield* crypto.randomUUIDv4.pipe(Effect.orDie)}`,
|
|
73
|
+
),
|
|
74
|
+
threadId: ThreadId.make(input.threadId),
|
|
67
75
|
message: {
|
|
68
|
-
messageId: yield* crypto.randomUUIDv4.pipe(Effect.orDie),
|
|
76
|
+
messageId: MessageId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie)),
|
|
69
77
|
role: "user",
|
|
70
78
|
text: input.message,
|
|
71
79
|
attachments: [],
|
|
@@ -74,7 +82,7 @@ export const makeThreadTurnContinueCommand = Effect.fn("makeThreadTurnContinueCo
|
|
|
74
82
|
interactionMode: "default",
|
|
75
83
|
...(input.modelSelection !== undefined ? { modelSelection: input.modelSelection } : {}),
|
|
76
84
|
createdAt,
|
|
77
|
-
} satisfies
|
|
85
|
+
} satisfies Extract<ClientOrchestrationCommand, { readonly type: "thread.turn.start" }>;
|
|
78
86
|
});
|
|
79
87
|
|
|
80
88
|
export const makeThreadArchiveCommand = Effect.fn("makeThreadArchiveCommand")(function* (
|
|
@@ -83,7 +91,9 @@ export const makeThreadArchiveCommand = Effect.fn("makeThreadArchiveCommand")(fu
|
|
|
83
91
|
const crypto = yield* Crypto.Crypto;
|
|
84
92
|
return {
|
|
85
93
|
type: "thread.archive",
|
|
86
|
-
commandId:
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
commandId: CommandId.make(
|
|
95
|
+
`t3cli:thread-archive:${yield* crypto.randomUUIDv4.pipe(Effect.orDie)}`,
|
|
96
|
+
),
|
|
97
|
+
threadId: ThreadId.make(threadId),
|
|
98
|
+
} satisfies Extract<ClientOrchestrationCommand, { readonly type: "thread.archive" }>;
|
|
89
99
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
2
|
import * as Option from "effect/Option";
|
|
3
3
|
import * as Stream from "effect/Stream";
|
|
4
|
+
import type { OrchestrationMessage, OrchestrationThread } from "@t3tools/contracts";
|
|
4
5
|
|
|
5
6
|
import { ThreadSessionError } from "../domain/error.ts";
|
|
6
7
|
import type { Orchestration } from "../orchestration/service.ts";
|
|
7
|
-
import type { ThreadDetail, ThreadMessage } from "../domain/schema.ts";
|
|
8
8
|
import type { WaitEvent } from "./service.ts";
|
|
9
9
|
import {
|
|
10
10
|
applyThreadEvent,
|
|
@@ -19,13 +19,13 @@ export function watchThread(input: {
|
|
|
19
19
|
readonly orchestration: Orchestration;
|
|
20
20
|
readonly threadId: string;
|
|
21
21
|
}) {
|
|
22
|
-
let current:
|
|
23
|
-
let currentMessages: Map<string,
|
|
22
|
+
let current: OrchestrationThread | undefined;
|
|
23
|
+
let currentMessages: Map<string, OrchestrationMessage> | undefined;
|
|
24
24
|
return Stream.scoped(
|
|
25
25
|
input.orchestration.watchThreadItems(input.threadId).pipe(
|
|
26
26
|
Stream.flatMap((item) => {
|
|
27
27
|
if (item.kind === "snapshot") {
|
|
28
|
-
const messages = new Map<string,
|
|
28
|
+
const messages = new Map<string, OrchestrationMessage>();
|
|
29
29
|
for (const message of item.snapshot.thread.messages) {
|
|
30
30
|
messages.set(messageKey(message), message);
|
|
31
31
|
}
|
package/src/auth/error.ts
CHANGED
|
@@ -23,7 +23,11 @@ export class AuthTransportError extends Schema.TaggedErrorClass<AuthTransportErr
|
|
|
23
23
|
{
|
|
24
24
|
message: Schema.String,
|
|
25
25
|
cause: Schema.optionalKey(
|
|
26
|
-
Schema.Union([
|
|
26
|
+
Schema.Union([
|
|
27
|
+
HttpClientError.HttpClientErrorSchema,
|
|
28
|
+
Schema.instanceOf(Schema.SchemaError),
|
|
29
|
+
UrlError,
|
|
30
|
+
]),
|
|
27
31
|
),
|
|
28
32
|
},
|
|
29
33
|
) {}
|
package/src/auth/pairing.ts
CHANGED
|
@@ -14,7 +14,11 @@ export function parsePairingUrl(value: string): Effect.Effect<PairingUrl, AuthPa
|
|
|
14
14
|
|
|
15
15
|
const hostedHost = url.searchParams.get("host")?.trim();
|
|
16
16
|
const baseUrl = normalizeBaseUrl(
|
|
17
|
-
yield* parseUrl(
|
|
17
|
+
yield* parseUrl(
|
|
18
|
+
hostedHost !== undefined && hostedHost.length > 0
|
|
19
|
+
? hostedHost
|
|
20
|
+
: new URL(".", url).toString(),
|
|
21
|
+
),
|
|
18
22
|
);
|
|
19
23
|
return { baseUrl, credential: token };
|
|
20
24
|
});
|
|
@@ -33,7 +37,7 @@ function normalizeBaseUrl(url: URL) {
|
|
|
33
37
|
return Url.mutate(url, (current) => {
|
|
34
38
|
current.hash = "";
|
|
35
39
|
current.search = "";
|
|
36
|
-
current.pathname = "";
|
|
40
|
+
current.pathname = current.pathname === "/" ? "" : current.pathname.replace(/\/+$/u, "");
|
|
37
41
|
})
|
|
38
42
|
.toString()
|
|
39
43
|
.replace(/\/$/, "");
|
package/src/auth/transport.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as Effect from "effect/Effect";
|
|
|
2
2
|
import { HttpClient, HttpClientError, HttpClientRequest } from "effect/unstable/http";
|
|
3
3
|
|
|
4
4
|
import type { ResolvedConfig } from "../config/service.ts";
|
|
5
|
+
import { toHttpEndpointUrl } from "../config/url.ts";
|
|
5
6
|
import { AuthTransportError } from "./error.ts";
|
|
6
7
|
import {
|
|
7
8
|
decodeAuthBearerBootstrapResult,
|
|
@@ -16,8 +17,8 @@ export const makeAuthTransport = Effect.fn("makeAuthTransport")(function* () {
|
|
|
16
17
|
readonly baseUrl: string;
|
|
17
18
|
readonly credential: string;
|
|
18
19
|
}) {
|
|
19
|
-
const
|
|
20
|
-
|
|
20
|
+
const url = yield* makeHttpEndpointUrl(input.baseUrl, "/api/auth/bootstrap/bearer");
|
|
21
|
+
const request = HttpClientRequest.post(url).pipe(
|
|
21
22
|
HttpClientRequest.acceptJson,
|
|
22
23
|
HttpClientRequest.bodyJsonUnsafe({ credential: input.credential }),
|
|
23
24
|
);
|
|
@@ -51,7 +52,7 @@ export const makeAuthTransport = Effect.fn("makeAuthTransport")(function* () {
|
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
const getSession = Effect.fn("AuthTransport.getSession")(function* (config: ResolvedConfig) {
|
|
54
|
-
const request = authenticatedRequest(config, "/api/auth/session", "get");
|
|
55
|
+
const request = yield* authenticatedRequest(config, "/api/auth/session", "get");
|
|
55
56
|
const response = yield* client.execute(request).pipe(
|
|
56
57
|
Effect.catchTags({
|
|
57
58
|
HttpClientError: (error) =>
|
|
@@ -84,7 +85,7 @@ export const makeAuthTransport = Effect.fn("makeAuthTransport")(function* () {
|
|
|
84
85
|
const issueWebSocketToken = Effect.fn("AuthTransport.issueWebSocketToken")(function* (
|
|
85
86
|
config: ResolvedConfig,
|
|
86
87
|
) {
|
|
87
|
-
const request = authenticatedRequest(config, "/api/auth/ws-token", "post");
|
|
88
|
+
const request = yield* authenticatedRequest(config, "/api/auth/ws-token", "post");
|
|
88
89
|
const response = yield* client.execute(request).pipe(
|
|
89
90
|
Effect.catchTags({
|
|
90
91
|
HttpClientError: (error) =>
|
|
@@ -122,11 +123,21 @@ export const makeAuthTransport = Effect.fn("makeAuthTransport")(function* () {
|
|
|
122
123
|
});
|
|
123
124
|
|
|
124
125
|
function authenticatedRequest(config: ResolvedConfig, path: string, method: "get" | "post") {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
return makeHttpEndpointUrl(config.url, path).pipe(
|
|
127
|
+
Effect.map((url) =>
|
|
128
|
+
method === "get" ? HttpClientRequest.get(url) : HttpClientRequest.post(url),
|
|
129
|
+
),
|
|
130
|
+
Effect.map((request) =>
|
|
131
|
+
request.pipe(HttpClientRequest.acceptJson, HttpClientRequest.bearerToken(config.token)),
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function makeHttpEndpointUrl(baseUrl: string, path: string) {
|
|
137
|
+
return toHttpEndpointUrl(baseUrl, path).pipe(
|
|
138
|
+
Effect.catchTags({
|
|
139
|
+
UrlError: (error) =>
|
|
140
|
+
Effect.fail(new AuthTransportError({ message: "auth request failed", cause: error })),
|
|
141
|
+
}),
|
|
131
142
|
);
|
|
132
143
|
}
|
package/src/cli/model-format.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { OrchestrationProjectShell } from "@t3tools/contracts";
|
|
2
2
|
|
|
3
|
-
export function formatProjectsHuman(projects: ReadonlyArray<
|
|
3
|
+
export function formatProjectsHuman(projects: ReadonlyArray<OrchestrationProjectShell>) {
|
|
4
4
|
return projects
|
|
5
5
|
.map((project) => `- ${project.title}\n id: ${project.id}\n path: ${project.workspaceRoot}\n`)
|
|
6
6
|
.join("");
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export function formatProjectAddedHuman(project:
|
|
9
|
+
export function formatProjectAddedHuman(project: OrchestrationProjectShell) {
|
|
10
10
|
return `project added: ${project.title}\nid: ${project.id}\npath: ${project.workspaceRoot}`;
|
|
11
11
|
}
|
package/src/cli/thread-format.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { WaitEvent } from "../application/service.ts";
|
|
2
|
-
import type {
|
|
2
|
+
import type { OrchestrationThread, OrchestrationThreadShell } from "@t3tools/contracts";
|
|
3
3
|
import { latestAssistantMessage, threadStatus } from "../domain/thread-lifecycle.ts";
|
|
4
4
|
|
|
5
|
-
export function formatThreadsHuman(threads: ReadonlyArray<
|
|
5
|
+
export function formatThreadsHuman(threads: ReadonlyArray<OrchestrationThreadShell>) {
|
|
6
6
|
return threads
|
|
7
7
|
.map(
|
|
8
8
|
(thread) =>
|
|
@@ -12,23 +12,23 @@ export function formatThreadsHuman(threads: ReadonlyArray<ThreadShell>) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function formatThreadStartedHuman(input: {
|
|
15
|
-
readonly thread:
|
|
15
|
+
readonly thread: OrchestrationThread;
|
|
16
16
|
readonly sequence: number;
|
|
17
17
|
}) {
|
|
18
18
|
return `thread started: ${input.thread.title}\nid: ${input.thread.id}\nstatus: ${threadStatus(input.thread)}\nsequence: ${input.sequence}`;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export function formatThreadMessagesHuman(thread:
|
|
21
|
+
export function formatThreadMessagesHuman(thread: OrchestrationThread, limit: number) {
|
|
22
22
|
const messages = limit === 0 ? thread.messages : thread.messages.slice(-limit);
|
|
23
23
|
return messages.map((message) => `\n### ${message.role}\n\n${message.text}\n`).join("");
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export function formatWaitDoneHuman(thread:
|
|
26
|
+
export function formatWaitDoneHuman(thread: OrchestrationThread) {
|
|
27
27
|
const latest = latestAssistantMessage(thread);
|
|
28
28
|
return `status: ${threadStatus(thread)}\n${latest !== undefined ? `\n### ${latest.role}\n\n${latest.text}\n` : ""}`;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export function formatThreadMessagesJson(thread:
|
|
31
|
+
export function formatThreadMessagesJson(thread: OrchestrationThread, full: boolean) {
|
|
32
32
|
return full ? thread : { thread: stripThreadMessages(thread), messages: thread.messages };
|
|
33
33
|
}
|
|
34
34
|
|
package/src/config/url.ts
CHANGED
|
@@ -26,6 +26,31 @@ export function toWebSocketBaseUrl(httpBaseUrl: string) {
|
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export function toHttpEndpointUrl(httpBaseUrl: string, pathname: string) {
|
|
30
|
+
return parseUrl(httpBaseUrl).pipe(
|
|
31
|
+
Effect.flatMap((url) => Effect.succeed(withPathname(url, pathname).toString())),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function toWebSocketEndpointUrl(httpBaseUrl: string, pathname: string) {
|
|
36
|
+
return parseUrl(httpBaseUrl).pipe(
|
|
37
|
+
Effect.flatMap((url) => {
|
|
38
|
+
if (url.protocol === "http:") {
|
|
39
|
+
return Effect.succeed(withPathname(url, pathname, "ws:").toString());
|
|
40
|
+
}
|
|
41
|
+
if (url.protocol === "https:") {
|
|
42
|
+
return Effect.succeed(withPathname(url, pathname, "wss:").toString());
|
|
43
|
+
}
|
|
44
|
+
return Effect.fail(
|
|
45
|
+
new UrlError({
|
|
46
|
+
message: `unsupported server url protocol: ${url.protocol}`,
|
|
47
|
+
protocol: url.protocol,
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
29
54
|
function parseUrl(value: string): Effect.Effect<URL, UrlError> {
|
|
30
55
|
return Effect.fromResult(Url.fromString(value)).pipe(
|
|
31
56
|
Effect.catchTags({
|
|
@@ -39,7 +64,7 @@ function normalizeBaseUrl(url: URL) {
|
|
|
39
64
|
return Url.mutate(url, (current) => {
|
|
40
65
|
current.hash = "";
|
|
41
66
|
current.search = "";
|
|
42
|
-
current.pathname =
|
|
67
|
+
current.pathname = normalizePathname(current.pathname);
|
|
43
68
|
})
|
|
44
69
|
.toString()
|
|
45
70
|
.replace(/\/$/, "");
|
|
@@ -48,8 +73,27 @@ function normalizeBaseUrl(url: URL) {
|
|
|
48
73
|
function makeWebSocketUrl(url: URL, protocol: "ws:" | "wss:") {
|
|
49
74
|
return Url.mutate(url, (current) => {
|
|
50
75
|
current.protocol = protocol;
|
|
51
|
-
current.pathname = "/ws";
|
|
76
|
+
current.pathname = appendPathname(current.pathname, "/ws");
|
|
52
77
|
current.search = "";
|
|
53
78
|
current.hash = "";
|
|
54
79
|
}).toString();
|
|
55
80
|
}
|
|
81
|
+
|
|
82
|
+
function withPathname(url: URL, pathname: string, protocol?: "ws:" | "wss:") {
|
|
83
|
+
return Url.mutate(url, (current) => {
|
|
84
|
+
if (protocol !== undefined) {
|
|
85
|
+
current.protocol = protocol;
|
|
86
|
+
}
|
|
87
|
+
current.pathname = appendPathname(current.pathname, pathname);
|
|
88
|
+
current.search = "";
|
|
89
|
+
current.hash = "";
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizePathname(pathname: string) {
|
|
94
|
+
return pathname === "/" ? "" : pathname.replace(/\/+$/u, "");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function appendPathname(basePathname: string, pathname: string) {
|
|
98
|
+
return `${normalizePathname(basePathname)}${pathname.startsWith("/") ? pathname : `/${pathname}`}`;
|
|
99
|
+
}
|
package/src/domain/helpers.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type * as Path from "effect/Path";
|
|
2
|
+
import type { OrchestrationProjectShell, OrchestrationShellSnapshot } from "@t3tools/contracts";
|
|
2
3
|
|
|
3
4
|
import { ProjectLookupError } from "./error.ts";
|
|
4
|
-
import type { ProjectShell, ShellSnapshot } from "./schema.ts";
|
|
5
5
|
|
|
6
|
-
export function resolveProject(
|
|
6
|
+
export function resolveProject(
|
|
7
|
+
snapshot: OrchestrationShellSnapshot,
|
|
8
|
+
ref: string,
|
|
9
|
+
path: Path.Path,
|
|
10
|
+
cwd: string,
|
|
11
|
+
) {
|
|
7
12
|
const byId = findProjectById(snapshot, ref);
|
|
8
13
|
if (byId !== null) {
|
|
9
14
|
return byId;
|
|
@@ -18,6 +23,9 @@ export function resolveProject(snapshot: ShellSnapshot, ref: string, path: Path.
|
|
|
18
23
|
throw new ProjectLookupError({ message: `project not found: ${ref}`, ref });
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
export function findProjectById(
|
|
26
|
+
export function findProjectById(
|
|
27
|
+
snapshot: OrchestrationShellSnapshot,
|
|
28
|
+
projectId: string,
|
|
29
|
+
): OrchestrationProjectShell | null {
|
|
22
30
|
return snapshot.projects.find((project) => project.id === projectId) ?? null;
|
|
23
31
|
}
|