Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
d63ccb7
feat(server): upgrade the ACP runtime and bridge T3 MCP to ACP agents
t3dotgg Sep 26, 2026
5091fee
fix(server): MCP-over-ACP notifications no longer wait behind tool calls
t3dotgg Sep 26, 2026
87ec3df
fix(server): Grok process-group stops return once Grok exits
t3dotgg Sep 26, 2026
b8ab1a6
docs: Grok's Auto-accept edits is still selectable
t3dotgg Sep 26, 2026
42eca9c
fix(server): Grok text generation picks models on ACP v2
t3dotgg Sep 26, 2026
1a859ed
chore(server): make currentGrokModelIdFromSessionSetup private
t3dotgg Sep 26, 2026
6c165d7
test(server): run Grok, Cursor and Antigravity adapter tests on ACP v1
t3dotgg Sep 26, 2026
12ed85e
perf(server): back off the /proc ownership watcher while a session is…
t3dotgg Sep 26, 2026
6dce1f4
fix(server): Auto-accept edits asks before ACP MCP tool calls
t3dotgg Sep 26, 2026
869cffd
fix(server): ACP auto-approval grants once, not always
t3dotgg Sep 26, 2026
fc5fdd9
fix(server): Antigravity file writes resolve linked parent directories
t3dotgg Sep 26, 2026
6f3525b
fix(server): ad-hoc ACP session/resume times out like startup resume
t3dotgg Sep 26, 2026
45d4050
fix(server): acp-mcp-call reports failed tool calls on stderr
t3dotgg Sep 26, 2026
64e12bb
chore(server): explain ACP nodeBuiltinImport directives, drop unused …
t3dotgg Sep 26, 2026
bd1bd2b
chore(server): explain the stdio bridge's nodeBuiltinImport directive
t3dotgg Sep 26, 2026
f56e8b3
fix(server): an ACP agent exit no longer closes the adapter's session…
t3dotgg Sep 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tmp/
1,164 changes: 1,013 additions & 151 deletions apps/server/scripts/acp-mock-agent.ts

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions apps/server/scripts/acp-thread-spawn-helper.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#define _GNU_SOURCE

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

static const char *pid_path;

static void *spawn_child(void *unused) {
(void)unused;
pid_t child = fork();
if (child == 0) {
execlp("sleep", "sleep", "120", NULL);
_exit(127);
}
if (child < 0) return NULL;
FILE *file = fopen(pid_path, "w");
if (file != NULL) {
fprintf(file, "%ld %d\n", syscall(SYS_gettid), child);
fclose(file);
}
waitpid(child, NULL, 0);
return NULL;
}

int main(int argc, char **argv) {
if (argc != 2) return 2;
pid_path = argv[1];
pthread_t worker;
if (pthread_create(&worker, NULL, spawn_child, NULL) != 0) return 3;
if (pthread_join(worker, NULL) != 0) return 4;
return 0;
}
3 changes: 3 additions & 0 deletions apps/server/scripts/acpMockCancellationState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function beginAcpMockPrompt(cancelledSessions: Set<string>, sessionId: string): void {
cancelledSessions.delete(sessionId);
}
2 changes: 1 addition & 1 deletion apps/server/src/bin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import * as CliError from "effect/unstable/cli/CliError";
import * as TestConsole from "effect/testing/TestConsole";
import { Command } from "effect/unstable/cli";

import { cli, makeCli } from "./bin.ts";
import { cli, makeCli } from "./binCli.ts";
import * as ServiceLauncherClient from "./cloud/serviceLauncherClient.ts";
import {
SERVICE_LAUNCHER_CONTEXT_ENV,
Expand Down
102 changes: 17 additions & 85 deletions apps/server/src/bin.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,13 @@
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
import * as NodeServices from "@effect/platform-node/NodeServices";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { Argument, Command } from "effect/unstable/cli";
import * as CliError from "effect/unstable/cli/CliError";

import * as NetService from "@t3tools/shared/Net";
import packageJson from "../package.json" with { type: "json" };
import { authCommand } from "./cli/auth.ts";
import { appCommand } from "./cli/app.ts";
import { connectCommand } from "./cli/connect.ts";
import { pairCommand } from "./cli/pair.ts";
import { hasCloudPublicConfig } from "./cloud/publicConfig.ts";
import { sharedServerCommandFlags } from "./cli/config.ts";
/**
* Thin CLI entry.
*
* Every ACP agent spawns `t3 acp-mcp-bridge` while opening its session, and
* terminal-fallback agents run `t3 acp-mcp-call` per tool call, so their
* startup sits on first-message latency. Both dispatch here before the full
* CLI module graph (seconds of evaluation) loads; everything else defers to
* the real CLI in ./binCli.ts.
*/
import { isEntrypoint } from "./entrypoint.ts";
import { projectCommand } from "./cli/project.ts";
import { runServerCommand, serveCommand, startCommand } from "./cli/server.ts";
import { serviceCommand } from "./cli/service.ts";
import { uninstallCommand } from "./cli/uninstall.ts";
import { updateCommand } from "./cli/update.ts";
import { claudeHistoryCommand } from "./cli/claudeHistory.ts";
import { serviceLauncherCommand } from "./cli/serviceLauncher.ts";
import { servicePreflightCommand } from "./cli/servicePreflight.ts";
import { sshHelperCommand } from "./cli/sshHelper.ts";
import { themeCommand } from "./cli/theme.ts";
import { traceCommand } from "./cli/trace.ts";
import { triageCommand } from "./cli/triage.ts";

const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer);

