Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
43545c1
Add command executor MCP server
steveluc Jan 17, 2026
bf20673
Add copyright header to view-logs.sh
steveluc Jan 17, 2026
7c2a81a
Fix repo policy issues
steveluc Jan 17, 2026
82a1910
Run prettier to format files
steveluc Jan 17, 2026
ea66752
Fix repo policy issues - trademark format and package.json script order
steveluc Jan 17, 2026
e4672de
Enhance VSCode split editor functionality
steveluc Jan 19, 2026
81d7d0c
Merge main into branch and resolve conflicts
steveluc Jan 19, 2026
7cf1b80
Fix prettier formatting for README and VSCODE_CAPABILITIES
steveluc Jan 19, 2026
0ebe52c
Add support for creating playlists with song lists in player agent
steveluc Jan 19, 2026
4d47a68
Merge remote-tracking branch 'origin/main' into add-command-executor-…
steveluc Jan 19, 2026
3938cc6
Add coderWrapper package for CLI assistant PTY wrapper
steveluc Jan 19, 2026
b88be7b
Fix prettier formatting for coderWrapper package
steveluc Jan 19, 2026
ec83de6
Merge remote-tracking branch 'origin/main' into add-command-executor-…
steveluc Jan 19, 2026
4c5b90d
Update pnpm-lock.yaml for coderWrapper package
steveluc Jan 19, 2026
cfa7c49
Add copyright header to test-wrapper.js
steveluc Jan 19, 2026
ccd621c
Add trademark section to README and copyright header to pnpm-lock.yaml
steveluc Jan 19, 2026
c9179ee
Complete trademark section with third-party policy
steveluc Jan 19, 2026
f2e77f2
Add cache support to coderWrapper with shared dispatcher
steveluc Jan 20, 2026
b0010a9
Fix prettier formatting
steveluc Jan 20, 2026
7618fa3
Merge remote-tracking branch 'origin/main' into add-command-executor-…
steveluc Jan 20, 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
185 changes: 127 additions & 58 deletions ts/packages/agentServer/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { createWebSocketChannelServer } from "websocket-channel-server";
import { createDispatcherRpcServer } from "@typeagent/dispatcher-rpc/dispatcher/server";
import { ClientIO, createDispatcher, RequestId } from "agent-dispatcher";
import { ClientIO, createDispatcher } from "agent-dispatcher";
import { getInstanceDir, getClientId } from "agent-dispatcher/helpers/data";
import {
getDefaultAppAgentProviders,
Expand All @@ -14,46 +14,94 @@ import { getFsStorageProvider } from "dispatcher-node-providers";
import { ChannelProvider } from "@typeagent/agent-rpc/channel";
import { createClientIORpcClient } from "@typeagent/dispatcher-rpc/clientio/client";
import { createRpc } from "@typeagent/agent-rpc/rpc";
import { createPromiseWithResolvers } from "@typeagent/common-utils";
import {
AgentServerInvokeFunctions,
ChannelName,
} from "@typeagent/agent-server-protocol";
import { AsyncLocalStorage } from "async_hooks";
import dotenv from "dotenv";
const envPath = new URL("../../../../.env", import.meta.url);
dotenv.config({ path: envPath });

const nullClientIO: ClientIO = {
clear: () => {},
exit: () => {},
setDisplayInfo: () => {},
setDisplay: () => {},
appendDisplay: () => {},
appendDiagnosticData: () => {},
setDynamicDisplay: () => {},
askYesNo: async (
message: string,
requestId: RequestId,
defaultValue: boolean = false,
) => defaultValue,
proposeAction: async () => undefined,
popupQuestion: async () => {
throw new Error("popupQuestion not implemented");
},
notify: () => {},
openLocalView: () => {},
closeLocalView: () => {},
takeAction: (action: string) => {
throw new Error(`Action ${action} not supported`);
},
};
// AsyncLocalStorage to track which client is making the current request
const currentClientContext = new AsyncLocalStorage<ClientIO>();

async function main() {
const clientIO = {
...nullClientIO,
};
const instanceDir = getInstanceDir();

// Track all connected clients and their ClientIO
const connectedClients = new Map<
ChannelProvider,
{ clientIO: ClientIO; closeFn: () => void }
>();

// Create a routing ClientIO that forwards calls to the current request's client
const routingClientIO: ClientIO = {
clear: (...args) => {
const client = currentClientContext.getStore();
client?.clear?.(...args);
},
exit: (...args) => {
const client = currentClientContext.getStore();
client?.exit?.(...args);
},
setDisplayInfo: (...args) => {
const client = currentClientContext.getStore();
client?.setDisplayInfo?.(...args);
},
setDisplay: (...args) => {
const client = currentClientContext.getStore();
client?.setDisplay?.(...args);
},
appendDisplay: (...args) => {
const client = currentClientContext.getStore();
client?.appendDisplay?.(...args);
},
appendDiagnosticData: (...args) => {
const client = currentClientContext.getStore();
client?.appendDiagnosticData?.(...args);
},
setDynamicDisplay: (...args) => {
const client = currentClientContext.getStore();
client?.setDynamicDisplay?.(...args);
},
askYesNo: async (...args) => {
const client = currentClientContext.getStore();
return client?.askYesNo?.(...args) ?? false;
},
proposeAction: async (...args) => {
const client = currentClientContext.getStore();
return client?.proposeAction?.(...args);
},
popupQuestion: async (...args) => {
const client = currentClientContext.getStore();
if (!client?.popupQuestion) {
throw new Error("popupQuestion not implemented");
}
return client.popupQuestion(...args);
},
notify: (...args) => {
const client = currentClientContext.getStore();
client?.notify?.(...args);
},
openLocalView: (...args) => {
const client = currentClientContext.getStore();
client?.openLocalView?.(...args);
},
closeLocalView: (...args) => {
const client = currentClientContext.getStore();
client?.closeLocalView?.(...args);
},
takeAction: (action: string, data?: unknown) => {
const client = currentClientContext.getStore();
if (!client?.takeAction) {
throw new Error(`Action ${action} not supported`);
}
return client.takeAction(action, data);
},
};

// Create single shared dispatcher with routing ClientIO
const dispatcher = await createDispatcher("agent server", {
appAgentProviders: getDefaultAppAgentProviders(instanceDir),
persistSession: true,
Expand All @@ -62,7 +110,7 @@ async function main() {
metrics: true,
dblogging: false,
clientId: getClientId(),
clientIO,
clientIO: routingClientIO,
indexingServiceRegistry: await getIndexingServiceRegistry(instanceDir),
constructionProvider: getDefaultConstructionProvider(),
conversationMemorySettings: {
Expand All @@ -74,39 +122,15 @@ async function main() {
// Ignore dispatcher close requests
dispatcher.close = async () => {};

let currentChannelProvider: ChannelProvider | undefined;
let currentCloseFn: (() => void) | undefined;
await createWebSocketChannelServer(
{ port: 8999 },
(channelProvider, closeFn) => {
const invokeFunctions: AgentServerInvokeFunctions = {
join: async () => {
if (currentChannelProvider !== undefined) {
if (channelProvider === currentChannelProvider) {
throw new Error("Already joined");
}

const promiseWithResolvers =
createPromiseWithResolvers<void>();
currentChannelProvider.on("disconnect", () => {
promiseWithResolvers.resolve();
});
currentCloseFn!();
await promiseWithResolvers.promise;
}

if (currentChannelProvider) {
throw new Error("Unable to disconnect");
if (connectedClients.has(channelProvider)) {
throw new Error("Already joined");
}

currentChannelProvider = channelProvider;
currentCloseFn = closeFn;
channelProvider.on("disconnect", () => {
currentChannelProvider = undefined;
currentCloseFn = undefined;
Object.assign(clientIO, nullClientIO);
});

const dispatcherChannel = channelProvider.createChannel(
ChannelName.Dispatcher,
);
Expand All @@ -115,8 +139,53 @@ async function main() {
);
const clientIORpcClient =
createClientIORpcClient(clientIOChannel);
Object.assign(clientIO, clientIORpcClient);
createDispatcherRpcServer(dispatcher, dispatcherChannel);

// Store this client's ClientIO
connectedClients.set(channelProvider, {
clientIO: clientIORpcClient,
closeFn,
});

channelProvider.on("disconnect", () => {
connectedClients.delete(channelProvider);
console.log(
`Client disconnected. Active connections: ${connectedClients.size}`,
);
});

// Wrap the dispatcher RPC server to set context for each request
const wrappedDispatcher = {
...dispatcher,
processCommand: async (
command: string,
requestId?: string,
attachments?: string[],
) => {
return currentClientContext.run(
clientIORpcClient,
() =>
dispatcher.processCommand(
command,
requestId,
attachments,
),
);
},
checkCache: async (request: string) => {
return currentClientContext.run(
clientIORpcClient,
() => dispatcher.checkCache(request),
);
},
};

createDispatcherRpcServer(
wrappedDispatcher as any,
dispatcherChannel,
);
console.log(
`Client connected. Active connections: ${connectedClients.size}`,
);
},
};

Expand Down
45 changes: 38 additions & 7 deletions ts/packages/coderWrapper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ coder-wrapper [options]

Options:
-a, --assistant <name> Specify the assistant to use (default: claude)
-d, --debug Enable debug logging with cache timing information
-h, --help Show this help message
```

Expand All @@ -49,14 +50,38 @@ coder-wrapper

# Explicitly specify Claude
coder-wrapper -a claude

# Enable debug mode to see cache hit/miss timing
coder-wrapper --debug
```

## How It Works

1. **PTY Wrapper**: Uses `node-pty` to spawn the assistant in a pseudo terminal
2. **Transparent I/O**: All stdin/stdout/stderr is passed through unchanged
3. **Terminal Features**: Supports colors, cursor control, and terminal resizing
4. **Clean Exit**: Handles Ctrl+C and process termination gracefully
2. **Cache Checking**: Intercepts user input and checks TypeAgent cache before forwarding to assistant
3. **Cache Hit**: Executes cached actions and returns results immediately (bypasses assistant)
4. **Cache Miss**: Forwards input to the assistant normally
5. **Transparent I/O**: All stdin/stdout/stderr is passed through unchanged
6. **Terminal Features**: Supports colors, cursor control, and terminal resizing
7. **Clean Exit**: Handles Ctrl+C and process termination gracefully

### Debug Mode

When `--debug` is enabled, the wrapper logs:

- Cache check attempts with command text
- Cache hit/miss status with timing (in milliseconds)
- Whether request was forwarded to assistant
- Total time for cache hits

Example debug output:

```
[CoderWrapper:Debug] Checking cache for: "play hello by adele"
[CoderWrapper:Debug] ✓ Cache HIT (234.56ms)
[Action result displayed here]
[CoderWrapper:Debug] Command completed from cache in 234.56ms
```

## Architecture

Expand Down Expand Up @@ -96,13 +121,19 @@ export const ASSISTANT_CONFIGS: Record<string, AssistantConfig> = {
};
```

## Features

- [x] Cache checking before forwarding to assistant
- [x] Debug mode with timing metrics for cache operations
- [x] Transparent PTY passthrough
- [x] Support for multiple CLI assistants

## Future Enhancements

- [ ] Cache checking before forwarding to assistant
- [ ] Request/response logging
- [ ] Performance metrics
- [ ] Cache hit/miss statistics
- [ ] Request/response logging to file
- [ ] Cumulative cache hit/miss statistics
- [ ] Support for intercepting and modifying requests
- [ ] Configuration file support

## Development

Expand Down
1 change: 1 addition & 0 deletions ts/packages/coderWrapper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"tsc": "tsc -b"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4",
"@typeagent/agent-server-client": "workspace:*",
"@typeagent/dispatcher-types": "workspace:*",
"node-pty": "^1.0.0"
Expand Down
Loading
Loading