thepopebot 1.2.75-beta.2 → 1.2.75-beta.4
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/bin/cli.js +24 -4
- package/bin/docker-build.js +5 -0
- package/bin/sync.js +84 -0
- package/lib/ai/async-channel.js +51 -0
- package/lib/ai/index.js +154 -153
- package/lib/ai/tools.js +46 -30
- package/lib/chat/actions.js +12 -0
- package/lib/chat/components/settings-coding-agents-page.js +139 -1
- package/lib/chat/components/settings-coding-agents-page.jsx +160 -0
- package/lib/code/port-forwards.js +14 -3
- package/lib/config.js +4 -0
- package/lib/tools/docker.js +3 -3
- package/package.json +1 -1
- package/setup/setup-ssl.mjs +407 -0
- package/templates/CLAUDE.md.template +3 -4
- package/templates/README.md +1 -1
- package/templates/docker-compose.custom.yml +35 -58
- package/templates/docker-compose.yml +12 -17
- package/templates/docs/CLI.md +3 -3
- package/templates/docs/CONFIGURATION.md +31 -65
- package/templates/docs/GETTING_STARTED.md +1 -1
- package/templates/docs/SECURITY.md +3 -3
- package/templates/docs/SKILLS.md +2 -1
- package/templates/docker-compose.litellm.yml +0 -82
- package/templates/traefik-dynamic.yml.example +0 -7
package/lib/ai/tools.js
CHANGED
|
@@ -54,8 +54,8 @@ const agentChatCodingTool = tool(
|
|
|
54
54
|
const codingAgent = getConfig('CODING_AGENT') || 'claude-code';
|
|
55
55
|
const containerName = `${codingAgent}-headless-${randomUUID().slice(0, 8)}`;
|
|
56
56
|
|
|
57
|
-
const { runHeadlessContainer } = await import('../tools/docker.js');
|
|
58
|
-
|
|
57
|
+
const { runHeadlessContainer, tailContainerLogs, waitForContainer, removeContainer } = await import('../tools/docker.js');
|
|
58
|
+
await runHeadlessContainer({
|
|
59
59
|
containerName,
|
|
60
60
|
repo,
|
|
61
61
|
branch: 'main',
|
|
@@ -66,20 +66,29 @@ const agentChatCodingTool = tool(
|
|
|
66
66
|
injectSecrets: true,
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
69
|
+
const streamCallback = runtime.configurable.streamCallback;
|
|
70
|
+
const { parseHeadlessStream } = await import('./headless-stream.js');
|
|
71
|
+
|
|
72
|
+
const logStream = await tailContainerLogs(containerName);
|
|
73
|
+
let resultSummary = '';
|
|
74
|
+
|
|
75
|
+
for await (const chunk of parseHeadlessStream(logStream, codingAgent)) {
|
|
76
|
+
if (chunk._resultSummary) resultSummary = chunk._resultSummary;
|
|
77
|
+
streamCallback?.(chunk);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const exitCode = await waitForContainer(containerName);
|
|
81
|
+
await removeContainer(containerName);
|
|
82
|
+
streamCallback?.(null);
|
|
83
|
+
|
|
84
|
+
if (exitCode !== 0) {
|
|
85
|
+
return `Task exited with errors (exit code ${exitCode}).`;
|
|
86
|
+
}
|
|
87
|
+
return resultSummary || 'Task completed successfully.';
|
|
77
88
|
} catch (err) {
|
|
78
89
|
console.error('[coding_agent] Failed:', err);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
error: err.message || 'Failed to launch investigation container',
|
|
82
|
-
});
|
|
90
|
+
runtime.configurable.streamCallback?.(null);
|
|
91
|
+
return `Failed to run coding agent: ${err.message}`;
|
|
83
92
|
}
|
|
84
93
|
},
|
|
85
94
|
{
|
|
@@ -91,7 +100,6 @@ const agentChatCodingTool = tool(
|
|
|
91
100
|
'A direct copy of the coding task including all relevant context from the conversation.'
|
|
92
101
|
),
|
|
93
102
|
}),
|
|
94
|
-
returnDirect: true,
|
|
95
103
|
}
|
|
96
104
|
);
|
|
97
105
|
|
|
@@ -110,30 +118,39 @@ const codeChatCodingTool = tool(
|
|
|
110
118
|
const featureBranch = workspace?.featureBranch;
|
|
111
119
|
const mode = codeModeType === 'code' ? 'dangerous' : 'plan';
|
|
112
120
|
|
|
113
|
-
const { runHeadlessContainer } = await import('../tools/docker.js');
|
|
121
|
+
const { runHeadlessContainer, tailContainerLogs, waitForContainer, removeContainer } = await import('../tools/docker.js');
|
|
114
122
|
const codingAgent = getConfig('CODING_AGENT') || 'claude-code';
|
|
115
123
|
const containerName = `${codingAgent}-headless-${randomUUID().slice(0, 8)}`;
|
|
116
124
|
|
|
117
|
-
|
|
125
|
+
await runHeadlessContainer({
|
|
118
126
|
containerName, repo, branch, featureBranch, workspaceId,
|
|
119
127
|
taskPrompt: prompt,
|
|
120
128
|
mode,
|
|
121
129
|
});
|
|
122
130
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
const streamCallback = runtime.configurable.streamCallback;
|
|
132
|
+
const { parseHeadlessStream } = await import('./headless-stream.js');
|
|
133
|
+
|
|
134
|
+
const logStream = await tailContainerLogs(containerName);
|
|
135
|
+
let resultSummary = '';
|
|
136
|
+
|
|
137
|
+
for await (const chunk of parseHeadlessStream(logStream, codingAgent)) {
|
|
138
|
+
if (chunk._resultSummary) resultSummary = chunk._resultSummary;
|
|
139
|
+
streamCallback?.(chunk);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const exitCode = await waitForContainer(containerName);
|
|
143
|
+
await removeContainer(containerName);
|
|
144
|
+
streamCallback?.(null);
|
|
145
|
+
|
|
146
|
+
if (exitCode !== 0) {
|
|
147
|
+
return `Task exited with errors (exit code ${exitCode}).`;
|
|
148
|
+
}
|
|
149
|
+
return resultSummary || 'Task completed successfully.';
|
|
131
150
|
} catch (err) {
|
|
132
151
|
console.error('[coding_agent] Failed:', err);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
error: err.message || 'Failed to launch headless coding task',
|
|
136
|
-
});
|
|
152
|
+
runtime.configurable.streamCallback?.(null);
|
|
153
|
+
return `Failed to run coding agent: ${err.message}`;
|
|
137
154
|
}
|
|
138
155
|
},
|
|
139
156
|
{
|
|
@@ -145,7 +162,6 @@ const codeChatCodingTool = tool(
|
|
|
145
162
|
'A direct copy of the coding task including all relevant context from the conversation.'
|
|
146
163
|
),
|
|
147
164
|
}),
|
|
148
|
-
returnDirect: true,
|
|
149
165
|
}
|
|
150
166
|
);
|
|
151
167
|
|
package/lib/chat/actions.js
CHANGED
|
@@ -653,6 +653,9 @@ export async function getCodingAgentSettings() {
|
|
|
653
653
|
const openCodeEnabled = getConfig('CODING_AGENT_OPENCODE_ENABLED');
|
|
654
654
|
const openCodeProvider = getConfig('CODING_AGENT_OPENCODE_PROVIDER') || '';
|
|
655
655
|
const openCodeModel = getConfig('CODING_AGENT_OPENCODE_MODEL') || '';
|
|
656
|
+
const kimiCliEnabled = getConfig('CODING_AGENT_KIMI_CLI_ENABLED');
|
|
657
|
+
const kimiCliProvider = getConfig('CODING_AGENT_KIMI_CLI_PROVIDER') || '';
|
|
658
|
+
const kimiCliModel = getConfig('CODING_AGENT_KIMI_CLI_MODEL') || '';
|
|
656
659
|
|
|
657
660
|
// Credential readiness
|
|
658
661
|
const oauthTokenCount = getOAuthTokenCount('claudeCode');
|
|
@@ -694,6 +697,11 @@ export async function getCodingAgentSettings() {
|
|
|
694
697
|
provider: openCodeProvider,
|
|
695
698
|
model: openCodeModel,
|
|
696
699
|
},
|
|
700
|
+
kimiCli: {
|
|
701
|
+
enabled: kimiCliEnabled === 'true',
|
|
702
|
+
provider: kimiCliProvider,
|
|
703
|
+
model: kimiCliModel,
|
|
704
|
+
},
|
|
697
705
|
builtinProviders: BUILTIN_PROVIDERS,
|
|
698
706
|
credentialStatuses,
|
|
699
707
|
customProviders,
|
|
@@ -735,6 +743,10 @@ export async function updateCodingAgentConfig(agent, config) {
|
|
|
735
743
|
if (config.enabled !== undefined) setConfigValue('CODING_AGENT_OPENCODE_ENABLED', String(config.enabled));
|
|
736
744
|
if (config.provider !== undefined) setConfigValue('CODING_AGENT_OPENCODE_PROVIDER', config.provider);
|
|
737
745
|
if (config.model !== undefined) setConfigValue('CODING_AGENT_OPENCODE_MODEL', config.model);
|
|
746
|
+
} else if (agent === 'kimi-cli') {
|
|
747
|
+
if (config.enabled !== undefined) setConfigValue('CODING_AGENT_KIMI_CLI_ENABLED', String(config.enabled));
|
|
748
|
+
if (config.provider !== undefined) setConfigValue('CODING_AGENT_KIMI_CLI_PROVIDER', config.provider);
|
|
749
|
+
if (config.model !== undefined) setConfigValue('CODING_AGENT_KIMI_CLI_MODEL', config.model);
|
|
738
750
|
} else {
|
|
739
751
|
return { error: 'Invalid agent' };
|
|
740
752
|
}
|
|
@@ -52,6 +52,9 @@ function DefaultAgentSection({ settings, onReload }) {
|
|
|
52
52
|
if (settings.openCode?.enabled && isOpenCodeReady(settings)) {
|
|
53
53
|
available.push({ value: "opencode", label: "OpenCode" });
|
|
54
54
|
}
|
|
55
|
+
if (settings.kimiCli?.enabled && isKimiCliReady(settings)) {
|
|
56
|
+
available.push({ value: "kimi-cli", label: "Kimi CLI" });
|
|
57
|
+
}
|
|
55
58
|
const handleChange = async (e) => {
|
|
56
59
|
setSaving(true);
|
|
57
60
|
const result = await setCodingAgentDefault(e.target.value);
|
|
@@ -99,7 +102,8 @@ function AgentCards({ settings, onReload }) {
|
|
|
99
102
|
/* @__PURE__ */ jsx(PiCard, { settings, onReload }),
|
|
100
103
|
/* @__PURE__ */ jsx(GeminiCliCard, { settings, onReload }),
|
|
101
104
|
/* @__PURE__ */ jsx(CodexCliCard, { settings, onReload }),
|
|
102
|
-
/* @__PURE__ */ jsx(OpenCodeCard, { settings, onReload })
|
|
105
|
+
/* @__PURE__ */ jsx(OpenCodeCard, { settings, onReload }),
|
|
106
|
+
/* @__PURE__ */ jsx(KimiCliCard, { settings, onReload })
|
|
103
107
|
] })
|
|
104
108
|
] });
|
|
105
109
|
}
|
|
@@ -687,6 +691,136 @@ function OpenCodeCard({ settings, onReload }) {
|
|
|
687
691
|
) })
|
|
688
692
|
] });
|
|
689
693
|
}
|
|
694
|
+
function KimiCliCard({ settings, onReload }) {
|
|
695
|
+
const config = settings.kimiCli;
|
|
696
|
+
const [modelText, setModelText] = useState(config.model || "");
|
|
697
|
+
const [saved, setSaved] = useState(false);
|
|
698
|
+
const showSaved = () => {
|
|
699
|
+
setSaved(true);
|
|
700
|
+
setTimeout(() => setSaved(false), 2e3);
|
|
701
|
+
};
|
|
702
|
+
const handleToggle = async () => {
|
|
703
|
+
await updateCodingAgentConfig("kimi-cli", { enabled: !config.enabled });
|
|
704
|
+
await onReload();
|
|
705
|
+
};
|
|
706
|
+
const handleProviderChange = async (e) => {
|
|
707
|
+
const newProvider = e.target.value;
|
|
708
|
+
const cp = settings?.customProviders?.find((p) => p.key === newProvider);
|
|
709
|
+
const models = getAgentModels(settings, newProvider);
|
|
710
|
+
const newModel = models.length > 0 ? models[0].id : cp?.models?.[0] || "";
|
|
711
|
+
setModelText(newModel);
|
|
712
|
+
await updateCodingAgentConfig("kimi-cli", { provider: newProvider, model: newModel });
|
|
713
|
+
await onReload();
|
|
714
|
+
showSaved();
|
|
715
|
+
};
|
|
716
|
+
const handleModelChange = async (e) => {
|
|
717
|
+
await updateCodingAgentConfig("kimi-cli", { model: e.target.value });
|
|
718
|
+
await onReload();
|
|
719
|
+
showSaved();
|
|
720
|
+
};
|
|
721
|
+
const handleModelTextSave = async () => {
|
|
722
|
+
await updateCodingAgentConfig("kimi-cli", { model: modelText });
|
|
723
|
+
await onReload();
|
|
724
|
+
showSaved();
|
|
725
|
+
};
|
|
726
|
+
const availableProviders = [];
|
|
727
|
+
if (settings?.builtinProviders && settings?.credentialStatuses) {
|
|
728
|
+
const statusMap = new Map(settings.credentialStatuses.map((s) => [s.key, s.isSet]));
|
|
729
|
+
for (const [slug, prov] of Object.entries(settings.builtinProviders)) {
|
|
730
|
+
const hasKey = prov.credentials.some((c) => statusMap.get(c.key));
|
|
731
|
+
if (hasKey) {
|
|
732
|
+
availableProviders.push({ slug, name: prov.name });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (settings?.customProviders) {
|
|
737
|
+
for (const cp of settings.customProviders) {
|
|
738
|
+
availableProviders.push({ slug: cp.key, name: cp.name });
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const ready = isKimiCliReady(settings);
|
|
742
|
+
const selectedProviderReady = availableProviders.some((p) => p.slug === config.provider);
|
|
743
|
+
const builtinModels = config.provider ? getAgentModels(settings, config.provider) : [];
|
|
744
|
+
const customProvider = settings?.customProviders?.find((p) => p.key === config.provider);
|
|
745
|
+
const providerModels = builtinModels.length > 0 ? builtinModels : (customProvider?.models || []).map((m) => ({ id: m, name: m }));
|
|
746
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card p-4", children: [
|
|
747
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-1", children: [
|
|
748
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
749
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Kimi CLI" }),
|
|
750
|
+
config.enabled && /* @__PURE__ */ jsx(StatusDot, { ready })
|
|
751
|
+
] }),
|
|
752
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
753
|
+
saved && /* @__PURE__ */ jsxs("span", { className: "text-xs text-green-500 inline-flex items-center gap-1", children: [
|
|
754
|
+
/* @__PURE__ */ jsx(CheckIcon, { size: 12 }),
|
|
755
|
+
" Saved"
|
|
756
|
+
] }),
|
|
757
|
+
/* @__PURE__ */ jsx(ToggleSwitch, { checked: config.enabled, onChange: handleToggle })
|
|
758
|
+
] })
|
|
759
|
+
] }),
|
|
760
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-3", children: "Coding agent by MoonshotAI with multi-provider support." }),
|
|
761
|
+
config.enabled && /* @__PURE__ */ jsx("div", { className: "border-t border-border pt-3 space-y-3", children: availableProviders.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
762
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
763
|
+
/* @__PURE__ */ jsx("label", { className: "text-sm font-medium", children: "Provider" }),
|
|
764
|
+
/* @__PURE__ */ jsxs(
|
|
765
|
+
"select",
|
|
766
|
+
{
|
|
767
|
+
value: config.provider || "",
|
|
768
|
+
onChange: handleProviderChange,
|
|
769
|
+
className: "w-48 rounded-md border border-border bg-background px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-foreground",
|
|
770
|
+
children: [
|
|
771
|
+
/* @__PURE__ */ jsx("option", { value: "", children: "Select provider..." }),
|
|
772
|
+
availableProviders.map((p) => /* @__PURE__ */ jsx("option", { value: p.slug, children: p.name }, p.slug))
|
|
773
|
+
]
|
|
774
|
+
}
|
|
775
|
+
)
|
|
776
|
+
] }),
|
|
777
|
+
config.provider && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
778
|
+
/* @__PURE__ */ jsx("label", { className: "text-sm font-medium", children: "Model" }),
|
|
779
|
+
providerModels.length > 0 ? /* @__PURE__ */ jsx(
|
|
780
|
+
"select",
|
|
781
|
+
{
|
|
782
|
+
value: config.model || "",
|
|
783
|
+
onChange: handleModelChange,
|
|
784
|
+
className: "w-48 rounded-md border border-border bg-background px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-foreground",
|
|
785
|
+
children: providerModels.map((m) => /* @__PURE__ */ jsx("option", { value: m.id, children: m.name }, m.id))
|
|
786
|
+
}
|
|
787
|
+
) : /* @__PURE__ */ jsx(
|
|
788
|
+
"input",
|
|
789
|
+
{
|
|
790
|
+
type: "text",
|
|
791
|
+
value: modelText,
|
|
792
|
+
onChange: (e) => setModelText(e.target.value),
|
|
793
|
+
onKeyDown: (e) => e.key === "Enter" && handleModelTextSave(),
|
|
794
|
+
placeholder: "Model name",
|
|
795
|
+
className: "w-48 rounded-md border border-border bg-background px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-foreground"
|
|
796
|
+
}
|
|
797
|
+
)
|
|
798
|
+
] }),
|
|
799
|
+
config.provider && !selectedProviderReady && /* @__PURE__ */ jsx(
|
|
800
|
+
CredentialHint,
|
|
801
|
+
{
|
|
802
|
+
ready: false,
|
|
803
|
+
missingText: `${config.provider} API Key is not set. Configure it on the LLMs page.`
|
|
804
|
+
}
|
|
805
|
+
),
|
|
806
|
+
config.provider && providerModels.length === 0 && /* @__PURE__ */ jsx("div", { className: "flex justify-end mt-1", children: /* @__PURE__ */ jsx(
|
|
807
|
+
"button",
|
|
808
|
+
{
|
|
809
|
+
onClick: handleModelTextSave,
|
|
810
|
+
disabled: modelText === (config.model || ""),
|
|
811
|
+
className: "rounded-md px-3 py-1.5 text-sm font-medium bg-foreground text-background hover:bg-foreground/90 disabled:opacity-50 transition-colors",
|
|
812
|
+
children: "Save"
|
|
813
|
+
}
|
|
814
|
+
) })
|
|
815
|
+
] }) : /* @__PURE__ */ jsx(
|
|
816
|
+
CredentialHint,
|
|
817
|
+
{
|
|
818
|
+
ready: false,
|
|
819
|
+
missingText: "Configure at least one LLM provider on the LLMs page to use Kimi CLI"
|
|
820
|
+
}
|
|
821
|
+
) })
|
|
822
|
+
] });
|
|
823
|
+
}
|
|
690
824
|
function getAgentModels(settings, providerSlug) {
|
|
691
825
|
const provider = settings?.builtinProviders?.[providerSlug];
|
|
692
826
|
if (!provider?.models) return [];
|
|
@@ -720,6 +854,10 @@ function isOpenCodeReady(settings) {
|
|
|
720
854
|
if (!settings.openCode?.enabled || !settings.openCode?.provider) return false;
|
|
721
855
|
return isProviderReady(settings, settings.openCode.provider);
|
|
722
856
|
}
|
|
857
|
+
function isKimiCliReady(settings) {
|
|
858
|
+
if (!settings.kimiCli?.enabled || !settings.kimiCli?.provider) return false;
|
|
859
|
+
return isProviderReady(settings, settings.kimiCli.provider);
|
|
860
|
+
}
|
|
723
861
|
function isProviderReady(settings, provider) {
|
|
724
862
|
const statusMap = new Map((settings.credentialStatuses || []).map((s) => [s.key, s.isSet]));
|
|
725
863
|
const builtin = settings.builtinProviders?.[provider];
|
|
@@ -72,6 +72,9 @@ function DefaultAgentSection({ settings, onReload }) {
|
|
|
72
72
|
if (settings.openCode?.enabled && isOpenCodeReady(settings)) {
|
|
73
73
|
available.push({ value: 'opencode', label: 'OpenCode' });
|
|
74
74
|
}
|
|
75
|
+
if (settings.kimiCli?.enabled && isKimiCliReady(settings)) {
|
|
76
|
+
available.push({ value: 'kimi-cli', label: 'Kimi CLI' });
|
|
77
|
+
}
|
|
75
78
|
|
|
76
79
|
const handleChange = async (e) => {
|
|
77
80
|
setSaving(true);
|
|
@@ -133,6 +136,7 @@ function AgentCards({ settings, onReload }) {
|
|
|
133
136
|
<GeminiCliCard settings={settings} onReload={onReload} />
|
|
134
137
|
<CodexCliCard settings={settings} onReload={onReload} />
|
|
135
138
|
<OpenCodeCard settings={settings} onReload={onReload} />
|
|
139
|
+
<KimiCliCard settings={settings} onReload={onReload} />
|
|
136
140
|
</div>
|
|
137
141
|
</div>
|
|
138
142
|
);
|
|
@@ -834,6 +838,157 @@ function OpenCodeCard({ settings, onReload }) {
|
|
|
834
838
|
);
|
|
835
839
|
}
|
|
836
840
|
|
|
841
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
842
|
+
// Kimi CLI card
|
|
843
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
844
|
+
|
|
845
|
+
function KimiCliCard({ settings, onReload }) {
|
|
846
|
+
const config = settings.kimiCli;
|
|
847
|
+
const [modelText, setModelText] = useState(config.model || '');
|
|
848
|
+
const [saved, setSaved] = useState(false);
|
|
849
|
+
|
|
850
|
+
const showSaved = () => {
|
|
851
|
+
setSaved(true);
|
|
852
|
+
setTimeout(() => setSaved(false), 2000);
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
const handleToggle = async () => {
|
|
856
|
+
await updateCodingAgentConfig('kimi-cli', { enabled: !config.enabled });
|
|
857
|
+
await onReload();
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
const handleProviderChange = async (e) => {
|
|
861
|
+
const newProvider = e.target.value;
|
|
862
|
+
const cp = settings?.customProviders?.find((p) => p.key === newProvider);
|
|
863
|
+
const models = getAgentModels(settings, newProvider);
|
|
864
|
+
const newModel = models.length > 0 ? models[0].id : (cp?.models?.[0] || '');
|
|
865
|
+
setModelText(newModel);
|
|
866
|
+
await updateCodingAgentConfig('kimi-cli', { provider: newProvider, model: newModel });
|
|
867
|
+
await onReload();
|
|
868
|
+
showSaved();
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
const handleModelChange = async (e) => {
|
|
872
|
+
await updateCodingAgentConfig('kimi-cli', { model: e.target.value });
|
|
873
|
+
await onReload();
|
|
874
|
+
showSaved();
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
const handleModelTextSave = async () => {
|
|
878
|
+
await updateCodingAgentConfig('kimi-cli', { model: modelText });
|
|
879
|
+
await onReload();
|
|
880
|
+
showSaved();
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
const availableProviders = [];
|
|
884
|
+
if (settings?.builtinProviders && settings?.credentialStatuses) {
|
|
885
|
+
const statusMap = new Map(settings.credentialStatuses.map((s) => [s.key, s.isSet]));
|
|
886
|
+
for (const [slug, prov] of Object.entries(settings.builtinProviders)) {
|
|
887
|
+
const hasKey = prov.credentials.some((c) => statusMap.get(c.key));
|
|
888
|
+
if (hasKey) {
|
|
889
|
+
availableProviders.push({ slug, name: prov.name });
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (settings?.customProviders) {
|
|
894
|
+
for (const cp of settings.customProviders) {
|
|
895
|
+
availableProviders.push({ slug: cp.key, name: cp.name });
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const ready = isKimiCliReady(settings);
|
|
900
|
+
const selectedProviderReady = availableProviders.some(p => p.slug === config.provider);
|
|
901
|
+
|
|
902
|
+
const builtinModels = config.provider ? getAgentModels(settings, config.provider) : [];
|
|
903
|
+
const customProvider = settings?.customProviders?.find((p) => p.key === config.provider);
|
|
904
|
+
const providerModels = builtinModels.length > 0 ? builtinModels : (customProvider?.models || []).map((m) => ({ id: m, name: m }));
|
|
905
|
+
|
|
906
|
+
return (
|
|
907
|
+
<div className="rounded-lg border bg-card p-4">
|
|
908
|
+
<div className="flex items-center justify-between mb-1">
|
|
909
|
+
<div className="flex items-center gap-2">
|
|
910
|
+
<span className="text-sm font-medium">Kimi CLI</span>
|
|
911
|
+
{config.enabled && <StatusDot ready={ready} />}
|
|
912
|
+
</div>
|
|
913
|
+
<div className="flex items-center gap-3">
|
|
914
|
+
{saved && <span className="text-xs text-green-500 inline-flex items-center gap-1"><CheckIcon size={12} /> Saved</span>}
|
|
915
|
+
<ToggleSwitch checked={config.enabled} onChange={handleToggle} />
|
|
916
|
+
</div>
|
|
917
|
+
</div>
|
|
918
|
+
<p className="text-xs text-muted-foreground mb-3">Coding agent by MoonshotAI with multi-provider support.</p>
|
|
919
|
+
|
|
920
|
+
{config.enabled && (
|
|
921
|
+
<div className="border-t border-border pt-3 space-y-3">
|
|
922
|
+
{availableProviders.length > 0 ? (
|
|
923
|
+
<>
|
|
924
|
+
<div className="flex items-center justify-between">
|
|
925
|
+
<label className="text-sm font-medium">Provider</label>
|
|
926
|
+
<select
|
|
927
|
+
value={config.provider || ''}
|
|
928
|
+
onChange={handleProviderChange}
|
|
929
|
+
className="w-48 rounded-md border border-border bg-background px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-foreground"
|
|
930
|
+
>
|
|
931
|
+
<option value="">Select provider...</option>
|
|
932
|
+
{availableProviders.map((p) => (
|
|
933
|
+
<option key={p.slug} value={p.slug}>{p.name}</option>
|
|
934
|
+
))}
|
|
935
|
+
</select>
|
|
936
|
+
</div>
|
|
937
|
+
|
|
938
|
+
{config.provider && (
|
|
939
|
+
<div className="flex items-center justify-between">
|
|
940
|
+
<label className="text-sm font-medium">Model</label>
|
|
941
|
+
{providerModels.length > 0 ? (
|
|
942
|
+
<select
|
|
943
|
+
value={config.model || ''}
|
|
944
|
+
onChange={handleModelChange}
|
|
945
|
+
className="w-48 rounded-md border border-border bg-background px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-foreground"
|
|
946
|
+
>
|
|
947
|
+
{providerModels.map((m) => (
|
|
948
|
+
<option key={m.id} value={m.id}>{m.name}</option>
|
|
949
|
+
))}
|
|
950
|
+
</select>
|
|
951
|
+
) : (
|
|
952
|
+
<input
|
|
953
|
+
type="text"
|
|
954
|
+
value={modelText}
|
|
955
|
+
onChange={(e) => setModelText(e.target.value)}
|
|
956
|
+
onKeyDown={(e) => e.key === 'Enter' && handleModelTextSave()}
|
|
957
|
+
placeholder="Model name"
|
|
958
|
+
className="w-48 rounded-md border border-border bg-background px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-foreground"
|
|
959
|
+
/>
|
|
960
|
+
)}
|
|
961
|
+
</div>
|
|
962
|
+
)}
|
|
963
|
+
|
|
964
|
+
{config.provider && !selectedProviderReady && (
|
|
965
|
+
<CredentialHint
|
|
966
|
+
ready={false}
|
|
967
|
+
missingText={`${config.provider} API Key is not set. Configure it on the LLMs page.`}
|
|
968
|
+
/>
|
|
969
|
+
)}
|
|
970
|
+
|
|
971
|
+
{config.provider && providerModels.length === 0 && (
|
|
972
|
+
<div className="flex justify-end mt-1">
|
|
973
|
+
<button onClick={handleModelTextSave} disabled={modelText === (config.model || '')}
|
|
974
|
+
className="rounded-md px-3 py-1.5 text-sm font-medium bg-foreground text-background hover:bg-foreground/90 disabled:opacity-50 transition-colors">
|
|
975
|
+
Save
|
|
976
|
+
</button>
|
|
977
|
+
</div>
|
|
978
|
+
)}
|
|
979
|
+
</>
|
|
980
|
+
) : (
|
|
981
|
+
<CredentialHint
|
|
982
|
+
ready={false}
|
|
983
|
+
missingText="Configure at least one LLM provider on the LLMs page to use Kimi CLI"
|
|
984
|
+
/>
|
|
985
|
+
)}
|
|
986
|
+
</div>
|
|
987
|
+
)}
|
|
988
|
+
</div>
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
|
|
837
992
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
838
993
|
// Shared helpers
|
|
839
994
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -881,6 +1036,11 @@ function isOpenCodeReady(settings) {
|
|
|
881
1036
|
return isProviderReady(settings, settings.openCode.provider);
|
|
882
1037
|
}
|
|
883
1038
|
|
|
1039
|
+
function isKimiCliReady(settings) {
|
|
1040
|
+
if (!settings.kimiCli?.enabled || !settings.kimiCli?.provider) return false;
|
|
1041
|
+
return isProviderReady(settings, settings.kimiCli.provider);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
884
1044
|
function isProviderReady(settings, provider) {
|
|
885
1045
|
// Check if selected provider has credentials
|
|
886
1046
|
const statusMap = new Map((settings.credentialStatuses || []).map(s => [s.key, s.isSet]));
|
|
@@ -98,7 +98,10 @@ export function clearWorkspaceForwards(workspaceId) {
|
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
100
|
* Write Traefik dynamic config as YAML for all active port forwards.
|
|
101
|
-
*
|
|
101
|
+
*
|
|
102
|
+
* When SSL_DOMAIN is set (custom compose with Let's Encrypt), routes use
|
|
103
|
+
* *.SSL_DOMAIN on the websecure entrypoint with the letsencrypt cert resolver.
|
|
104
|
+
* Otherwise, routes use *.localhost on the web entrypoint (local dev).
|
|
102
105
|
*
|
|
103
106
|
* MUST be YAML, not JSON — Traefik's file provider treats backticks in
|
|
104
107
|
* JSON as Go template delimiters, silently breaking Host() rules.
|
|
@@ -125,14 +128,22 @@ function writeTraefikConfig() {
|
|
|
125
128
|
return;
|
|
126
129
|
}
|
|
127
130
|
|
|
131
|
+
const sslDomain = process.env.SSL_DOMAIN;
|
|
132
|
+
const entrypoint = sslDomain ? 'websecure' : 'web';
|
|
133
|
+
|
|
128
134
|
const lines = ['http:', ' routers:'];
|
|
129
135
|
for (const { workspaceId, port } of entries) {
|
|
130
136
|
const key = `wksp-${workspaceId}-${port}`;
|
|
131
|
-
const
|
|
137
|
+
const subdomain = `${workspaceId.slice(0, 8)}-${port}`;
|
|
138
|
+
const host = sslDomain ? `${subdomain}.${sslDomain}` : `${subdomain}.localhost`;
|
|
132
139
|
lines.push(` ${key}:`);
|
|
133
140
|
lines.push(' rule: Host(`' + host + '`)');
|
|
134
141
|
lines.push(' entryPoints:');
|
|
135
|
-
lines.push(
|
|
142
|
+
lines.push(` - ${entrypoint}`);
|
|
143
|
+
if (sslDomain) {
|
|
144
|
+
lines.push(' tls:');
|
|
145
|
+
lines.push(' certResolver: letsencrypt');
|
|
146
|
+
}
|
|
136
147
|
lines.push(` service: ${key}`);
|
|
137
148
|
}
|
|
138
149
|
lines.push(' services:');
|
package/lib/config.js
CHANGED
|
@@ -54,6 +54,9 @@ const CONFIG_KEYS = new Set([
|
|
|
54
54
|
'CODING_AGENT_OPENCODE_ENABLED',
|
|
55
55
|
'CODING_AGENT_OPENCODE_PROVIDER',
|
|
56
56
|
'CODING_AGENT_OPENCODE_MODEL',
|
|
57
|
+
'CODING_AGENT_KIMI_CLI_ENABLED',
|
|
58
|
+
'CODING_AGENT_KIMI_CLI_PROVIDER',
|
|
59
|
+
'CODING_AGENT_KIMI_CLI_MODEL',
|
|
57
60
|
]);
|
|
58
61
|
|
|
59
62
|
// Default values
|
|
@@ -69,6 +72,7 @@ const DEFAULTS = {
|
|
|
69
72
|
CODING_AGENT_CODEX_CLI_ENABLED: 'false',
|
|
70
73
|
CODING_AGENT_CODEX_CLI_AUTH: 'api-key',
|
|
71
74
|
CODING_AGENT_OPENCODE_ENABLED: 'false',
|
|
75
|
+
CODING_AGENT_KIMI_CLI_ENABLED: 'false',
|
|
72
76
|
};
|
|
73
77
|
|
|
74
78
|
// In-memory cache on globalThis to survive Next.js webpack chunk duplication.
|
package/lib/tools/docker.js
CHANGED
|
@@ -334,9 +334,9 @@ function buildAgentAuthEnv(agent) {
|
|
|
334
334
|
}
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
|
-
} else if (agent === 'pi-coding-agent' || agent === 'opencode') {
|
|
338
|
-
// Pi and
|
|
339
|
-
const configPrefix = agent === 'opencode' ? 'CODING_AGENT_OPENCODE' : 'CODING_AGENT_PI';
|
|
337
|
+
} else if (agent === 'pi-coding-agent' || agent === 'opencode' || agent === 'kimi-cli') {
|
|
338
|
+
// Pi, OpenCode, and Kimi share the same multi-provider auth pattern
|
|
339
|
+
const configPrefix = agent === 'opencode' ? 'CODING_AGENT_OPENCODE' : agent === 'kimi-cli' ? 'CODING_AGENT_KIMI_CLI' : 'CODING_AGENT_PI';
|
|
340
340
|
const provider = getConfig(`${configPrefix}_PROVIDER`) || 'anthropic';
|
|
341
341
|
backendApi = provider;
|
|
342
342
|
const model = getConfig(`${configPrefix}_MODEL`);
|