const connectPublicConfigMissingMessage =
"T3 Connect commands are unavailable: this build is missing T3 Connect public configuration.";

class ConnectPublicConfigMissingError extends CliError.UserError {
override get message() {
return connectPublicConfigMissingMessage;
}
}

const connectUnavailableCommand = Command.make("connect", {
command: Argument.String("command").pipe(Argument.variadic),
}).pipe(
Command.withDescription("T3 Connect is unavailable in builds without public configuration."),
Command.unlisted,
Command.withHandler(() =>
Effect.fail(
new CliError.ShowHelp({
commandPath: ["t3", "connect"],
errors: [new ConnectPublicConfigMissingError({ cause: connectPublicConfigMissingMessage })],
}),
),
),
);

export const makeCli = ({ cloudEnabled = hasCloudPublicConfig } = {}) =>
Command.make("t3", { ...sharedServerCommandFlags }).pipe(
Command.withDescription("Run the T3 Code server."),
Command.withHandler((flags) => runServerCommand(flags)),
Command.withSubcommands([
startCommand,
serveCommand,
appCommand,
pairCommand,
authCommand,
projectCommand,
serviceCommand,
updateCommand,
uninstallCommand,
serviceLauncherCommand,
claudeHistoryCommand,
servicePreflightCommand,
sshHelperCommand,
themeCommand,
traceCommand,
triageCommand,
cloudEnabled ? connectCommand : connectUnavailableCommand,
]),
);

export const cli = makeCli();

if (
isEntrypoint({
Expand All @@ -87,9 +16,12 @@ if (
runtimeMain: import.meta.main,
})
) {
Command.run(cli, { version: packageJson.version }).pipe(
Effect.scoped,
Effect.provide(CliRuntimeLayer),
NodeRuntime.runMain,
);
const command = process.argv[2];
if (command === "acp-mcp-bridge" || command === "acp-mcp-call") {
const { runAcpMcpCliFastPath } = await import("./mcp/AcpMcpStdioBridge.ts");
await runAcpMcpCliFastPath(command, process.argv.slice(3));
} else {
const { runCli } = await import("./binCli.ts");
runCli();
}
}
91 changes: 91 additions & 0 deletions apps/server/src/binCli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
import * as NodeServices from "@effect/platform-node/NodeServices";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { Argument, Command } from "effect/unstable/cli";
import * as CliError from "effect/unstable/cli/CliError";

import * as NetService from "@t3tools/shared/Net";
import packageJson from "../package.json" with { type: "json" };
import { acpMcpBridgeCommand, acpMcpCallCommand } from "./cli/acpMcpBridge.ts";
import { authCommand } from "./cli/auth.ts";
import { appCommand } from "./cli/app.ts";
import { connectCommand } from "./cli/connect.ts";
import { pairCommand } from "./cli/pair.ts";
import { hasCloudPublicConfig } from "./cloud/publicConfig.ts";
import { sharedServerCommandFlags } from "./cli/config.ts";
import { projectCommand } from "./cli/project.ts";
import { runServerCommand, serveCommand, startCommand } from "./cli/server.ts";
import { serviceCommand } from "./cli/service.ts";
import { uninstallCommand } from "./cli/uninstall.ts";
import { updateCommand } from "./cli/update.ts";
import { claudeHistoryCommand } from "./cli/claudeHistory.ts";
import { serviceLauncherCommand } from "./cli/serviceLauncher.ts";
import { servicePreflightCommand } from "./cli/servicePreflight.ts";
import { sshHelperCommand } from "./cli/sshHelper.ts";
import { themeCommand } from "./cli/theme.ts";
import { traceCommand } from "./cli/trace.ts";
import { triageCommand } from "./cli/triage.ts";

const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer);

