Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions nodejs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions nodejs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"zod": "^4.3.6"
},
"devDependencies": {
"@platformatic/vfs": "^0.3.0",
"@types/node": "^25.2.0",
"@typescript-eslint/eslint-plugin": "^8.54.0",
"@typescript-eslint/parser": "^8.54.0",
Expand Down
27 changes: 25 additions & 2 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
StreamMessageReader,
StreamMessageWriter,
} from "vscode-jsonrpc/node.js";
import { createServerRpc } from "./generated/rpc.js";
import { createServerRpc, registerClientApiHandlers } from "./generated/rpc.js";
import { getSdkProtocolVersion } from "./sdkProtocolVersion.js";
import { CopilotSession, NO_RESULT_PERMISSION_V2_ERROR } from "./session.js";
import { getTraceContext } from "./telemetry.js";
Expand All @@ -46,6 +46,7 @@ import type {
SessionListFilter,
SessionMetadata,
SystemMessageCustomizeConfig,
SessionFsConfig,
TelemetryConfig,
Tool,
ToolCallRequestPayload,
Expand Down Expand Up @@ -216,6 +217,7 @@ export class CopilotClient {
| "onListModels"
| "telemetry"
| "onGetTraceContext"
| "sessionFs"
>
> & {
cliPath?: string;
Expand All @@ -238,6 +240,8 @@ export class CopilotClient {
private _rpc: ReturnType<typeof createServerRpc> | null = null;
private processExitPromise: Promise<never> | null = null; // Rejects when CLI process exits
private negotiatedProtocolVersion: number | null = null;
/** Connection-level session filesystem config, set via constructor option. */
private sessionFsConfig: SessionFsConfig | null = null;

/**
* Typed server-scoped RPC methods.
Expand Down Expand Up @@ -307,6 +311,7 @@ export class CopilotClient {

this.onListModels = options.onListModels;
this.onGetTraceContext = options.onGetTraceContext;
this.sessionFsConfig = options.sessionFs ?? null;

const effectiveEnv = options.env ?? process.env;
this.options = {
Expand Down Expand Up @@ -399,6 +404,15 @@ export class CopilotClient {
// Verify protocol version compatibility
await this.verifyProtocolVersion();

// If a session filesystem provider was configured, register it
if (this.sessionFsConfig) {
await this.connection!.sendRequest("sessionFs.setProvider", {
initialCwd: this.sessionFsConfig.initialCwd,
sessionStatePath: this.sessionFsConfig.sessionStatePath,
conventions: this.sessionFsConfig.conventions,
});
}

this.state = "connected";
} catch (error) {
this.state = "error";
Expand Down Expand Up @@ -1069,7 +1083,9 @@ export class CopilotClient {
throw new Error("Client not connected");
}

const response = await this.connection.sendRequest("session.list", { filter });
const response = await this.connection.sendRequest("session.list", {
filter,
});
const { sessions } = response as {
sessions: Array<{
sessionId: string;
Expand Down Expand Up @@ -1562,6 +1578,13 @@ export class CopilotClient {
await this.handleSystemMessageTransform(params)
);

// Register session filesystem RPC handlers if configured.
if (this.sessionFsConfig) {
registerClientApiHandlers(this.connection, {
sessionFs: this.sessionFsConfig,
});
}

this.connection.onClose(() => {
this.state = "disconnected";
});
Expand Down
210 changes: 210 additions & 0 deletions nodejs/src/generated/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,28 @@ export interface AccountGetQuotaResult {
};
}

export interface SessionFsSetProviderResult {
/**
* Whether the provider was set successfully
*/
success: boolean;
}

export interface SessionFsSetProviderParams {
/**
* Initial working directory for sessions
*/
initialCwd: string;
/**
* Path within each session's SessionFs where the runtime stores files for that session
*/
sessionStatePath: string;
/**
* Path conventions used by this filesystem
*/
conventions: "windows" | "linux";
}

export interface SessionModelGetCurrentResult {
/**
* Currently active model identifier
Expand Down Expand Up @@ -1050,6 +1072,145 @@ export interface SessionShellKillParams {
signal?: "SIGTERM" | "SIGKILL" | "SIGINT";
}

export interface SessionFsReadFileResult {
/**
* File content as UTF-8 string
*/
content: string;
}

export interface SessionFsReadFileParams {
/**
* Target session identifier
*/
sessionId: string;
/**
* Path using SessionFs conventions
*/
path: string;
}

export interface SessionFsWriteFileParams {
/**
* Target session identifier
*/
sessionId: string;
/**
* Path using SessionFs conventions
*/
path: string;
/**
* Content to write
*/
content: string;
/**
* Optional POSIX-style mode for newly created files
*/
mode?: number;
}

export interface SessionFsAppendFileParams {
/**
* Target session identifier
*/
sessionId: string;
/**
* Path using SessionFs conventions
*/
path: string;
/**
* Content to append
*/
content: string;
/**
* Optional POSIX-style mode for newly created files
*/
mode?: number;
}

export interface SessionFsExistsResult {
exists: boolean;
}

export interface SessionFsExistsParams {
/**
* Target session identifier
*/
sessionId: string;
/**
* Path using SessionFs conventions
*/
path: string;
}

export interface SessionFsStatResult {
isFile: boolean;
isDirectory: boolean;
size: number;
/**
* ISO 8601 timestamp of last modification
*/
mtime: string;
/**
* ISO 8601 timestamp of creation
*/
birthtime: string;
}

export interface SessionFsStatParams {
/**
* Target session identifier
*/
sessionId: string;
/**
* Path using SessionFs conventions
*/
path: string;
}

export interface SessionFsMkdirParams {
/**
* Target session identifier
*/
sessionId: string;
path: string;
recursive?: boolean;
}

export interface SessionFsReaddirResult {
/**
* Entry names in the directory
*/
entries: string[];
}

export interface SessionFsReaddirParams {
/**
* Target session identifier
*/
sessionId: string;
path: string;
}

export interface SessionFsRmParams {
/**
* Target session identifier
*/
sessionId: string;
path: string;
recursive?: boolean;
force?: boolean;
}

export interface SessionFsRenameParams {
/**
* Target session identifier
*/
sessionId: string;
src: string;
dest: string;
}

/** Create typed server-scoped RPC methods (no session required). */
export function createServerRpc(connection: MessageConnection) {
return {
Expand All @@ -1067,6 +1228,10 @@ export function createServerRpc(connection: MessageConnection) {
getQuota: async (): Promise<AccountGetQuotaResult> =>
connection.sendRequest("account.getQuota", {}),
},
sessionFs: {
setProvider: async (params: SessionFsSetProviderParams): Promise<SessionFsSetProviderResult> =>
connection.sendRequest("sessionFs.setProvider", params),
},
};
}

Expand Down Expand Up @@ -1188,3 +1353,48 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin
},
};
}

