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,41 @@
|
|
|
1
|
+
import type { EnvironmentShape } from "../environment/service.ts";
|
|
2
|
+
|
|
3
|
+
export const humanJsonFormatChoices = ["auto", "human", "json"] as const;
|
|
4
|
+
export const humanNdjsonFormatChoices = ["auto", "human", "ndjson"] as const;
|
|
5
|
+
export const humanJsonNdjsonFormatChoices = ["auto", "human", "json", "ndjson"] as const;
|
|
6
|
+
|
|
7
|
+
export type HumanJsonFormat = (typeof humanJsonFormatChoices)[number];
|
|
8
|
+
export type HumanNdjsonFormat = (typeof humanNdjsonFormatChoices)[number];
|
|
9
|
+
export type HumanJsonNdjsonFormat = (typeof humanJsonNdjsonFormatChoices)[number];
|
|
10
|
+
|
|
11
|
+
export function resolveOutputFormat<T extends "json" | "ndjson">(
|
|
12
|
+
format: "auto" | "human" | T,
|
|
13
|
+
environment: EnvironmentShape,
|
|
14
|
+
nonHumanFormat: T,
|
|
15
|
+
): "human" | T {
|
|
16
|
+
if (format !== "auto") {
|
|
17
|
+
return format;
|
|
18
|
+
}
|
|
19
|
+
return isHumanTerminal(environment) ? "human" : nonHumanFormat;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function canRenderLiveTerminal(environment: EnvironmentShape) {
|
|
23
|
+
return (
|
|
24
|
+
environment.stderrIsTTY && environment.env.TERM !== "dumb" && !isAgentEnvironment(environment)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isHumanTerminal(environment: EnvironmentShape) {
|
|
29
|
+
return (
|
|
30
|
+
environment.stdoutIsTTY && !isAgentEnvironment(environment) && environment.env.TERM !== "dumb"
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isAgentEnvironment(environment: EnvironmentShape) {
|
|
35
|
+
return (
|
|
36
|
+
environment.env.CI !== undefined ||
|
|
37
|
+
environment.env.CODEX_CI !== undefined ||
|
|
38
|
+
environment.env.CODEX_THREAD_ID !== undefined ||
|
|
39
|
+
environment.env.T3CLI_AGENT !== undefined
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ProjectShell } from "../domain/schema.ts";
|
|
2
|
+
|
|
3
|
+
export function formatProjectsHuman(projects: ReadonlyArray<ProjectShell>) {
|
|
4
|
+
return projects
|
|
5
|
+
.map((project) => `- ${project.title}\n id: ${project.id}\n path: ${project.workspaceRoot}\n`)
|
|
6
|
+
.join("");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function formatProjectAddedHuman(project: ProjectShell) {
|
|
10
|
+
return `project added: ${project.title}\nid: ${project.id}\npath: ${project.workspaceRoot}`;
|
|
11
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
4
|
+
|
|
5
|
+
import { formatProjectAddedHuman, formatProjectsHuman } from "./project-format.ts";
|
|
6
|
+
import { T3Application } from "../application/service.ts";
|
|
7
|
+
import { Environment } from "../environment/service.ts";
|
|
8
|
+
import { humanJsonFormatChoices, resolveOutputFormat } from "./output-format.ts";
|
|
9
|
+
import { T3Output } from "./output/service.ts";
|
|
10
|
+
|
|
11
|
+
export function createProjectsCommand() {
|
|
12
|
+
return Command.make("projects").pipe(
|
|
13
|
+
Command.withDescription("project commands"),
|
|
14
|
+
Command.withSubcommands([listCommand, addCommand]),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const listCommand = Command.make(
|
|
19
|
+
"list",
|
|
20
|
+
{
|
|
21
|
+
format: Flag.choice("format", humanJsonFormatChoices).pipe(Flag.withDefault("auto")),
|
|
22
|
+
},
|
|
23
|
+
({ format }) =>
|
|
24
|
+
Effect.gen(function* () {
|
|
25
|
+
const application = yield* T3Application;
|
|
26
|
+
const environment = yield* Environment;
|
|
27
|
+
const output = yield* T3Output;
|
|
28
|
+
const resolvedFormat = resolveOutputFormat(format, environment, "json");
|
|
29
|
+
const snapshot = yield* application.loadShell();
|
|
30
|
+
if (resolvedFormat === "json") {
|
|
31
|
+
yield* output.printJson(snapshot.projects);
|
|
32
|
+
} else {
|
|
33
|
+
yield* output.writeStdout(formatProjectsHuman(snapshot.projects));
|
|
34
|
+
}
|
|
35
|
+
}),
|
|
36
|
+
).pipe(Command.withDescription("list projects"));
|
|
37
|
+
|
|
38
|
+
const addCommand = Command.make(
|
|
39
|
+
"add",
|
|
40
|
+
{
|
|
41
|
+
path: Argument.string("path"),
|
|
42
|
+
title: Flag.string("title").pipe(Flag.optional),
|
|
43
|
+
format: Flag.choice("format", humanJsonFormatChoices).pipe(Flag.withDefault("auto")),
|
|
44
|
+
},
|
|
45
|
+
({ path, title, format }) =>
|
|
46
|
+
Effect.gen(function* () {
|
|
47
|
+
const application = yield* T3Application;
|
|
48
|
+
const environment = yield* Environment;
|
|
49
|
+
const output = yield* T3Output;
|
|
50
|
+
const resolvedFormat = resolveOutputFormat(format, environment, "json");
|
|
51
|
+
const titleValue = Option.getOrUndefined(title);
|
|
52
|
+
const result = yield* application.addProject({
|
|
53
|
+
path,
|
|
54
|
+
...(titleValue !== undefined && titleValue.length > 0 ? { title: titleValue } : {}),
|
|
55
|
+
});
|
|
56
|
+
if (resolvedFormat === "json") {
|
|
57
|
+
yield* output.printJson(result);
|
|
58
|
+
} else {
|
|
59
|
+
yield* output.printInfo(formatProjectAddedHuman(result.project));
|
|
60
|
+
}
|
|
61
|
+
}),
|
|
62
|
+
).pipe(Command.withDescription("add project"));
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { WaitEvent } from "../application/service.ts";
|
|
2
|
+
import type { ThreadDetail, ThreadShell } from "../domain/schema.ts";
|
|
3
|
+
import { latestAssistantMessage, threadStatus } from "../domain/thread-lifecycle.ts";
|
|
4
|
+
|
|
5
|
+
export function formatThreadsHuman(threads: ReadonlyArray<ThreadShell>) {
|
|
6
|
+
return threads
|
|
7
|
+
.map(
|
|
8
|
+
(thread) =>
|
|
9
|
+
`- ${thread.title}\n id: ${thread.id}\n status: ${threadStatus(thread)}\n updated: ${thread.updatedAt}\n`,
|
|
10
|
+
)
|
|
11
|
+
.join("");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatThreadStartedHuman(input: {
|
|
15
|
+
readonly thread: ThreadDetail;
|
|
16
|
+
readonly sequence: number;
|
|
17
|
+
}) {
|
|
18
|
+
return `thread started: ${input.thread.title}\nid: ${input.thread.id}\nstatus: ${threadStatus(input.thread)}\nsequence: ${input.sequence}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatThreadMessagesHuman(thread: ThreadDetail, limit: number) {
|
|
22
|
+
const messages = limit === 0 ? thread.messages : thread.messages.slice(-limit);
|
|
23
|
+
return messages.map((message) => `\n### ${message.role}\n\n${message.text}\n`).join("");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatWaitDoneHuman(thread: ThreadDetail) {
|
|
27
|
+
const latest = latestAssistantMessage(thread);
|
|
28
|
+
return `status: ${threadStatus(thread)}\n${latest !== undefined ? `\n### ${latest.role}\n\n${latest.text}\n` : ""}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function formatThreadMessagesJson(thread: ThreadDetail, full: boolean) {
|
|
32
|
+
return full ? thread : { thread: stripThreadMessages(thread), messages: thread.messages };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function formatWaitEventNdjson(event: WaitEvent) {
|
|
36
|
+
if (event.type !== "thread" && event.type !== "done") {
|
|
37
|
+
return event;
|
|
38
|
+
}
|
|
39
|
+
const compactThread = stripThreadHeavy(event.thread);
|
|
40
|
+
return event.type === "done"
|
|
41
|
+
? {
|
|
42
|
+
type: "done",
|
|
43
|
+
thread: compactThread,
|
|
44
|
+
latestAssistantMessage: latestAssistantMessage(event.thread) ?? null,
|
|
45
|
+
}
|
|
46
|
+
: {
|
|
47
|
+
type: "thread",
|
|
48
|
+
thread: compactThread,
|
|
49
|
+
messageCount: event.thread.messages.length,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function stripThreadMessages<T extends { readonly messages: unknown }>(thread: T) {
|
|
54
|
+
const { messages: _messages, ...rest } = thread;
|
|
55
|
+
return rest;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function stripThreadHeavy<
|
|
59
|
+
T extends {
|
|
60
|
+
readonly messages: unknown;
|
|
61
|
+
readonly activities?: unknown;
|
|
62
|
+
readonly proposedPlans?: unknown;
|
|
63
|
+
readonly checkpoints?: unknown;
|
|
64
|
+
},
|
|
65
|
+
>(thread: T) {
|
|
66
|
+
const {
|
|
67
|
+
messages: _messages,
|
|
68
|
+
activities: _activities,
|
|
69
|
+
proposedPlans: _proposedPlans,
|
|
70
|
+
checkpoints: _checkpoints,
|
|
71
|
+
...rest
|
|
72
|
+
} = thread;
|
|
73
|
+
return rest;
|
|
74
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
3
|
+
|
|
4
|
+
import { T3Application } from "../../application/service.ts";
|
|
5
|
+
import { Environment } from "../../environment/service.ts";
|
|
6
|
+
import { humanJsonFormatChoices, resolveOutputFormat } from "../output-format.ts";
|
|
7
|
+
import { T3Output } from "../output/service.ts";
|
|
8
|
+
|
|
9
|
+
export const archiveThreadCommand = Command.make(
|
|
10
|
+
"archive",
|
|
11
|
+
{
|
|
12
|
+
thread: Argument.string("thread"),
|
|
13
|
+
format: Flag.choice("format", humanJsonFormatChoices).pipe(Flag.withDefault("auto")),
|
|
14
|
+
},
|
|
15
|
+
({ thread, format }) =>
|
|
16
|
+
Effect.gen(function* () {
|
|
17
|
+
const application = yield* T3Application;
|
|
18
|
+
const environment = yield* Environment;
|
|
19
|
+
const output = yield* T3Output;
|
|
20
|
+
const resolvedFormat = resolveOutputFormat(format, environment, "json");
|
|
21
|
+
const dispatch = yield* application.archiveThread(thread);
|
|
22
|
+
if (resolvedFormat === "json") {
|
|
23
|
+
yield* output.printJson(dispatch);
|
|
24
|
+
} else {
|
|
25
|
+
yield* output.printInfo(`thread archived: ${thread}\nsequence: ${dispatch.sequence}`);
|
|
26
|
+
}
|
|
27
|
+
}),
|
|
28
|
+
).pipe(Command.withDescription("archive thread"));
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
3
|
+
|
|
4
|
+
import { formatThreadsHuman } from "../thread-format.ts";
|
|
5
|
+
import { T3Application } from "../../application/service.ts";
|
|
6
|
+
import { Environment } from "../../environment/service.ts";
|
|
7
|
+
import { humanJsonFormatChoices, resolveOutputFormat } from "../output-format.ts";
|
|
8
|
+
import { T3Output } from "../output/service.ts";
|
|
9
|
+
|
|
10
|
+
export const listThreadsCommand = Command.make(
|
|
11
|
+
"list",
|
|
12
|
+
{
|
|
13
|
+
project: Argument.string("project"),
|
|
14
|
+
format: Flag.choice("format", humanJsonFormatChoices).pipe(Flag.withDefault("auto")),
|
|
15
|
+
},
|
|
16
|
+
({ project, format }) =>
|
|
17
|
+
Effect.gen(function* () {
|
|
18
|
+
const application = yield* T3Application;
|
|
19
|
+
const environment = yield* Environment;
|
|
20
|
+
const output = yield* T3Output;
|
|
21
|
+
const resolvedFormat = resolveOutputFormat(format, environment, "json");
|
|
22
|
+
const result = yield* application.listThreads(project);
|
|
23
|
+
if (resolvedFormat === "json") {
|
|
24
|
+
yield* output.printJson(result.threads);
|
|
25
|
+
} else {
|
|
26
|
+
yield* output.writeStdout(formatThreadsHuman(result.threads));
|
|
27
|
+
}
|
|
28
|
+
}),
|
|
29
|
+
).pipe(Command.withDescription("list project threads"));
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
3
|
+
|
|
4
|
+
import { InvalidLimitError } from "../error.ts";
|
|
5
|
+
import { formatThreadMessagesHuman, formatThreadMessagesJson } from "../thread-format.ts";
|
|
6
|
+
import { T3Application } from "../../application/service.ts";
|
|
7
|
+
import { Environment } from "../../environment/service.ts";
|
|
8
|
+
import { humanJsonFormatChoices, resolveOutputFormat } from "../output-format.ts";
|
|
9
|
+
import { T3Output } from "../output/service.ts";
|
|
10
|
+
|
|
11
|
+
export const getThreadMessagesCommand = Command.make(
|
|
12
|
+
"messages",
|
|
13
|
+
{
|
|
14
|
+
thread: Argument.string("thread"),
|
|
15
|
+
limit: Flag.integer("limit").pipe(Flag.withDefault(20)),
|
|
16
|
+
full: Flag.boolean("full"),
|
|
17
|
+
format: Flag.choice("format", humanJsonFormatChoices).pipe(Flag.withDefault("auto")),
|
|
18
|
+
},
|
|
19
|
+
({ thread, limit, full, format }) =>
|
|
20
|
+
Effect.gen(function* () {
|
|
21
|
+
if (limit < 0) {
|
|
22
|
+
return yield* Effect.fail(
|
|
23
|
+
new InvalidLimitError({ message: `invalid limit: ${limit}`, value: String(limit) }),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const application = yield* T3Application;
|
|
27
|
+
const environment = yield* Environment;
|
|
28
|
+
const output = yield* T3Output;
|
|
29
|
+
const resolvedFormat = resolveOutputFormat(format, environment, "json");
|
|
30
|
+
const detail = yield* application.getThreadMessages(thread);
|
|
31
|
+
if (resolvedFormat === "json") {
|
|
32
|
+
return yield* output.printJson(formatThreadMessagesJson(detail, full));
|
|
33
|
+
}
|
|
34
|
+
return yield* output.writeStdout(formatThreadMessagesHuman(detail, limit));
|
|
35
|
+
}),
|
|
36
|
+
).pipe(Command.withDescription("get latest thread messages"));
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
4
|
+
|
|
5
|
+
import { readInitialMessage } from "../message-input.ts";
|
|
6
|
+
import { buildModelOptions } from "../model-options.ts";
|
|
7
|
+
import { T3Application } from "../../application/service.ts";
|
|
8
|
+
import { Environment } from "../../environment/service.ts";
|
|
9
|
+
import { T3Input } from "../input/service.ts";
|
|
10
|
+
import {
|
|
11
|
+
canRenderLiveTerminal,
|
|
12
|
+
humanJsonNdjsonFormatChoices,
|
|
13
|
+
resolveOutputFormat,
|
|
14
|
+
} from "../output-format.ts";
|
|
15
|
+
import { T3Output } from "../output/service.ts";
|
|
16
|
+
import { printWaitEventsHuman, printWaitEventsNdjson } from "../wait-events.ts";
|
|
17
|
+
|
|
18
|
+
export const sendThreadCommand = Command.make(
|
|
19
|
+
"send",
|
|
20
|
+
{
|
|
21
|
+
thread: Argument.string("thread"),
|
|
22
|
+
message: Argument.string("message").pipe(Argument.optional),
|
|
23
|
+
stdin: Flag.boolean("stdin"),
|
|
24
|
+
option: Flag.keyValuePair("option").pipe(Flag.optional),
|
|
25
|
+
reasoningEffort: Flag.string("reasoning-effort").pipe(Flag.optional),
|
|
26
|
+
effort: Flag.string("effort").pipe(Flag.optional),
|
|
27
|
+
fastMode: Flag.boolean("fast-mode").pipe(Flag.optional),
|
|
28
|
+
thinking: Flag.boolean("thinking").pipe(Flag.optional),
|
|
29
|
+
wait: Flag.boolean("wait"),
|
|
30
|
+
format: Flag.choice("format", humanJsonNdjsonFormatChoices).pipe(Flag.withDefault("auto")),
|
|
31
|
+
},
|
|
32
|
+
({ thread, message, stdin, option, reasoningEffort, effort, fastMode, thinking, wait, format }) =>
|
|
33
|
+
Effect.gen(function* () {
|
|
34
|
+
const inputService = yield* T3Input;
|
|
35
|
+
const text = yield* readInitialMessage({
|
|
36
|
+
message: Option.getOrUndefined(message),
|
|
37
|
+
fromStdin: stdin,
|
|
38
|
+
readStdin: inputService.readStdin,
|
|
39
|
+
});
|
|
40
|
+
const options = buildModelOptions({
|
|
41
|
+
option,
|
|
42
|
+
reasoningEffort,
|
|
43
|
+
effort,
|
|
44
|
+
fastMode,
|
|
45
|
+
thinking,
|
|
46
|
+
});
|
|
47
|
+
const input = {
|
|
48
|
+
message: text,
|
|
49
|
+
threadId: thread,
|
|
50
|
+
...(options.length > 0 ? { options } : {}),
|
|
51
|
+
};
|
|
52
|
+
const application = yield* T3Application;
|
|
53
|
+
const environment = yield* Environment;
|
|
54
|
+
const output = yield* T3Output;
|
|
55
|
+
const resolvedFormat = resolveOutputFormat(format, environment, wait ? "ndjson" : "json");
|
|
56
|
+
|
|
57
|
+
if (resolvedFormat === "ndjson") {
|
|
58
|
+
const sent = yield* application.sendThread(input, { until: wait ? "dispatch" : "visible" });
|
|
59
|
+
yield* output.printNdjson({ type: "dispatch", sequence: sent.dispatch.sequence });
|
|
60
|
+
if (wait) {
|
|
61
|
+
yield* printWaitEventsNdjson(output, application.watchThread(sent.threadId));
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (wait) {
|
|
67
|
+
const sent = yield* application.sendThread(input, { until: "dispatch" });
|
|
68
|
+
if (resolvedFormat === "json") {
|
|
69
|
+
const finalThread = yield* application.waitForThread(sent.threadId);
|
|
70
|
+
yield* output.printJson({
|
|
71
|
+
dispatch: sent.dispatch,
|
|
72
|
+
threadId: sent.threadId,
|
|
73
|
+
thread: finalThread,
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
yield* printWaitEventsHuman(output, application.watchThread(sent.threadId), {
|
|
78
|
+
threadId: sent.threadId,
|
|
79
|
+
live: canRenderLiveTerminal(environment),
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result = yield* application.sendThread(input, { until: "visible" });
|
|
85
|
+
if (resolvedFormat === "json") {
|
|
86
|
+
yield* output.printJson(result);
|
|
87
|
+
} else {
|
|
88
|
+
yield* output.printInfo(`message sent: ${result.threadId}`);
|
|
89
|
+
}
|
|
90
|
+
}),
|
|
91
|
+
).pipe(Command.withDescription("send message to existing thread"));
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import * as Stream from "effect/Stream";
|
|
4
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
5
|
+
|
|
6
|
+
import { readInitialMessage } from "../message-input.ts";
|
|
7
|
+
import { buildModelOptions } from "../model-options.ts";
|
|
8
|
+
import { formatThreadStartedHuman } from "../thread-format.ts";
|
|
9
|
+
import { T3Application } from "../../application/service.ts";
|
|
10
|
+
import { Environment } from "../../environment/service.ts";
|
|
11
|
+
import { T3Input } from "../input/service.ts";
|
|
12
|
+
import {
|
|
13
|
+
canRenderLiveTerminal,
|
|
14
|
+
humanJsonNdjsonFormatChoices,
|
|
15
|
+
resolveOutputFormat,
|
|
16
|
+
} from "../output-format.ts";
|
|
17
|
+
import { T3Output } from "../output/service.ts";
|
|
18
|
+
import { printWaitEventsHuman, printWaitEventsNdjson } from "../wait-events.ts";
|
|
19
|
+
|
|
20
|
+
export const startThreadCommand = Command.make(
|
|
21
|
+
"start",
|
|
22
|
+
{
|
|
23
|
+
project: Argument.string("project"),
|
|
24
|
+
message: Argument.string("message").pipe(Argument.optional),
|
|
25
|
+
stdin: Flag.boolean("stdin"),
|
|
26
|
+
title: Flag.string("title").pipe(Flag.optional),
|
|
27
|
+
worktree: Flag.string("worktree").pipe(Flag.optional),
|
|
28
|
+
provider: Flag.string("provider").pipe(Flag.optional),
|
|
29
|
+
model: Flag.string("model").pipe(Flag.optional),
|
|
30
|
+
option: Flag.keyValuePair("option").pipe(Flag.optional),
|
|
31
|
+
reasoningEffort: Flag.string("reasoning-effort").pipe(Flag.optional),
|
|
32
|
+
effort: Flag.string("effort").pipe(Flag.optional),
|
|
33
|
+
fastMode: Flag.boolean("fast-mode").pipe(Flag.optional),
|
|
34
|
+
thinking: Flag.boolean("thinking").pipe(Flag.optional),
|
|
35
|
+
wait: Flag.boolean("wait"),
|
|
36
|
+
format: Flag.choice("format", humanJsonNdjsonFormatChoices).pipe(Flag.withDefault("auto")),
|
|
37
|
+
},
|
|
38
|
+
({
|
|
39
|
+
project,
|
|
40
|
+
message,
|
|
41
|
+
stdin,
|
|
42
|
+
title,
|
|
43
|
+
worktree,
|
|
44
|
+
provider,
|
|
45
|
+
model,
|
|
46
|
+
option,
|
|
47
|
+
reasoningEffort,
|
|
48
|
+
effort,
|
|
49
|
+
fastMode,
|
|
50
|
+
thinking,
|
|
51
|
+
wait,
|
|
52
|
+
format,
|
|
53
|
+
}) =>
|
|
54
|
+
Effect.gen(function* () {
|
|
55
|
+
const inputService = yield* T3Input;
|
|
56
|
+
const text = yield* readInitialMessage({
|
|
57
|
+
message: Option.getOrUndefined(message),
|
|
58
|
+
fromStdin: stdin,
|
|
59
|
+
readStdin: inputService.readStdin,
|
|
60
|
+
});
|
|
61
|
+
const titleValue = Option.getOrUndefined(title);
|
|
62
|
+
const worktreeValue = Option.getOrUndefined(worktree);
|
|
63
|
+
const providerValue = Option.getOrUndefined(provider);
|
|
64
|
+
const modelValue = Option.getOrUndefined(model);
|
|
65
|
+
const options = buildModelOptions({
|
|
66
|
+
option,
|
|
67
|
+
reasoningEffort,
|
|
68
|
+
effort,
|
|
69
|
+
fastMode,
|
|
70
|
+
thinking,
|
|
71
|
+
});
|
|
72
|
+
const input = {
|
|
73
|
+
projectRef: project,
|
|
74
|
+
message: text,
|
|
75
|
+
...(titleValue !== undefined && titleValue.length > 0 ? { title: titleValue } : {}),
|
|
76
|
+
...(worktreeValue !== undefined && worktreeValue.length > 0
|
|
77
|
+
? { worktreePath: worktreeValue }
|
|
78
|
+
: {}),
|
|
79
|
+
...(providerValue !== undefined && providerValue.length > 0
|
|
80
|
+
? { provider: providerValue }
|
|
81
|
+
: {}),
|
|
82
|
+
...(modelValue !== undefined && modelValue.length > 0 ? { model: modelValue } : {}),
|
|
83
|
+
...(options.length > 0 ? { options } : {}),
|
|
84
|
+
};
|
|
85
|
+
const application = yield* T3Application;
|
|
86
|
+
const environment = yield* Environment;
|
|
87
|
+
const output = yield* T3Output;
|
|
88
|
+
const resolvedFormat = resolveOutputFormat(format, environment, wait ? "ndjson" : "json");
|
|
89
|
+
|
|
90
|
+
if (resolvedFormat === "ndjson") {
|
|
91
|
+
const started = yield* application.startThread(input, {
|
|
92
|
+
until: wait ? "dispatch" : "visible",
|
|
93
|
+
});
|
|
94
|
+
yield* output.printNdjson({ type: "dispatch", sequence: started.dispatch.sequence });
|
|
95
|
+
if (wait) {
|
|
96
|
+
yield* printWaitEventsNdjson(output, application.watchThread(started.threadId));
|
|
97
|
+
} else {
|
|
98
|
+
yield* printWaitEventsNdjson(
|
|
99
|
+
output,
|
|
100
|
+
Stream.fromIterable([{ type: "thread", thread: started.thread! }]),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (wait) {
|
|
107
|
+
const started = yield* application.startThread(input, { until: "dispatch" });
|
|
108
|
+
if (resolvedFormat === "json") {
|
|
109
|
+
const thread = yield* application.waitForThread(started.threadId);
|
|
110
|
+
yield* output.printJson({
|
|
111
|
+
dispatch: started.dispatch,
|
|
112
|
+
threadId: started.threadId,
|
|
113
|
+
thread,
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
yield* printWaitEventsHuman(output, application.watchThread(started.threadId), {
|
|
118
|
+
threadId: started.threadId,
|
|
119
|
+
live: canRenderLiveTerminal(environment),
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const result = yield* application.startThread(input, { until: "visible" });
|
|
125
|
+
if (resolvedFormat === "json") {
|
|
126
|
+
yield* output.printJson(result);
|
|
127
|
+
} else {
|
|
128
|
+
yield* output.printInfo(
|
|
129
|
+
formatThreadStartedHuman({
|
|
130
|
+
thread: result.thread!,
|
|
131
|
+
sequence: result.dispatch.sequence,
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}),
|
|
136
|
+
).pipe(Command.withDescription("start thread with initial message"));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
3
|
+
|
|
4
|
+
import { Environment } from "../../environment/service.ts";
|
|
5
|
+
import { T3Application } from "../../application/service.ts";
|
|
6
|
+
import {
|
|
7
|
+
canRenderLiveTerminal,
|
|
8
|
+
humanNdjsonFormatChoices,
|
|
9
|
+
resolveOutputFormat,
|
|
10
|
+
} from "../output-format.ts";
|
|
11
|
+
import { T3Output } from "../output/service.ts";
|
|
12
|
+
import { printWaitEventsHuman, printWaitEventsNdjson } from "../wait-events.ts";
|
|
13
|
+
|
|
14
|
+
export const waitForThreadCommand = Command.make(
|
|
15
|
+
"wait",
|
|
16
|
+
{
|
|
17
|
+
thread: Argument.string("thread"),
|
|
18
|
+
format: Flag.choice("format", humanNdjsonFormatChoices).pipe(Flag.withDefault("auto")),
|
|
19
|
+
},
|
|
20
|
+
({ thread, format }) =>
|
|
21
|
+
Effect.gen(function* () {
|
|
22
|
+
const application = yield* T3Application;
|
|
23
|
+
const environment = yield* Environment;
|
|
24
|
+
const output = yield* T3Output;
|
|
25
|
+
const resolvedFormat = resolveOutputFormat(format, environment, "ndjson");
|
|
26
|
+
if (resolvedFormat === "ndjson") {
|
|
27
|
+
yield* printWaitEventsNdjson(output, application.watchThread(thread));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
yield* printWaitEventsHuman(output, application.watchThread(thread), {
|
|
31
|
+
threadId: thread,
|
|
32
|
+
live: canRenderLiveTerminal(environment),
|
|
33
|
+
});
|
|
34
|
+
}),
|
|
35
|
+
).pipe(Command.withDescription("wait for thread to pause"));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Command } from "effect/unstable/cli";
|
|
2
|
+
|
|
3
|
+
import { archiveThreadCommand } from "./threads/archive.ts";
|
|
4
|
+
import { listThreadsCommand } from "./threads/list.ts";
|
|
5
|
+
import { getThreadMessagesCommand } from "./threads/messages.ts";
|
|
6
|
+
import { sendThreadCommand } from "./threads/send.ts";
|
|
7
|
+
import { startThreadCommand } from "./threads/start.ts";
|
|
8
|
+
import { waitForThreadCommand } from "./threads/wait.ts";
|
|
9
|
+
|
|
10
|
+
export function createThreadsCommand() {
|
|
11
|
+
return Command.make("threads").pipe(
|
|
12
|
+
Command.withDescription("thread commands"),
|
|
13
|
+
Command.withSubcommands([
|
|
14
|
+
listThreadsCommand,
|
|
15
|
+
startThreadCommand,
|
|
16
|
+
sendThreadCommand,
|
|
17
|
+
archiveThreadCommand,
|
|
18
|
+
getThreadMessagesCommand,
|
|
19
|
+
waitForThreadCommand,
|
|
20
|
+
]),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import * as Stream from "effect/Stream";
|
|
4
|
+
import * as Terminal from "effect/Terminal";
|
|
5
|
+
|
|
6
|
+
import type { ApplicationError } from "../application/error.ts";
|
|
7
|
+
import type { WaitEvent } from "../application/service.ts";
|
|
8
|
+
import { ThreadSessionError } from "../domain/error.ts";
|
|
9
|
+
import { latestAssistantMessage } from "../domain/thread-lifecycle.ts";
|
|
10
|
+
import type { T3Output } from "./output/service.ts";
|
|
11
|
+
import { formatWaitDoneHuman, formatWaitEventNdjson } from "./thread-format.ts";
|
|
12
|
+
|
|
13
|
+
export function printWaitEventsNdjson(
|
|
14
|
+
output: T3Output["Service"],
|
|
15
|
+
events: Stream.Stream<WaitEvent, ApplicationError>,
|
|
16
|
+
) {
|
|
17
|
+
return events.pipe(
|
|
18
|
+
Stream.tap((event) => output.printNdjson(formatWaitEventNdjson(event))),
|
|
19
|
+
Stream.runDrain,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function printWaitEventsHuman(
|
|
24
|
+
output: T3Output["Service"],
|
|
25
|
+
events: Stream.Stream<WaitEvent, ApplicationError>,
|
|
26
|
+
options: {
|
|
27
|
+
readonly threadId: string;
|
|
28
|
+
readonly live: boolean;
|
|
29
|
+
},
|
|
30
|
+
) {
|
|
31
|
+
let latest = "";
|
|
32
|
+
let status = `waiting for ${options.threadId}`;
|
|
33
|
+
let rendered = false;
|
|
34
|
+
let columns = 0;
|
|
35
|
+
|
|
36
|
+
const render = () => {
|
|
37
|
+
if (!options.live) {
|
|
38
|
+
return Effect.void;
|
|
39
|
+
}
|
|
40
|
+
rendered = true;
|
|
41
|
+
return output.writeStderr(
|
|
42
|
+
`\r\x1b[2K${fitLine(`${status}${latest.length > 0 ? ` | ${latest}` : ""}`, columns)}`,
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return Effect.gen(function* () {
|
|
47
|
+
if (options.live) {
|
|
48
|
+
const terminal = yield* Terminal.Terminal;
|
|
49
|
+
columns = yield* terminal.columns;
|
|
50
|
+
}
|
|
51
|
+
if (options.live) {
|
|
52
|
+
yield* render();
|
|
53
|
+
} else {
|
|
54
|
+
yield* output.writeStderr(`${status}...\n`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
yield* events.pipe(
|
|
58
|
+
Stream.tap((event) => {
|
|
59
|
+
if (event.type === "thread") {
|
|
60
|
+
const message = latestAssistantMessage(event.thread);
|
|
61
|
+
if (message !== undefined) {
|
|
62
|
+
latest = compactLine(message.text);
|
|
63
|
+
}
|
|
64
|
+
return render();
|
|
65
|
+
}
|
|
66
|
+
if (event.type === "message") {
|
|
67
|
+
if (event.message.role === "assistant") {
|
|
68
|
+
latest = compactLine(event.message.text);
|
|
69
|
+
}
|
|
70
|
+
return render();
|
|
71
|
+
}
|
|
72
|
+
if (event.type === "status") {
|
|
73
|
+
status = `${event.threadId}: ${event.status}`;
|
|
74
|
+
return render();
|
|
75
|
+
}
|
|
76
|
+
return Effect.gen(function* () {
|
|
77
|
+
if (rendered) {
|
|
78
|
+
yield* output.writeStderr("\r\x1b[2K");
|
|
79
|
+
}
|
|
80
|
+
yield* output.writeStdout(formatWaitDoneHuman(event.thread));
|
|
81
|
+
});
|
|
82
|
+
}),
|
|
83
|
+
Stream.runLast,
|
|
84
|
+
Effect.flatMap((event) => {
|
|
85
|
+
if (Option.isSome(event) && event.value.type === "done") {
|
|
86
|
+
return Effect.void;
|
|
87
|
+
}
|
|
88
|
+
return Effect.fail(
|
|
89
|
+
new ThreadSessionError({
|
|
90
|
+
message: `thread wait ended without done event: ${options.threadId}`,
|
|
91
|
+
threadId: options.threadId,
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function compactLine(text: string) {
|
|
100
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
101
|
+
return compact.length <= 120 ? compact : `...${compact.slice(-117)}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function fitLine(text: string, columns: number) {
|
|
105
|
+
if (columns <= 0 || text.length <= columns) {
|
|
106
|
+
return text;
|
|
107
|
+
}
|
|
108
|
+
if (columns <= 3) {
|
|
109
|
+
return text.slice(0, columns);
|
|
110
|
+
}
|
|
111
|
+
return `${text.slice(0, columns - 3)}...`;
|
|
112
|
+
}
|