linkshell-cli 0.2.89 → 0.2.90
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/dist/cli/src/index.js +1 -2
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-session.d.ts +3 -1
- package/dist/cli/src/runtime/acp/agent-session.js +44 -34
- package/dist/cli/src/runtime/acp/agent-session.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +7 -5
- package/dist/cli/src/runtime/acp/agent-workspace.js +96 -84
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/provider-resolver.d.ts +1 -0
- package/dist/cli/src/runtime/acp/provider-resolver.js +27 -0
- package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
- package/dist/cli/src/runtime/bridge-session.d.ts +1 -1
- package/dist/cli/src/runtime/bridge-session.js +18 -10
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -2
- package/src/runtime/acp/agent-session.ts +45 -35
- package/src/runtime/acp/agent-workspace.ts +97 -84
- package/src/runtime/acp/provider-resolver.ts +29 -0
- package/src/runtime/bridge-session.ts +17 -12
|
@@ -235,6 +235,7 @@ function summarizeFileChanges(changes: unknown[]): string | undefined {
|
|
|
235
235
|
|
|
236
236
|
export class AgentSessionProxy {
|
|
237
237
|
private client: AcpClient | undefined;
|
|
238
|
+
private activeProvider: AgentProvider | undefined;
|
|
238
239
|
private agentSessionId: string | undefined;
|
|
239
240
|
private status: AgentStatus = "unavailable";
|
|
240
241
|
private error: string | undefined;
|
|
@@ -253,7 +254,7 @@ export class AgentSessionProxy {
|
|
|
253
254
|
private readonly input: {
|
|
254
255
|
sessionId: string;
|
|
255
256
|
cwd: string;
|
|
256
|
-
|
|
257
|
+
availableProviders: AgentProvider[];
|
|
257
258
|
command?: string;
|
|
258
259
|
send: (envelope: Envelope) => void;
|
|
259
260
|
verbose?: boolean;
|
|
@@ -347,45 +348,53 @@ export class AgentSessionProxy {
|
|
|
347
348
|
this.sendCapabilities();
|
|
348
349
|
return;
|
|
349
350
|
}
|
|
350
|
-
await this.
|
|
351
|
+
await this.tryStartFirstAvailable();
|
|
351
352
|
}
|
|
352
353
|
|
|
353
|
-
private async
|
|
354
|
+
private async tryStartFirstAvailable(): Promise<void> {
|
|
354
355
|
if (this.client) return;
|
|
355
356
|
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (!resolved) {
|
|
361
|
-
this.status = "unavailable";
|
|
362
|
-
this.error = `Agent GUI requires --agent-command for ${this.input.provider}`;
|
|
363
|
-
this.sendCapabilities();
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
try {
|
|
368
|
-
this.client = new AcpClient({
|
|
369
|
-
command: resolved.command,
|
|
370
|
-
protocol: resolved.protocol,
|
|
371
|
-
framing: resolved.framing,
|
|
372
|
-
cwd: this.input.cwd,
|
|
373
|
-
onNotification: (method, params) => this.handleNotification(method, params),
|
|
374
|
-
onRequest: (method, params) => this.handleRequest(method, params),
|
|
375
|
-
onExit: (message) => this.handleExit(message),
|
|
357
|
+
for (const provider of this.input.availableProviders) {
|
|
358
|
+
const resolved = resolveAgentCommand({
|
|
359
|
+
provider,
|
|
360
|
+
command: this.input.command,
|
|
376
361
|
});
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
362
|
+
if (!resolved) continue;
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
this.client = new AcpClient({
|
|
366
|
+
command: resolved.command,
|
|
367
|
+
protocol: resolved.protocol,
|
|
368
|
+
framing: resolved.framing,
|
|
369
|
+
cwd: this.input.cwd,
|
|
370
|
+
onNotification: (method, params) => this.handleNotification(method, params),
|
|
371
|
+
onRequest: (method, params) => this.handleRequest(method, params),
|
|
372
|
+
onExit: (message) => this.handleExit(message),
|
|
373
|
+
});
|
|
374
|
+
await this.client.initialize();
|
|
375
|
+
this.activeProvider = provider;
|
|
376
|
+
this.initialized = true;
|
|
377
|
+
this.status = "idle";
|
|
378
|
+
this.error = undefined;
|
|
379
|
+
this.sendCapabilities();
|
|
380
|
+
return;
|
|
381
|
+
} catch (error) {
|
|
382
|
+
this.client?.stop();
|
|
383
|
+
this.client = undefined;
|
|
384
|
+
if (this.input.verbose) {
|
|
385
|
+
process.stderr.write(`[agent] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
388
|
}
|
|
389
|
+
|
|
390
|
+
this.status = "unavailable";
|
|
391
|
+
this.error = "没有可用的 Agent provider。请安装 Claude Code 或 Codex CLI。";
|
|
392
|
+
this.sendCapabilities();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private async ensureClient(): Promise<void> {
|
|
396
|
+
if (this.client) return;
|
|
397
|
+
await this.tryStartFirstAvailable();
|
|
389
398
|
}
|
|
390
399
|
|
|
391
400
|
private async ensureSession(
|
|
@@ -950,12 +959,13 @@ export class AgentSessionProxy {
|
|
|
950
959
|
|
|
951
960
|
private sendCapabilities(): void {
|
|
952
961
|
const enabled = Boolean(this.client && this.initialized && !this.error);
|
|
962
|
+
const activeProvider = this.activeProvider ?? this.input.availableProviders[0];
|
|
953
963
|
this.input.send(createEnvelope({
|
|
954
964
|
type: "agent.capabilities",
|
|
955
965
|
sessionId: this.input.sessionId,
|
|
956
966
|
payload: {
|
|
957
967
|
enabled,
|
|
958
|
-
provider:
|
|
968
|
+
provider: activeProvider ?? "codex",
|
|
959
969
|
protocolVersion: 1,
|
|
960
970
|
error: enabled ? undefined : this.error,
|
|
961
971
|
supportsSessionList: enabled,
|
|
@@ -656,21 +656,9 @@ function providerLabel(provider: AgentProvider): string {
|
|
|
656
656
|
return "Custom";
|
|
657
657
|
}
|
|
658
658
|
|
|
659
|
-
function providerSetupReason(provider: AgentProvider, activeProvider: AgentProvider, error?: string): string {
|
|
660
|
-
if (provider === activeProvider) {
|
|
661
|
-
return error ?? `${providerLabel(provider)} Agent 正在初始化或不可用。`;
|
|
662
|
-
}
|
|
663
|
-
if (provider === "codex") {
|
|
664
|
-
return `当前 CLI 启用的是 ${providerLabel(activeProvider)} Agent。`;
|
|
665
|
-
}
|
|
666
|
-
if (provider === "claude") {
|
|
667
|
-
return "Claude ACP adapter 尚未启用,请用 --agent-provider claude --agent-command 配置。";
|
|
668
|
-
}
|
|
669
|
-
return "Custom Agent 需要用 --agent-provider custom --agent-command 配置后才能使用。";
|
|
670
|
-
}
|
|
671
|
-
|
|
672
659
|
export class AgentWorkspaceProxy {
|
|
673
|
-
private
|
|
660
|
+
private clients = new Map<AgentProvider, AcpClient>();
|
|
661
|
+
private agentProtocols = new Map<AgentProvider, AgentProtocol>();
|
|
674
662
|
private initialized = false;
|
|
675
663
|
private status: AgentStatus = "unavailable";
|
|
676
664
|
private error: string | undefined;
|
|
@@ -686,13 +674,12 @@ export class AgentWorkspaceProxy {
|
|
|
686
674
|
private pendingStructuredInputs = new Map<string, { conversationId: string; input: AgentStructuredInput }>();
|
|
687
675
|
private structuredInputWaiters = new Map<string, PendingStructuredInputWaiter>();
|
|
688
676
|
private toolConversationIds = new Map<string, string>();
|
|
689
|
-
private agentProtocol: AgentProtocol | undefined;
|
|
690
677
|
|
|
691
678
|
constructor(
|
|
692
679
|
private readonly input: {
|
|
693
680
|
sessionId: string;
|
|
694
681
|
cwd: string;
|
|
695
|
-
|
|
682
|
+
availableProviders: AgentProvider[];
|
|
696
683
|
command?: string;
|
|
697
684
|
send: (envelope: Envelope) => void;
|
|
698
685
|
verbose?: boolean;
|
|
@@ -736,7 +723,8 @@ export class AgentWorkspaceProxy {
|
|
|
736
723
|
const payload = parseTypedPayload("agent.v2.cancel", envelope.payload);
|
|
737
724
|
const conversation = this.conversations.get(payload.conversationId);
|
|
738
725
|
this.cancelPendingPermissions(payload.conversationId);
|
|
739
|
-
this.
|
|
726
|
+
const cancelClient = conversation ? this.clientForProvider(conversation.provider) : undefined;
|
|
727
|
+
cancelClient?.cancel({
|
|
740
728
|
sessionId: conversation?.agentSessionId,
|
|
741
729
|
turnId: this.currentTurnId,
|
|
742
730
|
});
|
|
@@ -759,89 +747,104 @@ export class AgentWorkspaceProxy {
|
|
|
759
747
|
}
|
|
760
748
|
|
|
761
749
|
stop(): void {
|
|
762
|
-
this.
|
|
763
|
-
|
|
750
|
+
for (const client of this.clients.values()) {
|
|
751
|
+
client.stop();
|
|
752
|
+
}
|
|
753
|
+
this.clients.clear();
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
private clientForProvider(provider: AgentProvider): AcpClient | undefined {
|
|
757
|
+
return this.clients.get(provider);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
private protocolForProvider(provider: AgentProvider): AgentProtocol | undefined {
|
|
761
|
+
return this.agentProtocols.get(provider);
|
|
764
762
|
}
|
|
765
763
|
|
|
766
764
|
private async initialize(): Promise<void> {
|
|
767
765
|
if (this.initialized) return;
|
|
768
|
-
|
|
766
|
+
// trigger capability report immediately, lazy-start providers on first use
|
|
767
|
+
this.initialized = true;
|
|
768
|
+
this.status = "idle";
|
|
769
|
+
this.error = undefined;
|
|
770
|
+
this.sendCapabilities();
|
|
769
771
|
}
|
|
770
772
|
|
|
771
|
-
private async
|
|
772
|
-
|
|
773
|
+
private async ensureProviderClient(provider: AgentProvider): Promise<AcpClient | undefined> {
|
|
774
|
+
const existing = this.clients.get(provider);
|
|
775
|
+
if (existing) return existing;
|
|
773
776
|
|
|
774
777
|
const resolved = resolveAgentCommand({
|
|
775
|
-
provider
|
|
778
|
+
provider,
|
|
776
779
|
command: this.input.command,
|
|
777
780
|
});
|
|
778
781
|
if (!resolved) {
|
|
779
|
-
this.
|
|
780
|
-
|
|
781
|
-
|
|
782
|
+
if (this.input.verbose) {
|
|
783
|
+
process.stderr.write(`[agent:v2] no command for provider ${provider}\n`);
|
|
784
|
+
}
|
|
785
|
+
return undefined;
|
|
782
786
|
}
|
|
783
787
|
|
|
784
788
|
try {
|
|
785
|
-
this.
|
|
786
|
-
|
|
789
|
+
this.agentProtocols.set(provider, resolved.protocol);
|
|
790
|
+
const client = new AcpClient({
|
|
787
791
|
command: resolved.command,
|
|
788
792
|
protocol: resolved.protocol,
|
|
789
793
|
framing: resolved.framing,
|
|
790
794
|
cwd: this.input.cwd,
|
|
791
795
|
onNotification: (method, params) => this.handleNotification(method, params),
|
|
792
796
|
onRequest: (method, params) => this.handleRequest(method, params),
|
|
793
|
-
onExit: (message) => this.
|
|
797
|
+
onExit: (message) => this.handleProviderExit(provider, message),
|
|
794
798
|
});
|
|
795
|
-
await
|
|
796
|
-
this.
|
|
799
|
+
await client.initialize();
|
|
800
|
+
this.clients.set(provider, client);
|
|
797
801
|
this.status = "idle";
|
|
798
802
|
this.error = undefined;
|
|
803
|
+
this.sendCapabilities();
|
|
804
|
+
return client;
|
|
799
805
|
} catch (error) {
|
|
800
|
-
this.
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
806
|
+
if (this.input.verbose) {
|
|
807
|
+
process.stderr.write(`[agent:v2] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
808
|
+
}
|
|
809
|
+
return undefined;
|
|
804
810
|
}
|
|
805
811
|
}
|
|
806
812
|
|
|
807
813
|
private sendCapabilities(): void {
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
814
|
+
const providers = this.input.availableProviders.map((provider) => {
|
|
815
|
+
const client = this.clients.get(provider);
|
|
816
|
+
const protocol = this.agentProtocols.get(provider);
|
|
817
|
+
const enabled = Boolean(client);
|
|
818
|
+
const supportsImages = enabled && protocol === "codex-app-server";
|
|
819
|
+
return {
|
|
820
|
+
id: provider,
|
|
821
|
+
label: providerLabel(provider),
|
|
822
|
+
enabled,
|
|
823
|
+
reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
|
|
824
|
+
supportsImages,
|
|
825
|
+
supportsPermission: enabled,
|
|
826
|
+
supportsPlan: enabled,
|
|
827
|
+
supportsCancel: enabled,
|
|
828
|
+
};
|
|
829
|
+
});
|
|
830
|
+
const anyEnabled = providers.some((p) => p.enabled);
|
|
813
831
|
this.input.send(createEnvelope({
|
|
814
832
|
type: "agent.v2.capabilities",
|
|
815
833
|
sessionId: this.input.sessionId,
|
|
816
834
|
payload: {
|
|
817
|
-
enabled,
|
|
818
|
-
provider:
|
|
819
|
-
providers
|
|
820
|
-
const isActive = provider === activeProvider;
|
|
821
|
-
const canUse = isActive && enabled;
|
|
822
|
-
return {
|
|
823
|
-
id: provider,
|
|
824
|
-
label: providerLabel(provider),
|
|
825
|
-
enabled: canUse,
|
|
826
|
-
reason: canUse
|
|
827
|
-
? undefined
|
|
828
|
-
: providerSetupReason(provider, activeProvider, isActive ? this.error : undefined),
|
|
829
|
-
supportsImages: canUse && supportsImages,
|
|
830
|
-
supportsPermission: canUse,
|
|
831
|
-
supportsPlan: canUse,
|
|
832
|
-
supportsCancel: canUse,
|
|
833
|
-
};
|
|
834
|
-
}),
|
|
835
|
+
enabled: anyEnabled,
|
|
836
|
+
provider: this.input.availableProviders[0] ?? "codex",
|
|
837
|
+
providers,
|
|
835
838
|
protocolVersion: 1,
|
|
836
839
|
workspaceProtocolVersion: 2,
|
|
837
|
-
error:
|
|
838
|
-
supportsSessionList:
|
|
839
|
-
supportsSessionLoad:
|
|
840
|
-
supportsImages,
|
|
840
|
+
error: anyEnabled ? undefined : "没有可用的 Agent provider。请安装 Claude Code 或 Codex CLI。",
|
|
841
|
+
supportsSessionList: anyEnabled,
|
|
842
|
+
supportsSessionLoad: anyEnabled,
|
|
843
|
+
supportsImages: providers.some((p) => p.supportsImages),
|
|
841
844
|
supportsAudio: false,
|
|
842
|
-
supportsPermission:
|
|
843
|
-
supportsPlan:
|
|
844
|
-
supportsCancel:
|
|
845
|
+
supportsPermission: anyEnabled,
|
|
846
|
+
supportsPlan: anyEnabled,
|
|
847
|
+
supportsCancel: anyEnabled,
|
|
845
848
|
},
|
|
846
849
|
}));
|
|
847
850
|
}
|
|
@@ -856,18 +859,22 @@ export class AgentWorkspaceProxy {
|
|
|
856
859
|
permissionMode?: AgentPermissionMode;
|
|
857
860
|
title?: string;
|
|
858
861
|
}): Promise<AgentConversation | undefined> {
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
+
const provider = payload.provider ?? this.input.availableProviders[0];
|
|
863
|
+
if (!provider) {
|
|
864
|
+
return this.openFailure(payload, "没有可用的 Agent provider。");
|
|
865
|
+
}
|
|
866
|
+
if (!this.input.availableProviders.includes(provider)) {
|
|
862
867
|
return this.openFailure(
|
|
863
868
|
payload,
|
|
864
|
-
|
|
869
|
+
`${providerLabel(provider)} 未安装或不可用。`,
|
|
865
870
|
);
|
|
866
871
|
}
|
|
867
|
-
|
|
872
|
+
|
|
873
|
+
const client = await this.ensureProviderClient(provider);
|
|
874
|
+
if (!client) {
|
|
868
875
|
return this.openFailure(
|
|
869
876
|
payload,
|
|
870
|
-
|
|
877
|
+
`${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`,
|
|
871
878
|
);
|
|
872
879
|
}
|
|
873
880
|
|
|
@@ -895,8 +902,8 @@ export class AgentWorkspaceProxy {
|
|
|
895
902
|
|
|
896
903
|
try {
|
|
897
904
|
const result = agentSessionId
|
|
898
|
-
? await
|
|
899
|
-
: await
|
|
905
|
+
? await client.loadSession({ sessionId: agentSessionId, cwd })
|
|
906
|
+
: await client.newSession({ cwd });
|
|
900
907
|
agentSessionId = this.extractSessionId(result) ?? agentSessionId ?? id("agent-session");
|
|
901
908
|
const now = Date.now();
|
|
902
909
|
const conversationId = payload.conversationId ?? `agent:${agentSessionId}`;
|
|
@@ -904,7 +911,7 @@ export class AgentWorkspaceProxy {
|
|
|
904
911
|
...existingConversation,
|
|
905
912
|
id: conversationId,
|
|
906
913
|
agentSessionId,
|
|
907
|
-
provider
|
|
914
|
+
provider,
|
|
908
915
|
cwd,
|
|
909
916
|
title: payload.title ?? existingConversation?.title ?? titleFromCwd(cwd),
|
|
910
917
|
model: payload.model ?? existingConversation?.model,
|
|
@@ -949,7 +956,7 @@ export class AgentWorkspaceProxy {
|
|
|
949
956
|
const now = Date.now();
|
|
950
957
|
const conversation: AgentConversation = {
|
|
951
958
|
id: fallbackId,
|
|
952
|
-
provider: payload.provider ?? this.input.
|
|
959
|
+
provider: payload.provider ?? this.input.availableProviders[0] ?? "codex",
|
|
953
960
|
cwd,
|
|
954
961
|
title: payload.title ?? titleFromCwd(cwd),
|
|
955
962
|
model: payload.model,
|
|
@@ -989,9 +996,12 @@ export class AgentWorkspaceProxy {
|
|
|
989
996
|
const conversation =
|
|
990
997
|
this.conversations.get(payload.conversationId) ??
|
|
991
998
|
await this.openConversation({ conversationId: payload.conversationId });
|
|
992
|
-
if (!conversation || !
|
|
999
|
+
if (!conversation || !conversation.agentSessionId) return;
|
|
1000
|
+
const client = this.clientForProvider(conversation.provider);
|
|
1001
|
+
if (!client) return;
|
|
993
1002
|
|
|
994
|
-
|
|
1003
|
+
const protocol = this.protocolForProvider(conversation.provider);
|
|
1004
|
+
if (payload.contentBlocks.some((block) => block.type === "image") && protocol !== "codex-app-server") {
|
|
995
1005
|
conversation.status = "idle";
|
|
996
1006
|
conversation.lastActivityAt = Date.now();
|
|
997
1007
|
this.emitConversation(conversation);
|
|
@@ -1025,7 +1035,7 @@ export class AgentWorkspaceProxy {
|
|
|
1025
1035
|
this.emitConversation(conversation);
|
|
1026
1036
|
|
|
1027
1037
|
try {
|
|
1028
|
-
const result = await
|
|
1038
|
+
const result = await client.prompt({
|
|
1029
1039
|
sessionId: conversation.agentSessionId,
|
|
1030
1040
|
content: payload.contentBlocks,
|
|
1031
1041
|
clientMessageId: payload.clientMessageId,
|
|
@@ -1651,8 +1661,10 @@ export class AgentWorkspaceProxy {
|
|
|
1651
1661
|
));
|
|
1652
1662
|
this.permissionSources.delete(payload.requestId);
|
|
1653
1663
|
} else {
|
|
1654
|
-
this.
|
|
1655
|
-
|
|
1664
|
+
const conversation = this.conversations.get(payload.conversationId);
|
|
1665
|
+
const respondClient = conversation ? this.clientForProvider(conversation.provider) : undefined;
|
|
1666
|
+
respondClient?.respondPermission({
|
|
1667
|
+
sessionId: conversation?.agentSessionId,
|
|
1656
1668
|
requestId: payload.requestId,
|
|
1657
1669
|
outcome: payload.outcome === "cancelled" ? "deny" : payload.outcome,
|
|
1658
1670
|
optionId: selectedOptionId,
|
|
@@ -1919,12 +1931,12 @@ export class AgentWorkspaceProxy {
|
|
|
1919
1931
|
return undefined;
|
|
1920
1932
|
}
|
|
1921
1933
|
|
|
1922
|
-
private
|
|
1934
|
+
private handleProviderExit(provider: AgentProvider, message: string): void {
|
|
1935
|
+
this.clients.delete(provider);
|
|
1936
|
+
this.agentProtocols.delete(provider);
|
|
1923
1937
|
this.cancelPendingPermissions();
|
|
1924
|
-
this.status = "error";
|
|
1925
|
-
this.error = message;
|
|
1926
|
-
this.client = undefined;
|
|
1927
1938
|
for (const conversation of this.conversations.values()) {
|
|
1939
|
+
if (conversation.provider !== provider) continue;
|
|
1928
1940
|
conversation.status = "error";
|
|
1929
1941
|
conversation.lastMessagePreview = message;
|
|
1930
1942
|
conversation.lastActivityAt = Date.now();
|
|
@@ -1937,6 +1949,7 @@ export class AgentWorkspaceProxy {
|
|
|
1937
1949
|
createdAt: Date.now(),
|
|
1938
1950
|
});
|
|
1939
1951
|
}
|
|
1952
|
+
this.sendCapabilities();
|
|
1940
1953
|
}
|
|
1941
1954
|
|
|
1942
1955
|
private cancelPendingPermissions(conversationId?: string): void {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
|
|
1
3
|
export type AgentProvider = "codex" | "claude" | "custom";
|
|
2
4
|
export type AgentProtocol = "acp" | "codex-app-server";
|
|
3
5
|
export type AgentFraming = "content-length" | "newline";
|
|
@@ -33,5 +35,32 @@ export function resolveAgentCommand(input: {
|
|
|
33
35
|
};
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
if (input.provider === "claude") {
|
|
39
|
+
return {
|
|
40
|
+
provider: "claude",
|
|
41
|
+
command: "claude --acp",
|
|
42
|
+
protocol: "acp",
|
|
43
|
+
framing: "content-length",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// custom: caller must provide --agent-command
|
|
36
48
|
return null;
|
|
37
49
|
}
|
|
50
|
+
|
|
51
|
+
export function detectAvailableProviders(): AgentProvider[] {
|
|
52
|
+
const available: AgentProvider[] = [];
|
|
53
|
+
const bins = [
|
|
54
|
+
["claude", "claude"] as const,
|
|
55
|
+
["codex", "codex"] as const,
|
|
56
|
+
];
|
|
57
|
+
for (const [, bin] of bins) {
|
|
58
|
+
try {
|
|
59
|
+
execSync(`which ${bin}`, { stdio: "ignore" });
|
|
60
|
+
available.push(bin as AgentProvider);
|
|
61
|
+
} catch {
|
|
62
|
+
// not installed
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return available.length > 0 ? available : [];
|
|
66
|
+
}
|
|
@@ -21,7 +21,7 @@ import { getLanIp } from "../utils/lan-ip.js";
|
|
|
21
21
|
import { startKeepAwake, type KeepAwakeHandle } from "../utils/keep-awake.js";
|
|
22
22
|
import { AgentSessionProxy } from "./acp/agent-session.js";
|
|
23
23
|
import { AgentWorkspaceProxy } from "./acp/agent-workspace.js";
|
|
24
|
-
import type
|
|
24
|
+
import { detectAvailableProviders, type AgentProvider } from "./acp/provider-resolver.js";
|
|
25
25
|
|
|
26
26
|
export interface BridgeSessionOptions {
|
|
27
27
|
gatewayUrl: string;
|
|
@@ -330,13 +330,16 @@ export class BridgeSession {
|
|
|
330
330
|
}
|
|
331
331
|
if (this.options.agentUi) {
|
|
332
332
|
process.env.LINKSHELL_ID = this.terminalHookMarker(DEFAULT_TERMINAL_ID);
|
|
333
|
-
const
|
|
334
|
-
this.options.agentProvider
|
|
335
|
-
|
|
333
|
+
const availableProviders = this.options.agentProvider
|
|
334
|
+
? [normalizeAgentProvider(this.options.agentProvider)]
|
|
335
|
+
: detectAvailableProviders();
|
|
336
|
+
if (availableProviders.length === 0) {
|
|
337
|
+
availableProviders.push("codex"); // last-resort fallback
|
|
338
|
+
}
|
|
336
339
|
const agentOptions = {
|
|
337
340
|
sessionId: this.sessionId,
|
|
338
341
|
cwd: process.cwd(),
|
|
339
|
-
|
|
342
|
+
availableProviders,
|
|
340
343
|
command: this.options.agentCommand,
|
|
341
344
|
verbose: this.options.verbose,
|
|
342
345
|
send: (envelope: Envelope) => this.send(envelope),
|
|
@@ -347,7 +350,7 @@ export class BridgeSession {
|
|
|
347
350
|
this.agentWorkspace = new AgentWorkspaceProxy({
|
|
348
351
|
...agentOptions,
|
|
349
352
|
});
|
|
350
|
-
process.stderr.write(
|
|
353
|
+
process.stderr.write(`[bridge] agent workspace channel enabled (providers: ${availableProviders.join(", ")})\n`);
|
|
351
354
|
}
|
|
352
355
|
await this.spawnTerminal(DEFAULT_TERMINAL_ID, process.cwd());
|
|
353
356
|
this.connectGateway();
|
|
@@ -1274,13 +1277,15 @@ export class BridgeSession {
|
|
|
1274
1277
|
if (!term?.hookPort) return;
|
|
1275
1278
|
const marker = term.hookMarker;
|
|
1276
1279
|
const curlCmd = `curl -s -X POST "http://127.0.0.1:${term.hookPort}/hook?m=${marker}&lid=$LINKSHELL_ID" -H 'Content-Type: application/json' --data-binary @-`;
|
|
1277
|
-
const
|
|
1280
|
+
const providers = this.options.agentProvider
|
|
1281
|
+
? [normalizeAgentProvider(this.options.agentProvider)]
|
|
1282
|
+
: detectAvailableProviders();
|
|
1278
1283
|
try {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
+
for (const provider of providers) {
|
|
1285
|
+
if (provider === "codex") {
|
|
1286
|
+
this.setupCodexHooks(DEFAULT_TERMINAL_ID, curlCmd, marker);
|
|
1287
|
+
} else {
|
|
1288
|
+
// claude, custom
|
|
1284
1289
|
this.setupClaudeHooks(DEFAULT_TERMINAL_ID, curlCmd, [], marker);
|
|
1285
1290
|
}
|
|
1286
1291
|
}
|