/**
* Handler interface for the `sessionFs` client API group.
* Implement this to provide a custom sessionFs backend.
*/
export interface SessionFsHandler {
readFile(params: SessionFsReadFileParams): Promise<SessionFsReadFileResult>;
writeFile(params: SessionFsWriteFileParams): Promise<void>;
appendFile(params: SessionFsAppendFileParams): Promise<void>;
exists(params: SessionFsExistsParams): Promise<SessionFsExistsResult>;
stat(params: SessionFsStatParams): Promise<SessionFsStatResult>;
mkdir(params: SessionFsMkdirParams): Promise<void>;
readdir(params: SessionFsReaddirParams): Promise<SessionFsReaddirResult>;
rm(params: SessionFsRmParams): Promise<void>;
rename(params: SessionFsRenameParams): Promise<void>;
}

/** All client API handler groups. Each group is optional. */
export interface ClientApiHandlers {
sessionFs?: SessionFsHandler;
}

/**
* Register client API handlers on a JSON-RPC connection.
* The server calls these methods to delegate work to the client.
* Methods for unregistered groups will respond with a standard JSON-RPC
* method-not-found error.
*/
export function registerClientApiHandlers(
connection: MessageConnection,
handlers: ClientApiHandlers,
): void {
if (handlers.sessionFs) {
const h = handlers.sessionFs!;
connection.onRequest("sessionFs.readFile", (params: SessionFsReadFileParams) => h.readFile(params));
connection.onRequest("sessionFs.writeFile", (params: SessionFsWriteFileParams) => h.writeFile(params));
connection.onRequest("sessionFs.appendFile", (params: SessionFsAppendFileParams) => h.appendFile(params));
connection.onRequest("sessionFs.exists", (params: SessionFsExistsParams) => h.exists(params));
connection.onRequest("sessionFs.stat", (params: SessionFsStatParams) => h.stat(params));
connection.onRequest("sessionFs.mkdir", (params: SessionFsMkdirParams) => h.mkdir(params));
connection.onRequest("sessionFs.readdir", (params: SessionFsReaddirParams) => h.readdir(params));
connection.onRequest("sessionFs.rm", (params: SessionFsRmParams) => h.rm(params));
connection.onRequest("sessionFs.rename", (params: SessionFsRenameParams) => h.rename(params));
}
}
4 changes: 4 additions & 0 deletions nodejs/src/generated/session-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ export type SessionEvent =
* Whether the session was already in use by another client at start time
*/
alreadyInUse?: boolean;
/**
* Whether this session supports remote steering via Mission Control
*/
steerable?: boolean;
};
}
| {
Expand Down
3 changes: 3 additions & 0 deletions nodejs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ export type {
SessionListFilter,
SessionMetadata,
SessionUiApi,
SessionFsConfig,
SessionFsHandler,
ClientApiHandlers,
SystemMessageAppendConfig,
SystemMessageConfig,
SystemMessageCustomizeConfig,
Expand Down
Loading
Loading