feat(cli): support extension installation for teleportation

This commit is contained in:
Sehoon Shon
2026-03-19 00:30:10 -04:00
parent 4f8534b223
commit 6d3e4764cc
3 changed files with 68 additions and 0 deletions

View File

@@ -51,6 +51,7 @@ import {
type HookDefinition,
type HookEventName,
type ResolvedExtensionSetting,
type TrajectoryProvider,
coreEvents,
applyAdminAllowlist,
getAdminBlockedMcpServersMessage,
@@ -957,6 +958,23 @@ Would you like to attempt to install via "git clone" instead?`,
);
}
let trajectoryProviderModule: TrajectoryProvider | undefined;
if (config.trajectoryProvider) {
try {
const expectedPath = path.resolve(
effectiveExtensionPath,
config.trajectoryProvider,
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
trajectoryProviderModule = (await import(expectedPath))
.default as TrajectoryProvider;
} catch (e) {
debugLogger.warn(
`Failed to import trajectoryProvider at ${config.trajectoryProvider} for extension ${config.name}: ${getErrorMessage(e)}`,
);
}
}
return {
name: config.name,
version: config.version,
@@ -980,6 +998,7 @@ Would you like to attempt to install via "git clone" instead?`,
rules,
checkers,
plan: config.plan,
trajectoryProviderModule,
};
} catch (e) {
const extName = path.basename(extensionDir);

View File

@@ -46,6 +46,11 @@ export interface ExtensionConfig {
* Used to migrate an extension to a new repository source.
*/
migratedTo?: string;
/**
* Path to a module that implements the TrajectoryProvider interface.
* Used for importing binary chat histories like Jetski Teleportation.
*/
trajectoryProvider?: string;
}
export interface ExtensionUpdateInfo {

View File

@@ -242,6 +242,50 @@ export abstract class ExtensionLoader {
await this.stopExtension(extension);
await this.startExtension(extension);
}
/**
* Returns the most recent session from all extensions if it's within the threshold.
*/
async getRecentExternalSession(
workspaceUri?: string,
thresholdMs: number = 10 * 60 * 1000,
): Promise<{ prefix: string; id: string; displayName?: string } | null> {
const activeExtensions = this.getExtensions().filter((e) => e.isActive);
let mostRecent: {
prefix: string;
id: string;
displayName?: string;
mtime: number;
} | null = null;
for (const extension of activeExtensions) {
if (extension.trajectoryProviderModule) {
try {
const sessions =
await extension.trajectoryProviderModule.listSessions(workspaceUri);
for (const s of sessions) {
const mtime = new Date(s.mtime).getTime();
if (!mostRecent || mtime > mostRecent.mtime) {
mostRecent = {
prefix: extension.trajectoryProviderModule.prefix || '',
id: s.id,
displayName: s.displayName,
mtime,
};
}
}
} catch (_e) {
// Ignore extension errors
}
}
}
if (mostRecent && Date.now() - mostRecent.mtime < thresholdMs) {
return mostRecent;
}
return null;
}
}
export interface ExtensionEvents {