const connectPublicConfigMissingMessage =
"T3 Connect commands are unavailable: this build is missing T3 Connect public configuration.";

class ConnectPublicConfigMissingError extends CliError.UserError {
override get message() {
return connectPublicConfigMissingMessage;
}
}

const connectUnavailableCommand = Command.make("connect", {
command: Argument.String("command").pipe(Argument.variadic),
}).pipe(
Command.withDescription("T3 Connect is unavailable in builds without public configuration."),
Command.unlisted,
Command.withHandler(() =>
Effect.fail(
new CliError.ShowHelp({
commandPath: ["t3", "connect"],
errors: [new ConnectPublicConfigMissingError({ cause: connectPublicConfigMissingMessage })],
}),
),
),
);

export const makeCli = ({ cloudEnabled = hasCloudPublicConfig } = {}) =>
Command.make("t3", { ...sharedServerCommandFlags }).pipe(
Command.withDescription("Run the T3 Code server."),
Command.withHandler((flags) => runServerCommand(flags)),
Command.withSubcommands([
acpMcpBridgeCommand,
acpMcpCallCommand,
startCommand,
serveCommand,
appCommand,
pairCommand,
authCommand,
projectCommand,
serviceCommand,
updateCommand,
uninstallCommand,
serviceLauncherCommand,
claudeHistoryCommand,
servicePreflightCommand,
sshHelperCommand,
themeCommand,
traceCommand,
triageCommand,
cloudEnabled ? connectCommand : connectUnavailableCommand,
]),
);

export const cli = makeCli();

export function runCli() {
Command.run(cli, { version: packageJson.version }).pipe(
Effect.scoped,
Effect.provide(CliRuntimeLayer),
NodeRuntime.runMain,
);
}
34 changes: 34 additions & 0 deletions apps/server/src/cli/acpMcpBridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as Effect from "effect/Effect";
import { Argument, Command } from "effect/unstable/cli";

import { runAcpMcpCliFastPath } from "../mcp/AcpMcpStdioBridge.ts";

/**
* `t3 acp-mcp-bridge` — internal stdio MCP server that ACP agents spawn.
*
* The T3 server injects this command (with per-session endpoint and
* credential environment variables) into `session/new` so every ACP agent
* reaches the t3-code toolkit through ACP's required stdio MCP transport.
* The credential stays in the environment, never on the command line.
*
* Real invocations dispatch through the bin.ts fast path before the CLI
* graph loads; these definitions keep the commands wired for help and for
* anything that drives the full CLI programmatically.
*/
export const acpMcpBridgeCommand = Command.make("acp-mcp-bridge").pipe(
Command.withDescription("Bridge T3 Code's MCP endpoint to stdio for ACP agents."),
Command.unlisted,
Command.withHandler(() => Effect.promise(() => runAcpMcpCliFastPath("acp-mcp-bridge", []))),
);

/** Terminal fallback for ACP agents that do not expose injected MCP servers. */
export const acpMcpCallCommand = Command.make("acp-mcp-call", {
tool: Argument.String("tool"),
argumentsJson: Argument.String("arguments-json"),
}).pipe(
Command.withDescription("Call one T3 Code MCP tool from an ACP agent terminal."),
Command.unlisted,
Command.withHandler(({ tool, argumentsJson }) =>
Effect.promise(() => runAcpMcpCliFastPath("acp-mcp-call", [tool, argumentsJson])),
),
);
2 changes: 1 addition & 1 deletion apps/server/src/cli/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import * as Layer from "effect/Layer";
import { Command } from "effect/unstable/cli";
import { afterEach, describe, expect, vi } from "vite-plus/test";

import { makeCli } from "../bin.ts";
import { makeCli } from "../binCli.ts";

vi.mock("node:os", async (importOriginal) => {
const os = await importOriginal<typeof import("node:os")>();
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/cli/pair.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import * as Layer from "effect/Layer";
import * as TestConsole from "effect/testing/TestConsole";
import { Command } from "effect/unstable/cli";

import { cli } from "../bin.ts";
import { cli } from "../binCli.ts";
import {
SERVICE_LAUNCHER_CONTEXT_ENV,
SERVICE_LAUNCHER_PROTOCOL,
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/cli/theme.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import * as Layer from "effect/Layer";
import * as TestConsole from "effect/testing/TestConsole";
import { Command } from "effect/unstable/cli";

import { cli } from "../bin.ts";
import { cli } from "../binCli.ts";
import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";

Expand Down
Loading
Loading