Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec9ba5b784 | ||
|
|
cee4149884 | ||
|
|
5599e4cf35 | ||
|
|
84cdd2df73 | ||
|
|
7929f57460 | ||
|
|
7876679c5d | ||
|
|
bc6928525d | ||
|
|
755c847d9a | ||
|
|
80a8639940 | ||
|
|
6cb5704291 | ||
|
|
4a987c836d | ||
|
|
a2fb55326c | ||
|
|
af29c6a980 | ||
|
|
f2a0e8e5bb | ||
|
|
f6456c2883 | ||
|
|
39f0d000d1 | ||
|
|
a8d9d630bc | ||
|
|
e93a1d8138 | ||
|
|
f6681be6f4 | ||
|
|
c79ac3fe81 | ||
|
|
b78b06353a | ||
|
|
c49b6cc241 | ||
|
|
30c945fe92 | ||
|
|
dfd511c310 | ||
|
|
1657525201 | ||
|
|
3e4b0d0505 | ||
|
|
003c6c9ae1 | ||
|
|
41fbb4d8b0 | ||
|
|
eecb340f64 | ||
|
|
d029eaa0bb | ||
|
|
9c7dcc1ed7 |
No files matched your search
@@ -73,6 +73,7 @@
|
|||||||
- Pi sessions live under `~/.clawdbot/sessions/` by default; the base directory is not configurable.
|
- Pi sessions live under `~/.clawdbot/sessions/` by default; the base directory is not configurable.
|
||||||
- Environment variables: see `~/.profile`.
|
- Environment variables: see `~/.profile`.
|
||||||
- Never commit or publish real phone numbers, videos, or live configuration values. Use obviously fake placeholders in docs, tests, and examples.
|
- Never commit or publish real phone numbers, videos, or live configuration values. Use obviously fake placeholders in docs, tests, and examples.
|
||||||
|
- Release flow: always read `docs/reference/RELEASING.md` and `docs/platforms/mac/release.md` before any release work; do not ask routine questions once those docs answer them.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
- Rebrand/migration issues or legacy config/service warnings: run `clawdbot doctor` (see `docs/gateway/doctor.md`).
|
- Rebrand/migration issues or legacy config/service warnings: run `clawdbot doctor` (see `docs/gateway/doctor.md`).
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
Docs: https://docs.clawd.bot
|
Docs: https://docs.clawd.bot
|
||||||
|
|
||||||
|
## 2026.1.17 (Unreleased)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
- macOS: strip prerelease/build suffixes when parsing gateway semver patches. (#1110) — thanks @zerone0x.
|
||||||
|
- macOS: keep CLI install pinned to the full build suffix. (#1111) — thanks @artuskg.
|
||||||
|
|
||||||
## 2026.1.16-2
|
## 2026.1.16-2
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|||||||
+16
-16
@@ -2,6 +2,22 @@
|
|||||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||||
<channel>
|
<channel>
|
||||||
<title>Clawdbot</title>
|
<title>Clawdbot</title>
|
||||||
|
<item>
|
||||||
|
<title>2026.1.16-2</title>
|
||||||
|
<pubDate>Sat, 17 Jan 2026 12:46:22 +0000</pubDate>
|
||||||
|
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
|
||||||
|
<sparkle:version>6273</sparkle:version>
|
||||||
|
<sparkle:shortVersionString>2026.1.16-2</sparkle:shortVersionString>
|
||||||
|
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||||
|
<description><![CDATA[<h2>Clawdbot 2026.1.16-2</h2>
|
||||||
|
<h3>Changes</h3>
|
||||||
|
<ul>
|
||||||
|
<li>CLI: stamp build commit into dist metadata so banners show the commit in npm installs.</li>
|
||||||
|
</ul>
|
||||||
|
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||||
|
]]></description>
|
||||||
|
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.16-2/Clawdbot-2026.1.16-2.zip" length="21399591" type="application/octet-stream" sparkle:edSignature="zelT+KzN32cXsihbFniPF5Heq0hkwFfL3Agrh/AaoKUkr7kJAFarkGSOZRTWZ9y+DvOluzn2wHHjVigRjMzrBA=="/>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<title>2026.1.15</title>
|
<title>2026.1.15</title>
|
||||||
<pubDate>Fri, 16 Jan 2026 10:31:53 +0000</pubDate>
|
<pubDate>Fri, 16 Jan 2026 10:31:53 +0000</pubDate>
|
||||||
@@ -255,21 +271,5 @@
|
|||||||
]]></description>
|
]]></description>
|
||||||
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.14-1/Clawdbot-2026.1.14-1.zip" length="19887144" type="application/octet-stream" sparkle:edSignature="1irKxBLt2eRtns34m/8JsjL/ZzhZQNjahwrxtArTvzaCnidS/MEnpD4nV2SHnhuo8g+fJZQpV9NoCAoEOAinCw=="/>
|
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.14-1/Clawdbot-2026.1.14-1.zip" length="19887144" type="application/octet-stream" sparkle:edSignature="1irKxBLt2eRtns34m/8JsjL/ZzhZQNjahwrxtArTvzaCnidS/MEnpD4nV2SHnhuo8g+fJZQpV9NoCAoEOAinCw=="/>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<title>2026.1.12-2</title>
|
|
||||||
<pubDate>Tue, 13 Jan 2026 10:05:25 +0000</pubDate>
|
|
||||||
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
|
|
||||||
<sparkle:version>5534</sparkle:version>
|
|
||||||
<sparkle:shortVersionString>2026.1.12-2</sparkle:shortVersionString>
|
|
||||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
|
||||||
<description><![CDATA[<h2>Clawdbot 2026.1.12-2</h2>
|
|
||||||
<h3>Fixes</h3>
|
|
||||||
<ul>
|
|
||||||
<li>Packaging: include <code>dist/memory/**</code> in the npm tarball (fixes <code>ERR_MODULE_NOT_FOUND</code> for <code>dist/memory/index.js</code>).</li>
|
|
||||||
</ul>
|
|
||||||
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
|
|
||||||
]]></description>
|
|
||||||
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.12-2/Clawdbot-2026.1.12-2.zip" length="19854203" type="application/octet-stream" sparkle:edSignature="CVpUofNS+pl6Smk/K0Q8q35saRuuFx90s4sePABORFvGcAF1biajC8zpiImKuXpqD0ENb+VTwDJ1ul1Oxh3wDA=="/>
|
|
||||||
</item>
|
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
@@ -35,7 +35,7 @@ enum CLIInstaller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func install(statusHandler: @escaping @MainActor @Sendable (String) async -> Void) async {
|
static func install(statusHandler: @escaping @MainActor @Sendable (String) async -> Void) async {
|
||||||
let expected = GatewayEnvironment.expectedGatewayVersion()?.description ?? "latest"
|
let expected = GatewayEnvironment.expectedGatewayVersionString() ?? "latest"
|
||||||
let prefix = Self.installPrefix()
|
let prefix = Self.installPrefix()
|
||||||
await statusHandler("Installing clawdbot CLI…")
|
await statusHandler("Installing clawdbot CLI…")
|
||||||
let cmd = self.installScriptCommand(version: expected, prefix: prefix)
|
let cmd = self.installScriptCommand(version: expected, prefix: prefix)
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ struct ConfigSchemaForm: View {
|
|||||||
let path: ConfigPath
|
let path: ConfigPath
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
self.renderNode(schema, path: path)
|
self.renderNode(self.schema, path: self.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func renderNode(_ schema: ConfigSchemaNode, path: ConfigPath) -> AnyView {
|
private func renderNode(_ schema: ConfigSchemaNode, path: ConfigPath) -> AnyView {
|
||||||
let storedValue = store.configValue(at: path)
|
let storedValue = self.store.configValue(at: path)
|
||||||
let value = storedValue ?? schema.explicitDefault
|
let value = storedValue ?? schema.explicitDefault
|
||||||
let label = hintForPath(path, hints: store.configUiHints)?.label ?? schema.title
|
let label = hintForPath(path, hints: store.configUiHints)?.label ?? schema.title
|
||||||
let help = hintForPath(path, hints: store.configUiHints)?.help ?? schema.description
|
let help = hintForPath(path, hints: store.configUiHints)?.help ?? schema.description
|
||||||
@@ -21,7 +21,7 @@ struct ConfigSchemaForm: View {
|
|||||||
if nonNull.count == 1, let only = nonNull.first {
|
if nonNull.count == 1, let only = nonNull.first {
|
||||||
return self.renderNode(only, path: path)
|
return self.renderNode(only, path: path)
|
||||||
}
|
}
|
||||||
let literals = nonNull.compactMap { $0.literalValue }
|
let literals = nonNull.compactMap(\.literalValue)
|
||||||
if !literals.isEmpty, literals.count == nonNull.count {
|
if !literals.isEmpty, literals.count == nonNull.count {
|
||||||
return AnyView(
|
return AnyView(
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
@@ -31,15 +31,20 @@ struct ConfigSchemaForm: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
Picker("", selection: self.enumBinding(path, options: literals, defaultValue: schema.explicitDefault)) {
|
Picker(
|
||||||
|
"",
|
||||||
|
selection: self.enumBinding(
|
||||||
|
path,
|
||||||
|
options: literals,
|
||||||
|
defaultValue: schema.explicitDefault))
|
||||||
|
{
|
||||||
Text("Select…").tag(-1)
|
Text("Select…").tag(-1)
|
||||||
ForEach(literals.indices, id: \ .self) { index in
|
ForEach(literals.indices, id: \ .self) { index in
|
||||||
Text(String(describing: literals[index])).tag(index)
|
Text(String(describing: literals[index])).tag(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pickerStyle(.menu)
|
.pickerStyle(.menu)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,8 +76,7 @@ struct ConfigSchemaForm: View {
|
|||||||
if schema.allowsAdditionalProperties {
|
if schema.allowsAdditionalProperties {
|
||||||
self.renderAdditionalProperties(schema, path: path, value: value)
|
self.renderAdditionalProperties(schema, path: path, value: value)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
case "array":
|
case "array":
|
||||||
return AnyView(self.renderArray(schema, path: path, value: value, label: label, help: help))
|
return AnyView(self.renderArray(schema, path: path, value: value, label: label, help: help))
|
||||||
case "boolean":
|
case "boolean":
|
||||||
@@ -80,8 +84,7 @@ struct ConfigSchemaForm: View {
|
|||||||
Toggle(isOn: self.boolBinding(path, defaultValue: schema.explicitDefault as? Bool)) {
|
Toggle(isOn: self.boolBinding(path, defaultValue: schema.explicitDefault as? Bool)) {
|
||||||
if let label { Text(label) } else { Text("Enabled") }
|
if let label { Text(label) } else { Text("Enabled") }
|
||||||
}
|
}
|
||||||
.help(help ?? "")
|
.help(help ?? ""))
|
||||||
)
|
|
||||||
case "number", "integer":
|
case "number", "integer":
|
||||||
return AnyView(self.renderNumberField(schema, path: path, label: label, help: help))
|
return AnyView(self.renderNumberField(schema, path: path, label: label, help: help))
|
||||||
case "string":
|
case "string":
|
||||||
@@ -93,8 +96,7 @@ struct ConfigSchemaForm: View {
|
|||||||
Text("Unsupported field type.")
|
Text("Unsupported field type.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,9 +157,7 @@ struct ConfigSchemaForm: View {
|
|||||||
text: self.numberBinding(
|
text: self.numberBinding(
|
||||||
path,
|
path,
|
||||||
isInteger: schema.schemaType == "integer",
|
isInteger: schema.schemaType == "integer",
|
||||||
defaultValue: defaultValue
|
defaultValue: defaultValue))
|
||||||
)
|
|
||||||
)
|
|
||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,7 +189,7 @@ struct ConfigSchemaForm: View {
|
|||||||
Button("Remove") {
|
Button("Remove") {
|
||||||
var next = items
|
var next = items
|
||||||
next.remove(at: index)
|
next.remove(at: index)
|
||||||
store.updateConfigValue(path: path, value: next)
|
self.store.updateConfigValue(path: path, value: next)
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.controlSize(.small)
|
.controlSize(.small)
|
||||||
@@ -202,7 +202,7 @@ struct ConfigSchemaForm: View {
|
|||||||
} else {
|
} else {
|
||||||
next.append("")
|
next.append("")
|
||||||
}
|
}
|
||||||
store.updateConfigValue(path: path, value: next)
|
self.store.updateConfigValue(path: path, value: next)
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.controlSize(.small)
|
.controlSize(.small)
|
||||||
@@ -238,7 +238,7 @@ struct ConfigSchemaForm: View {
|
|||||||
Button("Remove") {
|
Button("Remove") {
|
||||||
var next = dict
|
var next = dict
|
||||||
next.removeValue(forKey: key)
|
next.removeValue(forKey: key)
|
||||||
store.updateConfigValue(path: path, value: next)
|
self.store.updateConfigValue(path: path, value: next)
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.controlSize(.small)
|
.controlSize(.small)
|
||||||
@@ -254,7 +254,7 @@ struct ConfigSchemaForm: View {
|
|||||||
key = "new-\(index)"
|
key = "new-\(index)"
|
||||||
}
|
}
|
||||||
next[key] = additionalSchema.defaultValue
|
next[key] = additionalSchema.defaultValue
|
||||||
store.updateConfigValue(path: path, value: next)
|
self.store.updateConfigValue(path: path, value: next)
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.controlSize(.small)
|
.controlSize(.small)
|
||||||
@@ -270,9 +270,8 @@ struct ConfigSchemaForm: View {
|
|||||||
},
|
},
|
||||||
set: { newValue in
|
set: { newValue in
|
||||||
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
store.updateConfigValue(path: path, value: trimmed.isEmpty ? nil : trimmed)
|
self.store.updateConfigValue(path: path, value: trimmed.isEmpty ? nil : trimmed)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func boolBinding(_ path: ConfigPath, defaultValue: Bool?) -> Binding<Bool> {
|
private func boolBinding(_ path: ConfigPath, defaultValue: Bool?) -> Binding<Bool> {
|
||||||
@@ -282,16 +281,15 @@ struct ConfigSchemaForm: View {
|
|||||||
return defaultValue ?? false
|
return defaultValue ?? false
|
||||||
},
|
},
|
||||||
set: { newValue in
|
set: { newValue in
|
||||||
store.updateConfigValue(path: path, value: newValue)
|
self.store.updateConfigValue(path: path, value: newValue)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func numberBinding(
|
private func numberBinding(
|
||||||
_ path: ConfigPath,
|
_ path: ConfigPath,
|
||||||
isInteger: Bool,
|
isInteger: Bool,
|
||||||
defaultValue: Double?
|
defaultValue: Double?) -> Binding<String>
|
||||||
) -> Binding<String> {
|
{
|
||||||
Binding(
|
Binding(
|
||||||
get: {
|
get: {
|
||||||
if let value = store.configValue(at: path) { return String(describing: value) }
|
if let value = store.configValue(at: path) { return String(describing: value) }
|
||||||
@@ -301,22 +299,21 @@ struct ConfigSchemaForm: View {
|
|||||||
set: { newValue in
|
set: { newValue in
|
||||||
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
if trimmed.isEmpty {
|
if trimmed.isEmpty {
|
||||||
store.updateConfigValue(path: path, value: nil)
|
self.store.updateConfigValue(path: path, value: nil)
|
||||||
} else if let value = Double(trimmed) {
|
} else if let value = Double(trimmed) {
|
||||||
store.updateConfigValue(path: path, value: isInteger ? Int(value) : value)
|
self.store.updateConfigValue(path: path, value: isInteger ? Int(value) : value)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func enumBinding(
|
private func enumBinding(
|
||||||
_ path: ConfigPath,
|
_ path: ConfigPath,
|
||||||
options: [Any],
|
options: [Any],
|
||||||
defaultValue: Any?
|
defaultValue: Any?) -> Binding<Int>
|
||||||
) -> Binding<Int> {
|
{
|
||||||
Binding(
|
Binding(
|
||||||
get: {
|
get: {
|
||||||
let value = store.configValue(at: path) ?? defaultValue
|
let value = self.store.configValue(at: path) ?? defaultValue
|
||||||
guard let value else { return -1 }
|
guard let value else { return -1 }
|
||||||
return options.firstIndex { option in
|
return options.firstIndex { option in
|
||||||
String(describing: option) == String(describing: value)
|
String(describing: option) == String(describing: value)
|
||||||
@@ -324,12 +321,11 @@ struct ConfigSchemaForm: View {
|
|||||||
},
|
},
|
||||||
set: { index in
|
set: { index in
|
||||||
guard index >= 0, index < options.count else {
|
guard index >= 0, index < options.count else {
|
||||||
store.updateConfigValue(path: path, value: nil)
|
self.store.updateConfigValue(path: path, value: nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
store.updateConfigValue(path: path, value: options[index])
|
self.store.updateConfigValue(path: path, value: options[index])
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func mapKeyBinding(path: ConfigPath, key: String) -> Binding<String> {
|
private func mapKeyBinding(path: ConfigPath, key: String) -> Binding<String> {
|
||||||
@@ -339,14 +335,13 @@ struct ConfigSchemaForm: View {
|
|||||||
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
guard !trimmed.isEmpty else { return }
|
guard !trimmed.isEmpty else { return }
|
||||||
guard trimmed != key else { return }
|
guard trimmed != key else { return }
|
||||||
let current = store.configValue(at: path) as? [String: Any] ?? [:]
|
let current = self.store.configValue(at: path) as? [String: Any] ?? [:]
|
||||||
guard current[trimmed] == nil else { return }
|
guard current[trimmed] == nil else { return }
|
||||||
var next = current
|
var next = current
|
||||||
next[trimmed] = current[key]
|
next[trimmed] = current[key]
|
||||||
next.removeValue(forKey: key)
|
next.removeValue(forKey: key)
|
||||||
store.updateConfigValue(path: path, value: next)
|
self.store.updateConfigValue(path: path, value: next)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,10 +350,10 @@ struct ChannelConfigForm: View {
|
|||||||
let channelId: String
|
let channelId: String
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if store.configSchemaLoading {
|
if self.store.configSchemaLoading {
|
||||||
ProgressView().controlSize(.small)
|
ProgressView().controlSize(.small)
|
||||||
} else if let schema = store.channelConfigSchema(for: channelId) {
|
} else if let schema = store.channelConfigSchema(for: channelId) {
|
||||||
ConfigSchemaForm(store: store, schema: schema, path: [.key("channels"), .key(channelId)])
|
ConfigSchemaForm(store: self.store, schema: schema, path: [.key("channels"), .key(self.channelId)])
|
||||||
} else {
|
} else {
|
||||||
Text("Schema unavailable for this channel.")
|
Text("Schema unavailable for this channel.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
|||||||
@@ -434,25 +434,25 @@ extension ChannelsSettings {
|
|||||||
|
|
||||||
private func resolveChannelDetailTitle(_ id: String) -> String {
|
private func resolveChannelDetailTitle(_ id: String) -> String {
|
||||||
switch id {
|
switch id {
|
||||||
case "whatsapp": return "WhatsApp Web"
|
case "whatsapp": "WhatsApp Web"
|
||||||
case "telegram": return "Telegram Bot"
|
case "telegram": "Telegram Bot"
|
||||||
case "discord": return "Discord Bot"
|
case "discord": "Discord Bot"
|
||||||
case "slack": return "Slack Bot"
|
case "slack": "Slack Bot"
|
||||||
case "signal": return "Signal REST"
|
case "signal": "Signal REST"
|
||||||
case "imessage": return "iMessage"
|
case "imessage": "iMessage"
|
||||||
default: return self.resolveChannelTitle(id)
|
default: self.resolveChannelTitle(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func resolveChannelSystemImage(_ id: String) -> String {
|
private func resolveChannelSystemImage(_ id: String) -> String {
|
||||||
switch id {
|
switch id {
|
||||||
case "whatsapp": return "message"
|
case "whatsapp": "message"
|
||||||
case "telegram": return "paperplane"
|
case "telegram": "paperplane"
|
||||||
case "discord": return "bubble.left.and.bubble.right"
|
case "discord": "bubble.left.and.bubble.right"
|
||||||
case "slack": return "number"
|
case "slack": "number"
|
||||||
case "signal": return "antenna.radiowaves.left.and.right"
|
case "signal": "antenna.radiowaves.left.and.right"
|
||||||
case "imessage": return "message.fill"
|
case "imessage": "message.fill"
|
||||||
default: return "message"
|
default: "message"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ extension ChannelsStore {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
guard path.count >= 2 else { return nil }
|
guard path.count >= 2 else { return nil }
|
||||||
if case .key("channels") = path[0], case .key(_) = path[1] {
|
if case .key("channels") = path[0], case .key = path[1] {
|
||||||
let fallbackPath = Array(path.dropFirst())
|
let fallbackPath = Array(path.dropFirst())
|
||||||
return valueAtPath(self.configDraft, path: fallbackPath)
|
return valueAtPath(self.configDraft, path: fallbackPath)
|
||||||
}
|
}
|
||||||
@@ -93,10 +93,10 @@ private func valueAtPath(_ root: Any, path: ConfigPath) -> Any? {
|
|||||||
var current: Any? = root
|
var current: Any? = root
|
||||||
for segment in path {
|
for segment in path {
|
||||||
switch segment {
|
switch segment {
|
||||||
case .key(let key):
|
case let .key(key):
|
||||||
guard let dict = current as? [String: Any] else { return nil }
|
guard let dict = current as? [String: Any] else { return nil }
|
||||||
current = dict[key]
|
current = dict[key]
|
||||||
case .index(let index):
|
case let .index(index):
|
||||||
guard let array = current as? [Any], array.indices.contains(index) else { return nil }
|
guard let array = current as? [Any], array.indices.contains(index) else { return nil }
|
||||||
current = array[index]
|
current = array[index]
|
||||||
}
|
}
|
||||||
@@ -107,7 +107,7 @@ private func valueAtPath(_ root: Any, path: ConfigPath) -> Any? {
|
|||||||
private func setValue(_ root: inout Any, path: ConfigPath, value: Any?) {
|
private func setValue(_ root: inout Any, path: ConfigPath, value: Any?) {
|
||||||
guard let segment = path.first else { return }
|
guard let segment = path.first else { return }
|
||||||
switch segment {
|
switch segment {
|
||||||
case .key(let key):
|
case let .key(key):
|
||||||
var dict = root as? [String: Any] ?? [:]
|
var dict = root as? [String: Any] ?? [:]
|
||||||
if path.count == 1 {
|
if path.count == 1 {
|
||||||
if let value {
|
if let value {
|
||||||
@@ -122,7 +122,7 @@ private func setValue(_ root: inout Any, path: ConfigPath, value: Any?) {
|
|||||||
setValue(&child, path: Array(path.dropFirst()), value: value)
|
setValue(&child, path: Array(path.dropFirst()), value: value)
|
||||||
dict[key] = child
|
dict[key] = child
|
||||||
root = dict
|
root = dict
|
||||||
case .index(let index):
|
case let .index(index):
|
||||||
var array = root as? [Any] ?? []
|
var array = root as? [Any] ?? []
|
||||||
if index >= array.count {
|
if index >= array.count {
|
||||||
array.append(contentsOf: repeatElement(NSNull() as Any, count: index - array.count + 1))
|
array.append(contentsOf: repeatElement(NSNull() as Any, count: index - array.count + 1))
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ struct ConfigSchemaNode {
|
|||||||
for segment in path {
|
for segment in path {
|
||||||
guard let node = current else { return nil }
|
guard let node = current else { return nil }
|
||||||
switch segment {
|
switch segment {
|
||||||
case .key(let key):
|
case let .key(key):
|
||||||
if node.schemaType == "object" {
|
if node.schemaType == "object" {
|
||||||
if let next = node.properties[key] {
|
if let next = node.properties[key] {
|
||||||
current = next
|
current = next
|
||||||
@@ -174,7 +174,7 @@ func hintForPath(_ path: ConfigPath, hints: [String: ConfigUiHint]) -> ConfigUiH
|
|||||||
var match = true
|
var match = true
|
||||||
for (index, seg) in segments.enumerated() {
|
for (index, seg) in segments.enumerated() {
|
||||||
let hintSegment = hintSegments[index]
|
let hintSegment = hintSegments[index]
|
||||||
if hintSegment != "*" && hintSegment != seg {
|
if hintSegment != "*", hintSegment != seg {
|
||||||
match = false
|
match = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -196,7 +196,7 @@ func isSensitivePath(_ path: ConfigPath) -> Bool {
|
|||||||
func pathKey(_ path: ConfigPath) -> String {
|
func pathKey(_ path: ConfigPath) -> String {
|
||||||
path.compactMap { segment -> String? in
|
path.compactMap { segment -> String? in
|
||||||
switch segment {
|
switch segment {
|
||||||
case .key(let key): return key
|
case let .key(key): return key
|
||||||
case .index: return nil
|
case .index: return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ extension ConfigSettings {
|
|||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.store.configDirty && !self.isNixMode {
|
if self.store.configDirty, !self.isNixMode {
|
||||||
Text("Unsaved changes")
|
Text("Unsaved changes")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ struct Semver: Comparable, CustomStringConvertible, Sendable {
|
|||||||
let major = Int(parts[0]),
|
let major = Int(parts[0]),
|
||||||
let minor = Int(parts[1])
|
let minor = Int(parts[1])
|
||||||
else { return nil }
|
else { return nil }
|
||||||
let patch = Int(parts[2]) ?? 0
|
// Strip prerelease suffix (e.g., "11-4" → "11", "5-beta.1" → "5")
|
||||||
return Semver(major: major, minor: minor, patch: patch)
|
let patchRaw = String(parts[2])
|
||||||
|
let patchNumeric = patchRaw.split { $0 == "-" || $0 == "+" }.first.flatMap { Int($0) } ?? 0
|
||||||
|
return Semver(major: major, minor: minor, patch: patchNumeric)
|
||||||
}
|
}
|
||||||
|
|
||||||
func compatible(with required: Semver) -> Bool {
|
func compatible(with required: Semver) -> Bool {
|
||||||
@@ -78,8 +80,13 @@ enum GatewayEnvironment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func expectedGatewayVersion() -> Semver? {
|
static func expectedGatewayVersion() -> Semver? {
|
||||||
|
Semver.parse(self.expectedGatewayVersionString())
|
||||||
|
}
|
||||||
|
|
||||||
|
static func expectedGatewayVersionString() -> String? {
|
||||||
let bundleVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
let bundleVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||||
return Semver.parse(bundleVersion)
|
let trimmed = bundleVersion?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return (trimmed?.isEmpty == false) ? trimmed : nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exposed for tests so we can inject fake version checks without rewriting bundle metadata.
|
// Exposed for tests so we can inject fake version checks without rewriting bundle metadata.
|
||||||
@@ -98,6 +105,7 @@ enum GatewayEnvironment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let expected = self.expectedGatewayVersion()
|
let expected = self.expectedGatewayVersion()
|
||||||
|
let expectedString = self.expectedGatewayVersionString()
|
||||||
|
|
||||||
let projectRoot = CommandResolver.projectRoot()
|
let projectRoot = CommandResolver.projectRoot()
|
||||||
let projectEntrypoint = CommandResolver.gatewayEntrypoint(in: projectRoot)
|
let projectEntrypoint = CommandResolver.gatewayEntrypoint(in: projectRoot)
|
||||||
@@ -108,8 +116,8 @@ enum GatewayEnvironment {
|
|||||||
kind: .missingNode,
|
kind: .missingNode,
|
||||||
nodeVersion: nil,
|
nodeVersion: nil,
|
||||||
gatewayVersion: nil,
|
gatewayVersion: nil,
|
||||||
requiredGateway: expected?.description,
|
requiredGateway: expectedString,
|
||||||
message: RuntimeLocator.describeFailure(err))
|
message: RuntimeLocator.describeFailure(err))
|
||||||
case let .success(runtime):
|
case let .success(runtime):
|
||||||
let gatewayBin = CommandResolver.clawdbotExecutable()
|
let gatewayBin = CommandResolver.clawdbotExecutable()
|
||||||
|
|
||||||
@@ -118,7 +126,7 @@ enum GatewayEnvironment {
|
|||||||
kind: .missingGateway,
|
kind: .missingGateway,
|
||||||
nodeVersion: runtime.version.description,
|
nodeVersion: runtime.version.description,
|
||||||
gatewayVersion: nil,
|
gatewayVersion: nil,
|
||||||
requiredGateway: expected?.description,
|
requiredGateway: expectedString,
|
||||||
message: "clawdbot CLI not found in PATH; install the CLI.")
|
message: "clawdbot CLI not found in PATH; install the CLI.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,13 +134,14 @@ enum GatewayEnvironment {
|
|||||||
?? self.readLocalGatewayVersion(projectRoot: projectRoot)
|
?? self.readLocalGatewayVersion(projectRoot: projectRoot)
|
||||||
|
|
||||||
if let expected, let installed, !installed.compatible(with: expected) {
|
if let expected, let installed, !installed.compatible(with: expected) {
|
||||||
|
let expectedText = expectedString ?? expected.description
|
||||||
return GatewayEnvironmentStatus(
|
return GatewayEnvironmentStatus(
|
||||||
kind: .incompatible(found: installed.description, required: expected.description),
|
kind: .incompatible(found: installed.description, required: expectedText),
|
||||||
nodeVersion: runtime.version.description,
|
nodeVersion: runtime.version.description,
|
||||||
gatewayVersion: installed.description,
|
gatewayVersion: installed.description,
|
||||||
requiredGateway: expected.description,
|
requiredGateway: expectedText,
|
||||||
message: """
|
message: """
|
||||||
Gateway version \(installed.description) is incompatible with app \(expected.description);
|
Gateway version \(installed.description) is incompatible with app \(expectedText);
|
||||||
install or update the global package.
|
install or update the global package.
|
||||||
""")
|
""")
|
||||||
}
|
}
|
||||||
@@ -150,7 +159,7 @@ enum GatewayEnvironment {
|
|||||||
kind: .ok,
|
kind: .ok,
|
||||||
nodeVersion: runtime.version.description,
|
nodeVersion: runtime.version.description,
|
||||||
gatewayVersion: gatewayVersionText,
|
gatewayVersion: gatewayVersionText,
|
||||||
requiredGateway: expected?.description,
|
requiredGateway: expectedString,
|
||||||
message: "Node \(runtime.version.description); gateway \(gatewayVersionText) \(gatewayLabelText)")
|
message: "Node \(runtime.version.description); gateway \(gatewayVersionText) \(gatewayLabelText)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,8 +227,18 @@ enum GatewayEnvironment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func installGlobal(version: Semver?, statusHandler: @escaping @Sendable (String) -> Void) async {
|
static func installGlobal(version: Semver?, statusHandler: @escaping @Sendable (String) -> Void) async {
|
||||||
|
await self.installGlobal(versionString: version?.description, statusHandler: statusHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func installGlobal(versionString: String?, statusHandler: @escaping @Sendable (String) -> Void) async {
|
||||||
let preferred = CommandResolver.preferredPaths().joined(separator: ":")
|
let preferred = CommandResolver.preferredPaths().joined(separator: ":")
|
||||||
let target = version?.description ?? "latest"
|
let trimmed = versionString?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let target: String
|
||||||
|
if let trimmed, !trimmed.isEmpty {
|
||||||
|
target = trimmed
|
||||||
|
} else {
|
||||||
|
target = "latest"
|
||||||
|
}
|
||||||
let npm = CommandResolver.findExecutable(named: "npm")
|
let npm = CommandResolver.findExecutable(named: "npm")
|
||||||
let pnpm = CommandResolver.findExecutable(named: "pnpm")
|
let pnpm = CommandResolver.findExecutable(named: "pnpm")
|
||||||
let bun = CommandResolver.findExecutable(named: "bun")
|
let bun = CommandResolver.findExecutable(named: "bun")
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ struct OnboardingView: View {
|
|||||||
|
|
||||||
var canAdvance: Bool { !self.isWizardBlocking }
|
var canAdvance: Bool { !self.isWizardBlocking }
|
||||||
var devLinkCommand: String {
|
var devLinkCommand: String {
|
||||||
let version = GatewayEnvironment.expectedGatewayVersion()?.description ?? "latest"
|
let version = GatewayEnvironment.expectedGatewayVersionString() ?? "latest"
|
||||||
return "npm install -g clawdbot@\(version)"
|
return "npm install -g clawdbot@\(version)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,17 @@ import Testing
|
|||||||
@Test func semverParsesCommonForms() {
|
@Test func semverParsesCommonForms() {
|
||||||
#expect(Semver.parse("1.2.3") == Semver(major: 1, minor: 2, patch: 3))
|
#expect(Semver.parse("1.2.3") == Semver(major: 1, minor: 2, patch: 3))
|
||||||
#expect(Semver.parse("v2.0.0") == Semver(major: 2, minor: 0, patch: 0))
|
#expect(Semver.parse("v2.0.0") == Semver(major: 2, minor: 0, patch: 0))
|
||||||
#expect(Semver.parse("3.4.5-beta.1") == Semver(major: 3, minor: 4, patch: 0)) // patch drops trailing text
|
#expect(Semver.parse("3.4.5-beta.1") == Semver(major: 3, minor: 4, patch: 5)) // prerelease suffix stripped
|
||||||
|
#expect(Semver.parse("2026.1.11-4") == Semver(major: 2026, minor: 1, patch: 11)) // build suffix stripped
|
||||||
|
#expect(Semver.parse("1.0.5+build.123") == Semver(major: 1, minor: 0, patch: 5)) // metadata suffix stripped
|
||||||
|
#expect(Semver.parse("1.2.3+build.123") == Semver(major: 1, minor: 2, patch: 3))
|
||||||
|
#expect(Semver.parse("1.2.3-rc.1+build.7") == Semver(major: 1, minor: 2, patch: 3))
|
||||||
|
#expect(Semver.parse("v1.2.3-rc.1") == Semver(major: 1, minor: 2, patch: 3))
|
||||||
|
#expect(Semver.parse("1.2.0") == Semver(major: 1, minor: 2, patch: 0))
|
||||||
#expect(Semver.parse(nil) == nil)
|
#expect(Semver.parse(nil) == nil)
|
||||||
#expect(Semver.parse("invalid") == nil)
|
#expect(Semver.parse("invalid") == nil)
|
||||||
|
#expect(Semver.parse("1.2") == nil)
|
||||||
|
#expect(Semver.parse("1.2.x") == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func semverCompatibilityRequiresSameMajorAndNotOlder() {
|
@Test func semverCompatibilityRequiresSameMajorAndNotOlder() {
|
||||||
@@ -36,6 +44,7 @@ import Testing
|
|||||||
|
|
||||||
@Test func expectedGatewayVersionFromStringUsesParser() {
|
@Test func expectedGatewayVersionFromStringUsesParser() {
|
||||||
#expect(GatewayEnvironment.expectedGatewayVersion(from: "v9.1.2") == Semver(major: 9, minor: 1, patch: 2))
|
#expect(GatewayEnvironment.expectedGatewayVersion(from: "v9.1.2") == Semver(major: 9, minor: 1, patch: 2))
|
||||||
|
#expect(GatewayEnvironment.expectedGatewayVersion(from: "2026.1.11-4") == Semver(major: 2026, minor: 1, patch: 11))
|
||||||
#expect(GatewayEnvironment.expectedGatewayVersion(from: nil) == nil)
|
#expect(GatewayEnvironment.expectedGatewayVersion(from: nil) == nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -360,6 +360,19 @@ To force a voice note bubble in agent replies, include this tag anywhere in the
|
|||||||
|
|
||||||
The tag is stripped from the delivered text. Other channels ignore this tag.
|
The tag is stripped from the delivered text. Other channels ignore this tag.
|
||||||
|
|
||||||
|
For message tool sends, set `asVoice: true` with a voice-compatible audio `media` URL
|
||||||
|
(`message` is optional when media is present):
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
"action": "send",
|
||||||
|
"channel": "telegram",
|
||||||
|
"to": "123456789",
|
||||||
|
"media": "https://example.com/voice.ogg",
|
||||||
|
"asVoice": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Streaming (drafts)
|
## Streaming (drafts)
|
||||||
Telegram can stream **draft bubbles** while the agent is generating a response.
|
Telegram can stream **draft bubbles** while the agent is generating a response.
|
||||||
Clawdbot uses Bot API `sendMessageDraft` (not real messages) and then sends the
|
Clawdbot uses Bot API `sendMessageDraft` (not real messages) and then sends the
|
||||||
|
|||||||
+2
-1
@@ -292,7 +292,7 @@ Options:
|
|||||||
- `--non-interactive`
|
- `--non-interactive`
|
||||||
- `--mode <local|remote>`
|
- `--mode <local|remote>`
|
||||||
- `--flow <quickstart|advanced>`
|
- `--flow <quickstart|advanced>`
|
||||||
- `--auth-choice <setup-token|claude-cli|token|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|codex-cli|gemini-api-key|zai-api-key|apiKey|minimax-api|opencode-zen|skip>`
|
- `--auth-choice <setup-token|claude-cli|token|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|codex-cli|gemini-api-key|zai-api-key|apiKey|minimax-api|opencode-zen|skip>`
|
||||||
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
|
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
|
||||||
- `--token <token>` (non-interactive; used with `--auth-choice token`)
|
- `--token <token>` (non-interactive; used with `--auth-choice token`)
|
||||||
- `--token-profile-id <id>` (non-interactive; default: `<provider>:manual`)
|
- `--token-profile-id <id>` (non-interactive; default: `<provider>:manual`)
|
||||||
@@ -302,6 +302,7 @@ Options:
|
|||||||
- `--openrouter-api-key <key>`
|
- `--openrouter-api-key <key>`
|
||||||
- `--ai-gateway-api-key <key>`
|
- `--ai-gateway-api-key <key>`
|
||||||
- `--moonshot-api-key <key>`
|
- `--moonshot-api-key <key>`
|
||||||
|
- `--kimi-code-api-key <key>`
|
||||||
- `--gemini-api-key <key>`
|
- `--gemini-api-key <key>`
|
||||||
- `--zai-api-key <key>`
|
- `--zai-api-key <key>`
|
||||||
- `--minimax-api-key <key>`
|
- `--minimax-api-key <key>`
|
||||||
|
|||||||
@@ -155,6 +155,34 @@ Moonshot uses OpenAI-compatible endpoints, so configure it as a custom provider:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Kimi Code
|
||||||
|
|
||||||
|
Kimi Code uses a dedicated endpoint and key (separate from Moonshot):
|
||||||
|
|
||||||
|
- Provider: `kimi-code`
|
||||||
|
- Auth: `KIMICODE_API_KEY`
|
||||||
|
- Example model: `kimi-code/kimi-for-coding`
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
env: { KIMICODE_API_KEY: "sk-..." },
|
||||||
|
agents: {
|
||||||
|
defaults: { model: { primary: "kimi-code/kimi-for-coding" } }
|
||||||
|
},
|
||||||
|
models: {
|
||||||
|
mode: "merge",
|
||||||
|
providers: {
|
||||||
|
"kimi-code": {
|
||||||
|
baseUrl: "https://api.kimi.com/coding/v1",
|
||||||
|
apiKey: "${KIMICODE_API_KEY}",
|
||||||
|
api: "openai-completions",
|
||||||
|
models: [{ id: "kimi-for-coding", name: "Kimi For Coding" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Synthetic
|
### Synthetic
|
||||||
|
|
||||||
Synthetic provides Anthropic-compatible models behind the `synthetic` provider:
|
Synthetic provides Anthropic-compatible models behind the `synthetic` provider:
|
||||||
|
|||||||
@@ -894,7 +894,6 @@
|
|||||||
"gateway/heartbeat",
|
"gateway/heartbeat",
|
||||||
"gateway/doctor",
|
"gateway/doctor",
|
||||||
"gateway/logging",
|
"gateway/logging",
|
||||||
"logging",
|
|
||||||
"gateway/security",
|
"gateway/security",
|
||||||
"gateway/sandbox-vs-tool-policy-vs-elevated",
|
"gateway/sandbox-vs-tool-policy-vs-elevated",
|
||||||
"gateway/sandboxing",
|
"gateway/sandboxing",
|
||||||
|
|||||||
@@ -2234,6 +2234,49 @@ Notes:
|
|||||||
- Model ref: `moonshot/kimi-k2-0905-preview`.
|
- Model ref: `moonshot/kimi-k2-0905-preview`.
|
||||||
- Use `https://api.moonshot.cn/v1` if you need the China endpoint.
|
- Use `https://api.moonshot.cn/v1` if you need the China endpoint.
|
||||||
|
|
||||||
|
### Kimi Code
|
||||||
|
|
||||||
|
Use Kimi Code's dedicated OpenAI-compatible endpoint (separate from Moonshot):
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
env: { KIMICODE_API_KEY: "sk-..." },
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: { primary: "kimi-code/kimi-for-coding" },
|
||||||
|
models: { "kimi-code/kimi-for-coding": { alias: "Kimi Code" } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
models: {
|
||||||
|
mode: "merge",
|
||||||
|
providers: {
|
||||||
|
"kimi-code": {
|
||||||
|
baseUrl: "https://api.kimi.com/coding/v1",
|
||||||
|
apiKey: "${KIMICODE_API_KEY}",
|
||||||
|
api: "openai-completions",
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: "kimi-for-coding",
|
||||||
|
name: "Kimi For Coding",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
|
contextWindow: 262144,
|
||||||
|
maxTokens: 32768,
|
||||||
|
headers: { "User-Agent": "KimiCLI/0.77" },
|
||||||
|
compat: { supportsDeveloperRole: false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Set `KIMICODE_API_KEY` in the environment or use `clawdbot onboard --auth-choice kimi-code-api-key`.
|
||||||
|
- Model ref: `kimi-code/kimi-for-coding`.
|
||||||
|
|
||||||
### Synthetic (Anthropic-compatible)
|
### Synthetic (Anthropic-compatible)
|
||||||
|
|
||||||
Use Synthetic's Anthropic-compatible endpoint:
|
Use Synthetic's Anthropic-compatible endpoint:
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/etc.)? See [Chann
|
|||||||
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
|
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
|
||||||
- [OpenRouter](/providers/openrouter)
|
- [OpenRouter](/providers/openrouter)
|
||||||
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
||||||
- [Moonshot AI (Kimi)](/providers/moonshot)
|
- [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot)
|
||||||
- [OpenCode Zen](/providers/opencode)
|
- [OpenCode Zen](/providers/opencode)
|
||||||
- [Z.AI](/providers/zai)
|
- [Z.AI](/providers/zai)
|
||||||
- [GLM models](/providers/glm)
|
- [GLM models](/providers/glm)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ model as `provider/model`.
|
|||||||
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
|
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
|
||||||
- [OpenRouter](/providers/openrouter)
|
- [OpenRouter](/providers/openrouter)
|
||||||
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
||||||
- [Moonshot AI (Kimi)](/providers/moonshot)
|
- [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot)
|
||||||
- [Synthetic](/providers/synthetic)
|
- [Synthetic](/providers/synthetic)
|
||||||
- [OpenCode Zen](/providers/opencode)
|
- [OpenCode Zen](/providers/opencode)
|
||||||
- [Z.AI](/providers/zai)
|
- [Z.AI](/providers/zai)
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
---
|
---
|
||||||
summary: "Use Moonshot AI (Kimi K2) with Clawdbot"
|
summary: "Configure Moonshot K2 vs Kimi Code (separate providers + keys)"
|
||||||
read_when:
|
read_when:
|
||||||
- You want to use Moonshot/Kimi models in Clawdbot
|
- You want Moonshot K2 (Moonshot Open Platform) vs Kimi Code setup
|
||||||
- You need the Moonshot auth + config example
|
- You need to understand separate endpoints, keys, and model refs
|
||||||
|
- You want copy/paste config for either provider
|
||||||
---
|
---
|
||||||
|
|
||||||
# Moonshot AI (Kimi)
|
# Moonshot AI (Kimi)
|
||||||
|
|
||||||
Moonshot provides the Kimi API with OpenAI-compatible endpoints. Configure the
|
Moonshot provides the Kimi API with OpenAI-compatible endpoints. Configure the
|
||||||
provider and set the default model to `moonshot/kimi-k2-0905-preview`.
|
provider and set the default model to `moonshot/kimi-k2-0905-preview`, or use
|
||||||
|
Kimi Code with `kimi-code/kimi-for-coding`.
|
||||||
|
|
||||||
Current Kimi K2 model IDs:
|
Current Kimi K2 model IDs:
|
||||||
{/* moonshot-kimi-k2-ids:start */}
|
{/* moonshot-kimi-k2-ids:start */}
|
||||||
@@ -21,7 +24,15 @@ Current Kimi K2 model IDs:
|
|||||||
clawdbot onboard --auth-choice moonshot-api-key
|
clawdbot onboard --auth-choice moonshot-api-key
|
||||||
```
|
```
|
||||||
|
|
||||||
## Config snippet
|
Kimi Code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot onboard --auth-choice kimi-code-api-key
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Moonshot and Kimi Code are separate providers. Keys are not interchangeable, endpoints differ, and model refs differ (Moonshot uses `moonshot/...`, Kimi Code uses `kimi-code/...`).
|
||||||
|
|
||||||
|
## Config snippet (Moonshot API)
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
@@ -92,9 +103,48 @@ clawdbot onboard --auth-choice moonshot-api-key
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Kimi Code
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
env: { KIMICODE_API_KEY: "sk-..." },
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: { primary: "kimi-code/kimi-for-coding" },
|
||||||
|
models: {
|
||||||
|
"kimi-code/kimi-for-coding": { alias: "Kimi Code" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
models: {
|
||||||
|
mode: "merge",
|
||||||
|
providers: {
|
||||||
|
"kimi-code": {
|
||||||
|
baseUrl: "https://api.kimi.com/coding/v1",
|
||||||
|
apiKey: "${KIMICODE_API_KEY}",
|
||||||
|
api: "openai-completions",
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: "kimi-for-coding",
|
||||||
|
name: "Kimi For Coding",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
|
contextWindow: 262144,
|
||||||
|
maxTokens: 32768,
|
||||||
|
headers: { "User-Agent": "KimiCLI/0.77" },
|
||||||
|
compat: { supportsDeveloperRole: false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Model refs use `moonshot/<modelId>`.
|
- Moonshot model refs use `moonshot/<modelId>`. Kimi Code model refs use `kimi-code/<modelId>`.
|
||||||
- Override pricing and context metadata in `models.providers` if needed.
|
- Override pricing and context metadata in `models.providers` if needed.
|
||||||
- If Moonshot publishes different context limits for a model, adjust
|
- If Moonshot publishes different context limits for a model, adjust
|
||||||
`contextWindow` accordingly.
|
`contextWindow` accordingly.
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ read_when:
|
|||||||
|
|
||||||
Use `pnpm` (Node 22+) from the repo root. Keep the working tree clean before tagging/publishing.
|
Use `pnpm` (Node 22+) from the repo root. Keep the working tree clean before tagging/publishing.
|
||||||
|
|
||||||
|
## Operator trigger
|
||||||
|
When the operator says “release”, immediately do this preflight (no extra questions unless blocked):
|
||||||
|
- Read this doc and `docs/platforms/mac/release.md`.
|
||||||
|
- Load env from `~/.profile` and confirm `SPARKLE_PRIVATE_KEY_FILE` + App Store Connect vars are set.
|
||||||
|
- Use Sparkle keys from `~/Library/CloudStorage/Dropbox/Backup/Sparkle` if needed.
|
||||||
|
|
||||||
1) **Version & metadata**
|
1) **Version & metadata**
|
||||||
- [ ] Bump `package.json` version (e.g., `1.1.0`).
|
- [ ] Bump `package.json` version (e.g., `1.1.0`).
|
||||||
- [ ] Run `pnpm plugins:sync` to align extension package versions + changelogs.
|
- [ ] Run `pnpm plugins:sync` to align extension package versions + changelogs.
|
||||||
|
|||||||
@@ -95,7 +95,8 @@ Tip: `--json` does **not** imply non-interactive mode. Use `--non-interactive` (
|
|||||||
- **Synthetic (Anthropic-compatible)**: prompts for `SYNTHETIC_API_KEY`.
|
- **Synthetic (Anthropic-compatible)**: prompts for `SYNTHETIC_API_KEY`.
|
||||||
- More detail: [Synthetic](/providers/synthetic)
|
- More detail: [Synthetic](/providers/synthetic)
|
||||||
- **Moonshot (Kimi K2)**: config is auto-written.
|
- **Moonshot (Kimi K2)**: config is auto-written.
|
||||||
- More detail: [Moonshot AI](/providers/moonshot)
|
- **Kimi Code**: config is auto-written.
|
||||||
|
- More detail: [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot)
|
||||||
- **Skip**: no auth configured yet.
|
- **Skip**: no auth configured yet.
|
||||||
- Pick a default model from detected options (or enter provider/model manually).
|
- Pick a default model from detected options (or enter provider/model manually).
|
||||||
- Wizard runs a model check and warns if the configured model is unknown or missing auth.
|
- Wizard runs a model check and warns if the configured model is unknown or missing auth.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { MatrixClient } from "matrix-js-sdk";
|
|||||||
|
|
||||||
import { chunkMarkdownText } from "../../../../../src/auto-reply/chunk.js";
|
import { chunkMarkdownText } from "../../../../../src/auto-reply/chunk.js";
|
||||||
import type { ReplyPayload } from "../../../../../src/auto-reply/types.js";
|
import type { ReplyPayload } from "../../../../../src/auto-reply/types.js";
|
||||||
import { danger } from "../../../../../src/globals.js";
|
import { danger, logVerbose } from "../../../../../src/globals.js";
|
||||||
import type { RuntimeEnv } from "../../../../../src/runtime.js";
|
import type { RuntimeEnv } from "../../../../../src/runtime.js";
|
||||||
import { sendMessageMatrix } from "../send.js";
|
import { sendMessageMatrix } from "../send.js";
|
||||||
|
|
||||||
@@ -18,7 +18,12 @@ export async function deliverMatrixReplies(params: {
|
|||||||
const chunkLimit = Math.min(params.textLimit, 4000);
|
const chunkLimit = Math.min(params.textLimit, 4000);
|
||||||
let hasReplied = false;
|
let hasReplied = false;
|
||||||
for (const reply of params.replies) {
|
for (const reply of params.replies) {
|
||||||
if (!reply?.text && !reply?.mediaUrl && !(reply?.mediaUrls?.length ?? 0)) {
|
const hasMedia = Boolean(reply?.mediaUrl) || (reply?.mediaUrls?.length ?? 0) > 0;
|
||||||
|
if (!reply?.text && !hasMedia) {
|
||||||
|
if (reply?.audioAsVoice) {
|
||||||
|
logVerbose("matrix reply has audioAsVoice without media/text; skipping");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
params.runtime.error?.(danger("matrix reply missing text/media"));
|
params.runtime.error?.(danger("matrix reply missing text/media"));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -57,6 +62,7 @@ export async function deliverMatrixReplies(params: {
|
|||||||
mediaUrl,
|
mediaUrl,
|
||||||
replyToId: shouldIncludeReply(replyToId) ? replyToId : undefined,
|
replyToId: shouldIncludeReply(replyToId) ? replyToId : undefined,
|
||||||
threadId: params.threadId,
|
threadId: params.threadId,
|
||||||
|
audioAsVoice: reply.audioAsVoice,
|
||||||
});
|
});
|
||||||
if (shouldIncludeReply(replyToId)) {
|
if (shouldIncludeReply(replyToId)) {
|
||||||
hasReplied = true;
|
hasReplied = true;
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { parsePollStartContent } from "./poll-types.js";
|
||||||
|
|
||||||
|
describe("parsePollStartContent", () => {
|
||||||
|
it("parses legacy m.poll payloads", () => {
|
||||||
|
const summary = parsePollStartContent({
|
||||||
|
"m.poll": {
|
||||||
|
question: { "m.text": "Lunch?" },
|
||||||
|
kind: "m.poll.disclosed",
|
||||||
|
max_selections: 1,
|
||||||
|
answers: [
|
||||||
|
{ id: "answer1", "m.text": "Yes" },
|
||||||
|
{ id: "answer2", "m.text": "No" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(summary?.question).toBe("Lunch?");
|
||||||
|
expect(summary?.answers).toEqual(["Yes", "No"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,15 +7,17 @@
|
|||||||
* - m.poll.end - Closes a poll
|
* - m.poll.end - Closes a poll
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { TimelineEvents } from "matrix-js-sdk/lib/@types/event.js";
|
||||||
|
import type { ExtensibleAnyMessageEventContent } from "matrix-js-sdk/lib/@types/extensible_events.js";
|
||||||
import type { PollInput } from "../../../../src/polls.js";
|
import type { PollInput } from "../../../../src/polls.js";
|
||||||
|
|
||||||
export const M_POLL_START = "m.poll.start";
|
export const M_POLL_START = "m.poll.start" as const;
|
||||||
export const M_POLL_RESPONSE = "m.poll.response";
|
export const M_POLL_RESPONSE = "m.poll.response" as const;
|
||||||
export const M_POLL_END = "m.poll.end";
|
export const M_POLL_END = "m.poll.end" as const;
|
||||||
|
|
||||||
export const ORG_POLL_START = "org.matrix.msc3381.poll.start";
|
export const ORG_POLL_START = "org.matrix.msc3381.poll.start" as const;
|
||||||
export const ORG_POLL_RESPONSE = "org.matrix.msc3381.poll.response";
|
export const ORG_POLL_RESPONSE = "org.matrix.msc3381.poll.response" as const;
|
||||||
export const ORG_POLL_END = "org.matrix.msc3381.poll.end";
|
export const ORG_POLL_END = "org.matrix.msc3381.poll.end" as const;
|
||||||
|
|
||||||
export const POLL_EVENT_TYPES = [
|
export const POLL_EVENT_TYPES = [
|
||||||
M_POLL_START,
|
M_POLL_START,
|
||||||
@@ -32,9 +34,7 @@ export const POLL_END_TYPES = [M_POLL_END, ORG_POLL_END];
|
|||||||
|
|
||||||
export type PollKind = "m.poll.disclosed" | "m.poll.undisclosed";
|
export type PollKind = "m.poll.disclosed" | "m.poll.undisclosed";
|
||||||
|
|
||||||
export type TextContent = {
|
export type TextContent = ExtensibleAnyMessageEventContent & {
|
||||||
"m.text"?: string;
|
|
||||||
"org.matrix.msc1767.text"?: string;
|
|
||||||
body?: string;
|
body?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,25 +42,19 @@ export type PollAnswer = {
|
|||||||
id: string;
|
id: string;
|
||||||
} & TextContent;
|
} & TextContent;
|
||||||
|
|
||||||
export type PollStartContent = {
|
export type PollStartSubtype = {
|
||||||
"m.poll"?: {
|
question: TextContent;
|
||||||
question: TextContent;
|
kind?: PollKind;
|
||||||
kind?: PollKind;
|
max_selections?: number;
|
||||||
max_selections?: number;
|
answers: PollAnswer[];
|
||||||
answers: PollAnswer[];
|
|
||||||
};
|
|
||||||
"org.matrix.msc3381.poll.start"?: {
|
|
||||||
question: TextContent;
|
|
||||||
kind?: PollKind;
|
|
||||||
max_selections?: number;
|
|
||||||
answers: PollAnswer[];
|
|
||||||
};
|
|
||||||
"m.relates_to"?: {
|
|
||||||
rel_type: "m.reference";
|
|
||||||
event_id: string;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LegacyPollStartContent = {
|
||||||
|
"m.poll"?: PollStartSubtype;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PollStartContent = TimelineEvents[typeof M_POLL_START] | LegacyPollStartContent;
|
||||||
|
|
||||||
export type PollSummary = {
|
export type PollSummary = {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
@@ -82,7 +76,9 @@ export function getTextContent(text?: TextContent): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function parsePollStartContent(content: PollStartContent): PollSummary | null {
|
export function parsePollStartContent(content: PollStartContent): PollSummary | null {
|
||||||
const poll = content["m.poll"] ?? content["org.matrix.msc3381.poll.start"];
|
const poll = (content as Record<string, PollStartSubtype | undefined>)[M_POLL_START]
|
||||||
|
?? (content as Record<string, PollStartSubtype | undefined>)[ORG_POLL_START]
|
||||||
|
?? (content as Record<string, PollStartSubtype | undefined>)["m.poll"];
|
||||||
if (!poll) return null;
|
if (!poll) return null;
|
||||||
|
|
||||||
const question = getTextContent(poll.question);
|
const question = getTextContent(poll.question);
|
||||||
@@ -121,6 +117,11 @@ function buildTextContent(body: string): TextContent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildPollFallbackText(question: string, answers: string[]): string {
|
||||||
|
if (answers.length === 0) return question;
|
||||||
|
return `${question}\n${answers.map((answer, idx) => `${idx + 1}. ${answer}`).join("\n")}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildPollStartContent(poll: PollInput): PollStartContent {
|
export function buildPollStartContent(poll: PollInput): PollStartContent {
|
||||||
const question = poll.question.trim();
|
const question = poll.question.trim();
|
||||||
const answers = poll.options
|
const answers = poll.options
|
||||||
@@ -132,13 +133,19 @@ export function buildPollStartContent(poll: PollInput): PollStartContent {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const maxSelections = poll.multiple ? Math.max(1, answers.length) : 1;
|
const maxSelections = poll.multiple ? Math.max(1, answers.length) : 1;
|
||||||
|
const fallbackText = buildPollFallbackText(
|
||||||
|
question,
|
||||||
|
answers.map((answer) => getTextContent(answer)),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"m.poll": {
|
[M_POLL_START]: {
|
||||||
question: buildTextContent(question),
|
question: buildTextContent(question),
|
||||||
kind: poll.multiple ? "m.poll.undisclosed" : "m.poll.disclosed",
|
kind: poll.multiple ? "m.poll.undisclosed" : "m.poll.disclosed",
|
||||||
max_selections: maxSelections,
|
max_selections: maxSelections,
|
||||||
answers,
|
answers,
|
||||||
},
|
},
|
||||||
|
"m.text": fallbackText,
|
||||||
|
"org.matrix.msc1767.text": fallbackText,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -31,6 +31,11 @@ vi.mock("../../../../src/web/media.js", () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../../../src/media/image-ops.js", () => ({
|
||||||
|
getImageMetadata: vi.fn().mockResolvedValue(null),
|
||||||
|
resizeToJpeg: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
let sendMessageMatrix: typeof import("./send.js").sendMessageMatrix;
|
let sendMessageMatrix: typeof import("./send.js").sendMessageMatrix;
|
||||||
|
|
||||||
const makeClient = () => {
|
const makeClient = () => {
|
||||||
@@ -65,13 +70,13 @@ describe("sendMessageMatrix media", () => {
|
|||||||
const uploadArg = uploadContent.mock.calls[0]?.[0];
|
const uploadArg = uploadContent.mock.calls[0]?.[0];
|
||||||
expect(Buffer.isBuffer(uploadArg)).toBe(true);
|
expect(Buffer.isBuffer(uploadArg)).toBe(true);
|
||||||
|
|
||||||
const content = sendMessage.mock.calls[0]?.[2] as {
|
const content = sendMessage.mock.calls[0]?.[1] as {
|
||||||
url?: string;
|
url?: string;
|
||||||
msgtype?: string;
|
msgtype?: string;
|
||||||
format?: string;
|
format?: string;
|
||||||
formatted_body?: string;
|
formatted_body?: string;
|
||||||
};
|
};
|
||||||
expect(content.msgtype).toBe("m.file");
|
expect(content.msgtype).toBe("m.image");
|
||||||
expect(content.format).toBe("org.matrix.custom.html");
|
expect(content.format).toBe("org.matrix.custom.html");
|
||||||
expect(content.formatted_body).toContain("caption");
|
expect(content.formatted_body).toContain("caption");
|
||||||
expect(content.url).toBe("mxc://example/file");
|
expect(content.url).toBe("mxc://example/file");
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import type { AccountDataEvents, MatrixClient } from "matrix-js-sdk";
|
import type { AccountDataEvents, MatrixClient } from "matrix-js-sdk";
|
||||||
import { EventType, MsgType, RelationType } from "matrix-js-sdk";
|
import { EventType, MsgType, RelationType } from "matrix-js-sdk";
|
||||||
import type {
|
import type {
|
||||||
ReactionEventContent,
|
|
||||||
RoomMessageEventContent,
|
RoomMessageEventContent,
|
||||||
|
ReactionEventContent,
|
||||||
} from "matrix-js-sdk/lib/@types/events.js";
|
} from "matrix-js-sdk/lib/@types/events.js";
|
||||||
|
|
||||||
import { chunkMarkdownText, resolveTextChunkLimit } from "../../../../src/auto-reply/chunk.js";
|
import { chunkMarkdownText, resolveTextChunkLimit } from "../../../../src/auto-reply/chunk.js";
|
||||||
import { loadConfig } from "../../../../src/config/config.js";
|
import { loadConfig } from "../../../../src/config/config.js";
|
||||||
|
import { isVoiceCompatibleAudio } from "../../../../src/media/audio.js";
|
||||||
|
import { mediaKindFromMime } from "../../../../src/media/constants.js";
|
||||||
|
import { getImageMetadata, resizeToJpeg } from "../../../../src/media/image-ops.js";
|
||||||
import type { PollInput } from "../../../../src/polls.js";
|
import type { PollInput } from "../../../../src/polls.js";
|
||||||
import { loadWebMedia } from "../../../../src/web/media.js";
|
import { loadWebMedia } from "../../../../src/web/media.js";
|
||||||
import { getActiveMatrixClient } from "./active-client.js";
|
import { getActiveMatrixClient } from "./active-client.js";
|
||||||
@@ -47,6 +50,8 @@ export type MatrixSendOpts = {
|
|||||||
replyToId?: string;
|
replyToId?: string;
|
||||||
threadId?: string | number | null;
|
threadId?: string | number | null;
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
|
/** Send audio as voice message (voice bubble) instead of audio file. Defaults to false. */
|
||||||
|
audioAsVoice?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function ensureNodeRuntime() {
|
function ensureNodeRuntime() {
|
||||||
@@ -71,6 +76,12 @@ function normalizeTarget(raw: string): string {
|
|||||||
return trimmed;
|
return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeThreadId(raw?: string | number | null): string | null {
|
||||||
|
if (raw === undefined || raw === null) return null;
|
||||||
|
const trimmed = String(raw).trim();
|
||||||
|
return trimmed ? trimmed : null;
|
||||||
|
}
|
||||||
|
|
||||||
async function resolveDirectRoomId(client: MatrixClient, userId: string): Promise<string> {
|
async function resolveDirectRoomId(client: MatrixClient, userId: string): Promise<string> {
|
||||||
const trimmed = userId.trim();
|
const trimmed = userId.trim();
|
||||||
if (!trimmed.startsWith("@")) {
|
if (!trimmed.startsWith("@")) {
|
||||||
@@ -119,6 +130,18 @@ export async function resolveMatrixRoomId(
|
|||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MatrixImageInfo = {
|
||||||
|
w?: number;
|
||||||
|
h?: number;
|
||||||
|
thumbnail_url?: string;
|
||||||
|
thumbnail_info?: {
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
mimetype: string;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
function buildMediaContent(params: {
|
function buildMediaContent(params: {
|
||||||
msgtype: MsgType.Image | MsgType.Audio | MsgType.Video | MsgType.File;
|
msgtype: MsgType.Image | MsgType.Audio | MsgType.Video | MsgType.File;
|
||||||
body: string;
|
body: string;
|
||||||
@@ -127,8 +150,24 @@ function buildMediaContent(params: {
|
|||||||
mimetype?: string;
|
mimetype?: string;
|
||||||
size: number;
|
size: number;
|
||||||
relation?: MatrixReplyRelation;
|
relation?: MatrixReplyRelation;
|
||||||
|
isVoice?: boolean;
|
||||||
|
durationMs?: number;
|
||||||
|
imageInfo?: MatrixImageInfo;
|
||||||
}): RoomMessageEventContent {
|
}): RoomMessageEventContent {
|
||||||
const info = { mimetype: params.mimetype, size: params.size };
|
const info: Record<string, unknown> = { mimetype: params.mimetype, size: params.size };
|
||||||
|
if (params.durationMs !== undefined) {
|
||||||
|
info.duration = params.durationMs;
|
||||||
|
}
|
||||||
|
if (params.imageInfo) {
|
||||||
|
if (params.imageInfo.w) info.w = params.imageInfo.w;
|
||||||
|
if (params.imageInfo.h) info.h = params.imageInfo.h;
|
||||||
|
if (params.imageInfo.thumbnail_url) {
|
||||||
|
info.thumbnail_url = params.imageInfo.thumbnail_url;
|
||||||
|
if (params.imageInfo.thumbnail_info) {
|
||||||
|
info.thumbnail_info = params.imageInfo.thumbnail_info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const base: MatrixMessageContent = {
|
const base: MatrixMessageContent = {
|
||||||
msgtype: params.msgtype,
|
msgtype: params.msgtype,
|
||||||
body: params.body,
|
body: params.body,
|
||||||
@@ -136,6 +175,12 @@ function buildMediaContent(params: {
|
|||||||
info,
|
info,
|
||||||
url: params.url,
|
url: params.url,
|
||||||
};
|
};
|
||||||
|
if (params.isVoice) {
|
||||||
|
base["org.matrix.msc3245.voice"] = {};
|
||||||
|
base["org.matrix.msc1767.audio"] = {
|
||||||
|
duration: params.durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
if (params.relation) {
|
if (params.relation) {
|
||||||
base["m.relates_to"] = params.relation;
|
base["m.relates_to"] = params.relation;
|
||||||
}
|
}
|
||||||
@@ -171,6 +216,75 @@ function buildReplyRelation(replyToId?: string): MatrixReplyRelation | undefined
|
|||||||
return { "m.in_reply_to": { event_id: trimmed } };
|
return { "m.in_reply_to": { event_id: trimmed } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveMatrixMsgType(
|
||||||
|
contentType?: string,
|
||||||
|
fileName?: string,
|
||||||
|
): MsgType.Image | MsgType.Audio | MsgType.Video | MsgType.File {
|
||||||
|
const kind = mediaKindFromMime(contentType ?? "");
|
||||||
|
switch (kind) {
|
||||||
|
case "image":
|
||||||
|
return MsgType.Image;
|
||||||
|
case "audio":
|
||||||
|
return MsgType.Audio;
|
||||||
|
case "video":
|
||||||
|
return MsgType.Video;
|
||||||
|
default:
|
||||||
|
return MsgType.File;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMatrixVoiceDecision(opts: {
|
||||||
|
wantsVoice: boolean;
|
||||||
|
contentType?: string;
|
||||||
|
fileName?: string;
|
||||||
|
}): { useVoice: boolean } {
|
||||||
|
if (!opts.wantsVoice) return { useVoice: false };
|
||||||
|
if (isVoiceCompatibleAudio({ contentType: opts.contentType, fileName: opts.fileName })) {
|
||||||
|
return { useVoice: true };
|
||||||
|
}
|
||||||
|
return { useVoice: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const THUMBNAIL_MAX_SIDE = 800;
|
||||||
|
const THUMBNAIL_QUALITY = 80;
|
||||||
|
|
||||||
|
async function prepareImageInfo(params: {
|
||||||
|
buffer: Buffer;
|
||||||
|
client: MatrixClient;
|
||||||
|
}): Promise<MatrixImageInfo | undefined> {
|
||||||
|
const meta = await getImageMetadata(params.buffer).catch(() => null);
|
||||||
|
if (!meta) return undefined;
|
||||||
|
const imageInfo: MatrixImageInfo = { w: meta.width, h: meta.height };
|
||||||
|
const maxDim = Math.max(meta.width, meta.height);
|
||||||
|
if (maxDim > THUMBNAIL_MAX_SIDE) {
|
||||||
|
try {
|
||||||
|
const thumbBuffer = await resizeToJpeg({
|
||||||
|
buffer: params.buffer,
|
||||||
|
maxSide: THUMBNAIL_MAX_SIDE,
|
||||||
|
quality: THUMBNAIL_QUALITY,
|
||||||
|
withoutEnlargement: true,
|
||||||
|
});
|
||||||
|
const thumbMeta = await getImageMetadata(thumbBuffer).catch(() => null);
|
||||||
|
const thumbUri = await params.client.uploadContent(thumbBuffer as MatrixUploadContent, {
|
||||||
|
type: "image/jpeg",
|
||||||
|
name: "thumbnail.jpg",
|
||||||
|
});
|
||||||
|
imageInfo.thumbnail_url = thumbUri.content_uri;
|
||||||
|
if (thumbMeta) {
|
||||||
|
imageInfo.thumbnail_info = {
|
||||||
|
w: thumbMeta.width,
|
||||||
|
h: thumbMeta.height,
|
||||||
|
mimetype: "image/jpeg",
|
||||||
|
size: thumbBuffer.byteLength,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Thumbnail generation failed, continue without it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return imageInfo;
|
||||||
|
}
|
||||||
|
|
||||||
async function uploadFile(
|
async function uploadFile(
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
file: MatrixUploadContent | Buffer,
|
file: MatrixUploadContent | Buffer,
|
||||||
@@ -238,14 +352,10 @@ export async function sendMessageMatrix(
|
|||||||
const textLimit = resolveTextChunkLimit(cfg, "matrix");
|
const textLimit = resolveTextChunkLimit(cfg, "matrix");
|
||||||
const chunkLimit = Math.min(textLimit, MATRIX_TEXT_LIMIT);
|
const chunkLimit = Math.min(textLimit, MATRIX_TEXT_LIMIT);
|
||||||
const chunks = chunkMarkdownText(trimmedMessage, chunkLimit);
|
const chunks = chunkMarkdownText(trimmedMessage, chunkLimit);
|
||||||
const rawThreadId = opts.threadId;
|
const threadId = normalizeThreadId(opts.threadId);
|
||||||
const threadId =
|
|
||||||
rawThreadId !== undefined && rawThreadId !== null
|
|
||||||
? String(rawThreadId).trim()
|
|
||||||
: null;
|
|
||||||
const relation = threadId ? undefined : buildReplyRelation(opts.replyToId);
|
const relation = threadId ? undefined : buildReplyRelation(opts.replyToId);
|
||||||
const sendContent = (content: RoomMessageEventContent) =>
|
const sendContent = (content: RoomMessageEventContent) =>
|
||||||
client.sendMessage(roomId, threadId ?? undefined, content);
|
threadId ? client.sendMessage(roomId, threadId, content) : client.sendMessage(roomId, content);
|
||||||
|
|
||||||
let lastMessageId = "";
|
let lastMessageId = "";
|
||||||
if (opts.mediaUrl) {
|
if (opts.mediaUrl) {
|
||||||
@@ -255,9 +365,17 @@ export async function sendMessageMatrix(
|
|||||||
contentType: media.contentType,
|
contentType: media.contentType,
|
||||||
filename: media.fileName,
|
filename: media.fileName,
|
||||||
});
|
});
|
||||||
const msgtype = MsgType.File;
|
const baseMsgType = resolveMatrixMsgType(media.contentType, media.fileName);
|
||||||
|
const { useVoice } = resolveMatrixVoiceDecision({
|
||||||
|
wantsVoice: opts.audioAsVoice === true,
|
||||||
|
contentType: media.contentType,
|
||||||
|
fileName: media.fileName,
|
||||||
|
});
|
||||||
|
const msgtype = useVoice ? MsgType.Audio : baseMsgType;
|
||||||
|
const isImage = msgtype === MsgType.Image;
|
||||||
|
const imageInfo = isImage ? await prepareImageInfo({ buffer: media.buffer, client }) : undefined;
|
||||||
const [firstChunk, ...rest] = chunks;
|
const [firstChunk, ...rest] = chunks;
|
||||||
const body = firstChunk ?? media.fileName ?? "(file)";
|
const body = useVoice ? "Voice message" : (firstChunk ?? media.fileName ?? "(file)");
|
||||||
const content = buildMediaContent({
|
const content = buildMediaContent({
|
||||||
msgtype,
|
msgtype,
|
||||||
body,
|
body,
|
||||||
@@ -266,10 +384,13 @@ export async function sendMessageMatrix(
|
|||||||
mimetype: media.contentType,
|
mimetype: media.contentType,
|
||||||
size: media.buffer.byteLength,
|
size: media.buffer.byteLength,
|
||||||
relation,
|
relation,
|
||||||
|
isVoice: useVoice,
|
||||||
|
imageInfo,
|
||||||
});
|
});
|
||||||
const response = await sendContent(content);
|
const response = await sendContent(content);
|
||||||
lastMessageId = response.event_id ?? lastMessageId;
|
lastMessageId = response.event_id ?? lastMessageId;
|
||||||
for (const chunk of rest) {
|
const textChunks = useVoice ? chunks : rest;
|
||||||
|
for (const chunk of textChunks) {
|
||||||
const text = chunk.trim();
|
const text = chunk.trim();
|
||||||
if (!text) continue;
|
if (!text) continue;
|
||||||
const followup = buildTextContent(text);
|
const followup = buildTextContent(text);
|
||||||
@@ -316,17 +437,19 @@ export async function sendPollMatrix(
|
|||||||
try {
|
try {
|
||||||
const roomId = await resolveMatrixRoomId(client, to);
|
const roomId = await resolveMatrixRoomId(client, to);
|
||||||
const pollContent = buildPollStartContent(poll);
|
const pollContent = buildPollStartContent(poll);
|
||||||
const rawThreadId = opts.threadId;
|
const threadId = normalizeThreadId(opts.threadId);
|
||||||
const threadId =
|
const response = threadId
|
||||||
rawThreadId !== undefined && rawThreadId !== null
|
? await client.sendEvent(
|
||||||
? String(rawThreadId).trim()
|
roomId,
|
||||||
: null;
|
threadId,
|
||||||
const response = await client.sendEvent(
|
M_POLL_START,
|
||||||
roomId,
|
pollContent,
|
||||||
threadId ?? undefined,
|
)
|
||||||
M_POLL_START as EventType.RoomMessage,
|
: await client.sendEvent(
|
||||||
pollContent as unknown as RoomMessageEventContent,
|
roomId,
|
||||||
);
|
M_POLL_START,
|
||||||
|
pollContent,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
eventId: response.event_id ?? "unknown",
|
eventId: response.event_id ?? "unknown",
|
||||||
|
|||||||
Whitespace-only changes.
Generated
+6
@@ -232,6 +232,12 @@ importers:
|
|||||||
specifier: 3.14.5
|
specifier: 3.14.5
|
||||||
version: 3.14.5(typescript@5.9.3)
|
version: 3.14.5(typescript@5.9.3)
|
||||||
|
|
||||||
|
extensions/copilot-proxy: {}
|
||||||
|
|
||||||
|
extensions/google-antigravity-auth: {}
|
||||||
|
|
||||||
|
extensions/google-gemini-cli-auth: {}
|
||||||
|
|
||||||
extensions/matrix:
|
extensions/matrix:
|
||||||
dependencies:
|
dependencies:
|
||||||
markdown-it:
|
markdown-it:
|
||||||
|
|||||||
@@ -168,24 +168,33 @@ TRASH
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select_skip_hooks() {
|
||||||
|
# Hooks multiselect: pick "Skip for now".
|
||||||
|
wait_for_log "Enable hooks?" 60 || true
|
||||||
|
send $'"'"' \r'"'"' 0.6
|
||||||
|
}
|
||||||
|
|
||||||
send_local_basic() {
|
send_local_basic() {
|
||||||
# Risk acknowledgement (default is "No").
|
# Risk acknowledgement (default is "No").
|
||||||
send $'"'"'y\r'"'"' 0.6
|
send $'"'"'y\r'"'"' 0.6
|
||||||
# Choose local gateway, accept defaults, skip channels/skills/daemon, skip UI.
|
# Choose local gateway, accept defaults, skip channels/skills/daemon, skip UI.
|
||||||
send $'"'"'\r'"'"' 0.5
|
send $'"'"'\r'"'"' 0.5
|
||||||
|
select_skip_hooks
|
||||||
}
|
}
|
||||||
|
|
||||||
send_reset_config_only() {
|
send_reset_config_only() {
|
||||||
# Risk acknowledgement (default is "No").
|
# Risk acknowledgement (default is "No").
|
||||||
send $'"'"'y\r'"'"' 0.8
|
send $'"'"'y\r'"'"' 0.8
|
||||||
# Reset config + reuse the local defaults flow.
|
# Select reset flow for existing config.
|
||||||
send $'"'"'\e[B'"'"' 0.3
|
wait_for_log "Config handling" 40 || true
|
||||||
send $'"'"'\e[B'"'"' 0.3
|
send $'"'"'\e[B'"'"' 0.3
|
||||||
send $'"'"'\r'"'"' 0.4
|
send $'"'"'\e[B'"'"' 0.3
|
||||||
send $'"'"'\r'"'"' 0.4
|
send $'"'"'\r'"'"' 0.4
|
||||||
send "" 1.2
|
# Reset scope -> Config only (default).
|
||||||
send_local_basic
|
wait_for_log "Reset scope" 40 || true
|
||||||
}
|
send $'"'"'\r'"'"' 0.4
|
||||||
|
select_skip_hooks
|
||||||
|
}
|
||||||
|
|
||||||
send_channels_flow() {
|
send_channels_flow() {
|
||||||
# Configure channels via configure wizard.
|
# Configure channels via configure wizard.
|
||||||
@@ -202,11 +211,13 @@ TRASH
|
|||||||
|
|
||||||
send_skills_flow() {
|
send_skills_flow() {
|
||||||
# Select skills section and skip optional installs.
|
# Select skills section and skip optional installs.
|
||||||
send $'"'"'\r'"'"' 1.2
|
wait_for_log "Where will the Gateway run?" 40 || true
|
||||||
send "" 1.0
|
send $'"'"'\r'"'"' 0.8
|
||||||
# Configure skills now? -> No
|
# Configure skills now? -> No
|
||||||
send $'"'"'n\r'"'"' 1.2
|
wait_for_log "Configure skills now?" 40 || true
|
||||||
send "" 2.0
|
send $'"'"'n\r'"'"' 0.8
|
||||||
|
wait_for_log "Configure complete." 40 || true
|
||||||
|
send "" 0.8
|
||||||
}
|
}
|
||||||
|
|
||||||
run_case_local_basic() {
|
run_case_local_basic() {
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
|||||||
openrouter: "OPENROUTER_API_KEY",
|
openrouter: "OPENROUTER_API_KEY",
|
||||||
"vercel-ai-gateway": "AI_GATEWAY_API_KEY",
|
"vercel-ai-gateway": "AI_GATEWAY_API_KEY",
|
||||||
moonshot: "MOONSHOT_API_KEY",
|
moonshot: "MOONSHOT_API_KEY",
|
||||||
|
"kimi-code": "KIMICODE_API_KEY",
|
||||||
minimax: "MINIMAX_API_KEY",
|
minimax: "MINIMAX_API_KEY",
|
||||||
synthetic: "SYNTHETIC_API_KEY",
|
synthetic: "SYNTHETIC_API_KEY",
|
||||||
mistral: "MISTRAL_API_KEY",
|
mistral: "MISTRAL_API_KEY",
|
||||||
|
|||||||
@@ -37,6 +37,18 @@ const MOONSHOT_DEFAULT_COST = {
|
|||||||
cacheRead: 0,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
};
|
};
|
||||||
|
const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
|
||||||
|
const KIMI_CODE_MODEL_ID = "kimi-for-coding";
|
||||||
|
const KIMI_CODE_CONTEXT_WINDOW = 262144;
|
||||||
|
const KIMI_CODE_MAX_TOKENS = 32768;
|
||||||
|
const KIMI_CODE_HEADERS = { "User-Agent": "KimiCLI/0.77" } as const;
|
||||||
|
const KIMI_CODE_COMPAT = { supportsDeveloperRole: false } as const;
|
||||||
|
const KIMI_CODE_DEFAULT_COST = {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
};
|
||||||
|
|
||||||
function normalizeApiKeyConfig(value: string): string {
|
function normalizeApiKeyConfig(value: string): string {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
@@ -184,6 +196,26 @@ function buildMoonshotProvider(): ProviderConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildKimiCodeProvider(): ProviderConfig {
|
||||||
|
return {
|
||||||
|
baseUrl: KIMI_CODE_BASE_URL,
|
||||||
|
api: "openai-completions",
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: KIMI_CODE_MODEL_ID,
|
||||||
|
name: "Kimi For Coding",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
cost: KIMI_CODE_DEFAULT_COST,
|
||||||
|
contextWindow: KIMI_CODE_CONTEXT_WINDOW,
|
||||||
|
maxTokens: KIMI_CODE_MAX_TOKENS,
|
||||||
|
headers: KIMI_CODE_HEADERS,
|
||||||
|
compat: KIMI_CODE_COMPAT,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildSyntheticProvider(): ProviderConfig {
|
function buildSyntheticProvider(): ProviderConfig {
|
||||||
return {
|
return {
|
||||||
baseUrl: SYNTHETIC_BASE_URL,
|
baseUrl: SYNTHETIC_BASE_URL,
|
||||||
@@ -212,6 +244,13 @@ export function resolveImplicitProviders(params: { agentDir: string }): ModelsCo
|
|||||||
providers.moonshot = { ...buildMoonshotProvider(), apiKey: moonshotKey };
|
providers.moonshot = { ...buildMoonshotProvider(), apiKey: moonshotKey };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const kimiCodeKey =
|
||||||
|
resolveEnvApiKeyVarName("kimi-code") ??
|
||||||
|
resolveApiKeyFromProfiles({ provider: "kimi-code", store: authStore });
|
||||||
|
if (kimiCodeKey) {
|
||||||
|
providers["kimi-code"] = { ...buildKimiCodeProvider(), apiKey: kimiCodeKey };
|
||||||
|
}
|
||||||
|
|
||||||
const syntheticKey =
|
const syntheticKey =
|
||||||
resolveEnvApiKeyVarName("synthetic") ??
|
resolveEnvApiKeyVarName("synthetic") ??
|
||||||
resolveApiKeyFromProfiles({ provider: "synthetic", store: authStore });
|
resolveApiKeyFromProfiles({ provider: "synthetic", store: authStore });
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ function buildSendSchema(options: { includeButtons: boolean }) {
|
|||||||
media: Type.Optional(Type.String()),
|
media: Type.Optional(Type.String()),
|
||||||
replyTo: Type.Optional(Type.String()),
|
replyTo: Type.Optional(Type.String()),
|
||||||
threadId: Type.Optional(Type.String()),
|
threadId: Type.Optional(Type.String()),
|
||||||
|
asVoice: Type.Optional(Type.Boolean()),
|
||||||
bestEffort: Type.Optional(Type.Boolean()),
|
bestEffort: Type.Optional(Type.Boolean()),
|
||||||
gifPlayback: Type.Optional(Type.Boolean()),
|
gifPlayback: Type.Optional(Type.Boolean()),
|
||||||
buttons: Type.Optional(
|
buttons: Type.Optional(
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ export async function handleTelegramAction(
|
|||||||
buttons,
|
buttons,
|
||||||
replyToMessageId: replyToMessageId ?? undefined,
|
replyToMessageId: replyToMessageId ?? undefined,
|
||||||
messageThreadId: messageThreadId ?? undefined,
|
messageThreadId: messageThreadId ?? undefined,
|
||||||
|
asVoice: typeof params.asVoice === "boolean" ? params.asVoice : undefined,
|
||||||
});
|
});
|
||||||
return jsonResult({
|
return jsonResult({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { prependSystemEvents } from "./session-updates.js";
|
|||||||
describe("prependSystemEvents", () => {
|
describe("prependSystemEvents", () => {
|
||||||
it("adds a UTC timestamp to queued system events", async () => {
|
it("adds a UTC timestamp to queued system events", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const timestamp = new Date("2026-01-12T20:19:17");
|
const timestamp = new Date("2026-01-12T20:19:17Z");
|
||||||
vi.setSystemTime(timestamp);
|
vi.setSystemTime(timestamp);
|
||||||
|
|
||||||
enqueueSystemEvent("Model switched.", { sessionKey: "agent:main:main" });
|
enqueueSystemEvent("Model switched.", { sessionKey: "agent:main:main" });
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { ClawdbotConfig } from "../../../config/config.js";
|
||||||
|
import { telegramMessageActions } from "./telegram.js";
|
||||||
|
|
||||||
|
const handleTelegramAction = vi.fn(async () => ({ ok: true }));
|
||||||
|
|
||||||
|
vi.mock("../../../agents/tools/telegram-actions.js", () => ({
|
||||||
|
handleTelegramAction: (...args: unknown[]) => handleTelegramAction(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("telegramMessageActions", () => {
|
||||||
|
it("allows media-only sends and passes asVoice", async () => {
|
||||||
|
handleTelegramAction.mockClear();
|
||||||
|
const cfg = { channels: { telegram: { botToken: "tok" } } } as ClawdbotConfig;
|
||||||
|
|
||||||
|
await telegramMessageActions.handleAction({
|
||||||
|
action: "send",
|
||||||
|
params: {
|
||||||
|
to: "123",
|
||||||
|
media: "https://example.com/voice.ogg",
|
||||||
|
asVoice: true,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
accountId: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(handleTelegramAction).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
action: "sendMessage",
|
||||||
|
to: "123",
|
||||||
|
content: "",
|
||||||
|
mediaUrl: "https://example.com/voice.ogg",
|
||||||
|
asVoice: true,
|
||||||
|
}),
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -10,6 +10,29 @@ import type { ChannelMessageActionAdapter, ChannelMessageActionName } from "../t
|
|||||||
|
|
||||||
const providerId = "telegram";
|
const providerId = "telegram";
|
||||||
|
|
||||||
|
function readTelegramSendParams(params: Record<string, unknown>) {
|
||||||
|
const to = readStringParam(params, "to", { required: true });
|
||||||
|
const mediaUrl = readStringParam(params, "media", { trim: false });
|
||||||
|
const content =
|
||||||
|
readStringParam(params, "message", {
|
||||||
|
required: !mediaUrl,
|
||||||
|
allowEmpty: true,
|
||||||
|
}) ?? "";
|
||||||
|
const replyTo = readStringParam(params, "replyTo");
|
||||||
|
const threadId = readStringParam(params, "threadId");
|
||||||
|
const buttons = params.buttons;
|
||||||
|
const asVoice = typeof params.asVoice === "boolean" ? params.asVoice : undefined;
|
||||||
|
return {
|
||||||
|
to,
|
||||||
|
content,
|
||||||
|
mediaUrl: mediaUrl ?? undefined,
|
||||||
|
replyToMessageId: replyTo ?? undefined,
|
||||||
|
messageThreadId: threadId ?? undefined,
|
||||||
|
buttons,
|
||||||
|
asVoice,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const telegramMessageActions: ChannelMessageActionAdapter = {
|
export const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||||
listActions: ({ cfg }) => {
|
listActions: ({ cfg }) => {
|
||||||
const accounts = listEnabledTelegramAccounts(cfg).filter(
|
const accounts = listEnabledTelegramAccounts(cfg).filter(
|
||||||
@@ -41,25 +64,12 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
|||||||
},
|
},
|
||||||
handleAction: async ({ action, params, cfg, accountId }) => {
|
handleAction: async ({ action, params, cfg, accountId }) => {
|
||||||
if (action === "send") {
|
if (action === "send") {
|
||||||
const to = readStringParam(params, "to", { required: true });
|
const sendParams = readTelegramSendParams(params);
|
||||||
const content = readStringParam(params, "message", {
|
|
||||||
required: true,
|
|
||||||
allowEmpty: true,
|
|
||||||
});
|
|
||||||
const mediaUrl = readStringParam(params, "media", { trim: false });
|
|
||||||
const replyTo = readStringParam(params, "replyTo");
|
|
||||||
const threadId = readStringParam(params, "threadId");
|
|
||||||
const buttons = params.buttons;
|
|
||||||
return await handleTelegramAction(
|
return await handleTelegramAction(
|
||||||
{
|
{
|
||||||
action: "sendMessage",
|
action: "sendMessage",
|
||||||
to,
|
...sendParams,
|
||||||
content,
|
|
||||||
mediaUrl: mediaUrl ?? undefined,
|
|
||||||
replyToMessageId: replyTo ?? undefined,
|
|
||||||
messageThreadId: threadId ?? undefined,
|
|
||||||
accountId: accountId ?? undefined,
|
accountId: accountId ?? undefined,
|
||||||
buttons,
|
|
||||||
},
|
},
|
||||||
cfg,
|
cfg,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -180,6 +180,29 @@ describe("cli program (smoke)", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("passes kimi code api key to onboard", async () => {
|
||||||
|
const program = buildProgram();
|
||||||
|
await program.parseAsync(
|
||||||
|
[
|
||||||
|
"onboard",
|
||||||
|
"--non-interactive",
|
||||||
|
"--auth-choice",
|
||||||
|
"kimi-code-api-key",
|
||||||
|
"--kimi-code-api-key",
|
||||||
|
"sk-kimi-code-test",
|
||||||
|
],
|
||||||
|
{ from: "user" },
|
||||||
|
);
|
||||||
|
expect(onboardCommand).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
nonInteractive: true,
|
||||||
|
authChoice: "kimi-code-api-key",
|
||||||
|
kimiCodeApiKey: "sk-kimi-code-test",
|
||||||
|
}),
|
||||||
|
runtime,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("passes synthetic api key to onboard", async () => {
|
it("passes synthetic api key to onboard", async () => {
|
||||||
const program = buildProgram();
|
const program = buildProgram();
|
||||||
await program.parseAsync(
|
await program.parseAsync(
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function registerOnboardCommand(program: Command) {
|
|||||||
.option("--mode <mode>", "Wizard mode: local|remote")
|
.option("--mode <mode>", "Wizard mode: local|remote")
|
||||||
.option(
|
.option(
|
||||||
"--auth-choice <choice>",
|
"--auth-choice <choice>",
|
||||||
"Auth: setup-token|claude-cli|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|synthetic-api-key|codex-cli|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip",
|
"Auth: setup-token|claude-cli|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|codex-cli|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip",
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--token-provider <id>",
|
"--token-provider <id>",
|
||||||
@@ -68,6 +68,7 @@ export function registerOnboardCommand(program: Command) {
|
|||||||
.option("--openrouter-api-key <key>", "OpenRouter API key")
|
.option("--openrouter-api-key <key>", "OpenRouter API key")
|
||||||
.option("--ai-gateway-api-key <key>", "Vercel AI Gateway API key")
|
.option("--ai-gateway-api-key <key>", "Vercel AI Gateway API key")
|
||||||
.option("--moonshot-api-key <key>", "Moonshot API key")
|
.option("--moonshot-api-key <key>", "Moonshot API key")
|
||||||
|
.option("--kimi-code-api-key <key>", "Kimi Code API key")
|
||||||
.option("--gemini-api-key <key>", "Gemini API key")
|
.option("--gemini-api-key <key>", "Gemini API key")
|
||||||
.option("--zai-api-key <key>", "Z.AI API key")
|
.option("--zai-api-key <key>", "Z.AI API key")
|
||||||
.option("--minimax-api-key <key>", "MiniMax API key")
|
.option("--minimax-api-key <key>", "MiniMax API key")
|
||||||
@@ -116,6 +117,7 @@ export function registerOnboardCommand(program: Command) {
|
|||||||
openrouterApiKey: opts.openrouterApiKey as string | undefined,
|
openrouterApiKey: opts.openrouterApiKey as string | undefined,
|
||||||
aiGatewayApiKey: opts.aiGatewayApiKey as string | undefined,
|
aiGatewayApiKey: opts.aiGatewayApiKey as string | undefined,
|
||||||
moonshotApiKey: opts.moonshotApiKey as string | undefined,
|
moonshotApiKey: opts.moonshotApiKey as string | undefined,
|
||||||
|
kimiCodeApiKey: opts.kimiCodeApiKey as string | undefined,
|
||||||
geminiApiKey: opts.geminiApiKey as string | undefined,
|
geminiApiKey: opts.geminiApiKey as string | undefined,
|
||||||
zaiApiKey: opts.zaiApiKey as string | undefined,
|
zaiApiKey: opts.zaiApiKey as string | undefined,
|
||||||
minimaxApiKey: opts.minimaxApiKey as string | undefined,
|
minimaxApiKey: opts.minimaxApiKey as string | undefined,
|
||||||
|
|||||||
@@ -247,9 +247,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
|||||||
}
|
}
|
||||||
if (opts.channel && !configSnapshot.valid) {
|
if (opts.channel && !configSnapshot.valid) {
|
||||||
const issues = configSnapshot.issues.map((issue) => `- ${issue.path}: ${issue.message}`);
|
const issues = configSnapshot.issues.map((issue) => `- ${issue.path}: ${issue.message}`);
|
||||||
defaultRuntime.error(
|
defaultRuntime.error(["Config is invalid; cannot set update channel.", ...issues].join("\n"));
|
||||||
["Config is invalid; cannot set update channel.", ...issues].join("\n"),
|
|
||||||
);
|
|
||||||
defaultRuntime.exit(1);
|
defaultRuntime.exit(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ describe("buildAuthChoiceOptions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(options.some((opt) => opt.value === "moonshot-api-key")).toBe(true);
|
expect(options.some((opt) => opt.value === "moonshot-api-key")).toBe(true);
|
||||||
|
expect(options.some((opt) => opt.value === "kimi-code-api-key")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes Vercel AI Gateway auth choice", () => {
|
it("includes Vercel AI Gateway auth choice", () => {
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
|||||||
{
|
{
|
||||||
value: "moonshot",
|
value: "moonshot",
|
||||||
label: "Moonshot AI",
|
label: "Moonshot AI",
|
||||||
hint: "Kimi K2 preview",
|
hint: "Kimi K2 + Kimi Code",
|
||||||
choices: ["moonshot-api-key"],
|
choices: ["moonshot-api-key", "kimi-code-api-key"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "zai",
|
value: "zai",
|
||||||
@@ -180,6 +180,7 @@ export function buildAuthChoiceOptions(params: {
|
|||||||
label: "Vercel AI Gateway API key",
|
label: "Vercel AI Gateway API key",
|
||||||
});
|
});
|
||||||
options.push({ value: "moonshot-api-key", label: "Moonshot AI API key" });
|
options.push({ value: "moonshot-api-key", label: "Moonshot AI API key" });
|
||||||
|
options.push({ value: "kimi-code-api-key", label: "Kimi Code API key" });
|
||||||
options.push({ value: "synthetic-api-key", label: "Synthetic API key" });
|
options.push({ value: "synthetic-api-key", label: "Synthetic API key" });
|
||||||
options.push({
|
options.push({
|
||||||
value: "github-copilot",
|
value: "github-copilot",
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
} from "./google-gemini-model-default.js";
|
} from "./google-gemini-model-default.js";
|
||||||
import {
|
import {
|
||||||
applyAuthProfileConfig,
|
applyAuthProfileConfig,
|
||||||
|
applyKimiCodeConfig,
|
||||||
|
applyKimiCodeProviderConfig,
|
||||||
applyMoonshotConfig,
|
applyMoonshotConfig,
|
||||||
applyMoonshotProviderConfig,
|
applyMoonshotProviderConfig,
|
||||||
applyOpencodeZenConfig,
|
applyOpencodeZenConfig,
|
||||||
@@ -24,11 +26,13 @@ import {
|
|||||||
applyVercelAiGatewayConfig,
|
applyVercelAiGatewayConfig,
|
||||||
applyVercelAiGatewayProviderConfig,
|
applyVercelAiGatewayProviderConfig,
|
||||||
applyZaiConfig,
|
applyZaiConfig,
|
||||||
|
KIMI_CODE_MODEL_REF,
|
||||||
MOONSHOT_DEFAULT_MODEL_REF,
|
MOONSHOT_DEFAULT_MODEL_REF,
|
||||||
OPENROUTER_DEFAULT_MODEL_REF,
|
OPENROUTER_DEFAULT_MODEL_REF,
|
||||||
SYNTHETIC_DEFAULT_MODEL_REF,
|
SYNTHETIC_DEFAULT_MODEL_REF,
|
||||||
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||||
setGeminiApiKey,
|
setGeminiApiKey,
|
||||||
|
setKimiCodeApiKey,
|
||||||
setMoonshotApiKey,
|
setMoonshotApiKey,
|
||||||
setOpencodeZenApiKey,
|
setOpencodeZenApiKey,
|
||||||
setOpenrouterApiKey,
|
setOpenrouterApiKey,
|
||||||
@@ -208,6 +212,55 @@ export async function applyAuthChoiceApiProviders(
|
|||||||
return { config: nextConfig, agentModelOverride };
|
return { config: nextConfig, agentModelOverride };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (params.authChoice === "kimi-code-api-key") {
|
||||||
|
await params.prompter.note(
|
||||||
|
[
|
||||||
|
"Kimi Code uses a dedicated endpoint and API key.",
|
||||||
|
"Get your API key at: https://www.kimi.com/code/en",
|
||||||
|
].join("\n"),
|
||||||
|
"Kimi Code",
|
||||||
|
);
|
||||||
|
let hasCredential = false;
|
||||||
|
const envKey = resolveEnvApiKey("kimi-code");
|
||||||
|
if (envKey) {
|
||||||
|
const useExisting = await params.prompter.confirm({
|
||||||
|
message: `Use existing KIMICODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||||
|
initialValue: true,
|
||||||
|
});
|
||||||
|
if (useExisting) {
|
||||||
|
await setKimiCodeApiKey(envKey.apiKey, params.agentDir);
|
||||||
|
hasCredential = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasCredential) {
|
||||||
|
const key = await params.prompter.text({
|
||||||
|
message: "Enter Kimi Code API key",
|
||||||
|
validate: validateApiKeyInput,
|
||||||
|
});
|
||||||
|
await setKimiCodeApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
|
||||||
|
}
|
||||||
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||||
|
profileId: "kimi-code:default",
|
||||||
|
provider: "kimi-code",
|
||||||
|
mode: "api_key",
|
||||||
|
});
|
||||||
|
{
|
||||||
|
const applied = await applyDefaultModelChoice({
|
||||||
|
config: nextConfig,
|
||||||
|
setDefaultModel: params.setDefaultModel,
|
||||||
|
defaultModel: KIMI_CODE_MODEL_REF,
|
||||||
|
applyDefaultConfig: applyKimiCodeConfig,
|
||||||
|
applyProviderConfig: applyKimiCodeProviderConfig,
|
||||||
|
noteDefault: KIMI_CODE_MODEL_REF,
|
||||||
|
noteAgentModel,
|
||||||
|
prompter: params.prompter,
|
||||||
|
});
|
||||||
|
nextConfig = applied.config;
|
||||||
|
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
||||||
|
}
|
||||||
|
return { config: nextConfig, agentModelOverride };
|
||||||
|
}
|
||||||
|
|
||||||
if (params.authChoice === "gemini-api-key") {
|
if (params.authChoice === "gemini-api-key") {
|
||||||
let hasCredential = false;
|
let hasCredential = false;
|
||||||
const envKey = resolveEnvApiKey("google");
|
const envKey = resolveEnvApiKey("google");
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
|
|||||||
"openrouter-api-key": "openrouter",
|
"openrouter-api-key": "openrouter",
|
||||||
"ai-gateway-api-key": "vercel-ai-gateway",
|
"ai-gateway-api-key": "vercel-ai-gateway",
|
||||||
"moonshot-api-key": "moonshot",
|
"moonshot-api-key": "moonshot",
|
||||||
|
"kimi-code-api-key": "kimi-code",
|
||||||
"gemini-api-key": "google",
|
"gemini-api-key": "google",
|
||||||
"zai-api-key": "zai",
|
"zai-api-key": "zai",
|
||||||
"synthetic-api-key": "synthetic",
|
"synthetic-api-key": "synthetic",
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import {
|
|||||||
ZAI_DEFAULT_MODEL_REF,
|
ZAI_DEFAULT_MODEL_REF,
|
||||||
} from "./onboard-auth.credentials.js";
|
} from "./onboard-auth.credentials.js";
|
||||||
import {
|
import {
|
||||||
|
buildKimiCodeModelDefinition,
|
||||||
buildMoonshotModelDefinition,
|
buildMoonshotModelDefinition,
|
||||||
|
KIMI_CODE_BASE_URL,
|
||||||
|
KIMI_CODE_MODEL_ID,
|
||||||
|
KIMI_CODE_MODEL_REF,
|
||||||
MOONSHOT_BASE_URL,
|
MOONSHOT_BASE_URL,
|
||||||
MOONSHOT_DEFAULT_MODEL_ID,
|
MOONSHOT_DEFAULT_MODEL_ID,
|
||||||
MOONSHOT_DEFAULT_MODEL_REF,
|
MOONSHOT_DEFAULT_MODEL_REF,
|
||||||
@@ -192,6 +196,71 @@ export function applyMoonshotConfig(cfg: ClawdbotConfig): ClawdbotConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function applyKimiCodeProviderConfig(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||||
|
const models = { ...cfg.agents?.defaults?.models };
|
||||||
|
models[KIMI_CODE_MODEL_REF] = {
|
||||||
|
...models[KIMI_CODE_MODEL_REF],
|
||||||
|
alias: models[KIMI_CODE_MODEL_REF]?.alias ?? "Kimi Code",
|
||||||
|
};
|
||||||
|
|
||||||
|
const providers = { ...cfg.models?.providers };
|
||||||
|
const existingProvider = providers["kimi-code"];
|
||||||
|
const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : [];
|
||||||
|
const defaultModel = buildKimiCodeModelDefinition();
|
||||||
|
const hasDefaultModel = existingModels.some((model) => model.id === KIMI_CODE_MODEL_ID);
|
||||||
|
const mergedModels = hasDefaultModel ? existingModels : [...existingModels, defaultModel];
|
||||||
|
const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
> as { apiKey?: string };
|
||||||
|
const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined;
|
||||||
|
const normalizedApiKey = resolvedApiKey?.trim();
|
||||||
|
providers["kimi-code"] = {
|
||||||
|
...existingProviderRest,
|
||||||
|
baseUrl: KIMI_CODE_BASE_URL,
|
||||||
|
api: "openai-completions",
|
||||||
|
...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}),
|
||||||
|
models: mergedModels.length > 0 ? mergedModels : [defaultModel],
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
agents: {
|
||||||
|
...cfg.agents,
|
||||||
|
defaults: {
|
||||||
|
...cfg.agents?.defaults,
|
||||||
|
models,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
models: {
|
||||||
|
mode: cfg.models?.mode ?? "merge",
|
||||||
|
providers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyKimiCodeConfig(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||||
|
const next = applyKimiCodeProviderConfig(cfg);
|
||||||
|
const existingModel = next.agents?.defaults?.model;
|
||||||
|
return {
|
||||||
|
...next,
|
||||||
|
agents: {
|
||||||
|
...next.agents,
|
||||||
|
defaults: {
|
||||||
|
...next.agents?.defaults,
|
||||||
|
model: {
|
||||||
|
...(existingModel && "fallbacks" in (existingModel as Record<string, unknown>)
|
||||||
|
? {
|
||||||
|
fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks,
|
||||||
|
}
|
||||||
|
: undefined),
|
||||||
|
primary: KIMI_CODE_MODEL_REF,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function applySyntheticProviderConfig(cfg: ClawdbotConfig): ClawdbotConfig {
|
export function applySyntheticProviderConfig(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||||
const models = { ...cfg.agents?.defaults?.models };
|
const models = { ...cfg.agents?.defaults?.models };
|
||||||
models[SYNTHETIC_DEFAULT_MODEL_REF] = {
|
models[SYNTHETIC_DEFAULT_MODEL_REF] = {
|
||||||
|
|||||||
@@ -73,6 +73,19 @@ export async function setMoonshotApiKey(key: string, agentDir?: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function setKimiCodeApiKey(key: string, agentDir?: string) {
|
||||||
|
// Write to resolved agent dir so gateway finds credentials on startup.
|
||||||
|
upsertAuthProfile({
|
||||||
|
profileId: "kimi-code:default",
|
||||||
|
credential: {
|
||||||
|
type: "api_key",
|
||||||
|
provider: "kimi-code",
|
||||||
|
key,
|
||||||
|
},
|
||||||
|
agentDir: resolveAuthAgentDir(agentDir),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function setSyntheticApiKey(key: string, agentDir?: string) {
|
export async function setSyntheticApiKey(key: string, agentDir?: string) {
|
||||||
// Write to resolved agent dir so gateway finds credentials on startup.
|
// Write to resolved agent dir so gateway finds credentials on startup.
|
||||||
upsertAuthProfile({
|
upsertAuthProfile({
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ export const MOONSHOT_DEFAULT_MODEL_ID = "kimi-k2-0905-preview";
|
|||||||
export const MOONSHOT_DEFAULT_MODEL_REF = `moonshot/${MOONSHOT_DEFAULT_MODEL_ID}`;
|
export const MOONSHOT_DEFAULT_MODEL_REF = `moonshot/${MOONSHOT_DEFAULT_MODEL_ID}`;
|
||||||
export const MOONSHOT_DEFAULT_CONTEXT_WINDOW = 256000;
|
export const MOONSHOT_DEFAULT_CONTEXT_WINDOW = 256000;
|
||||||
export const MOONSHOT_DEFAULT_MAX_TOKENS = 8192;
|
export const MOONSHOT_DEFAULT_MAX_TOKENS = 8192;
|
||||||
|
export const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
|
||||||
|
export const KIMI_CODE_MODEL_ID = "kimi-for-coding";
|
||||||
|
export const KIMI_CODE_MODEL_REF = `kimi-code/${KIMI_CODE_MODEL_ID}`;
|
||||||
|
export const KIMI_CODE_CONTEXT_WINDOW = 262144;
|
||||||
|
export const KIMI_CODE_MAX_TOKENS = 32768;
|
||||||
|
export const KIMI_CODE_HEADERS = { "User-Agent": "KimiCLI/0.77" } as const;
|
||||||
|
export const KIMI_CODE_COMPAT = { supportsDeveloperRole: false } as const;
|
||||||
|
|
||||||
// Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs.
|
// Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs.
|
||||||
export const MINIMAX_API_COST = {
|
export const MINIMAX_API_COST = {
|
||||||
@@ -38,6 +45,12 @@ export const MOONSHOT_DEFAULT_COST = {
|
|||||||
cacheRead: 0,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
};
|
};
|
||||||
|
export const KIMI_CODE_DEFAULT_COST = {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
};
|
||||||
|
|
||||||
const MINIMAX_MODEL_CATALOG = {
|
const MINIMAX_MODEL_CATALOG = {
|
||||||
"MiniMax-M2.1": { name: "MiniMax M2.1", reasoning: false },
|
"MiniMax-M2.1": { name: "MiniMax M2.1", reasoning: false },
|
||||||
@@ -89,3 +102,17 @@ export function buildMoonshotModelDefinition(): ModelDefinitionConfig {
|
|||||||
maxTokens: MOONSHOT_DEFAULT_MAX_TOKENS,
|
maxTokens: MOONSHOT_DEFAULT_MAX_TOKENS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildKimiCodeModelDefinition(): ModelDefinitionConfig {
|
||||||
|
return {
|
||||||
|
id: KIMI_CODE_MODEL_ID,
|
||||||
|
name: "Kimi For Coding",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
cost: KIMI_CODE_DEFAULT_COST,
|
||||||
|
contextWindow: KIMI_CODE_CONTEXT_WINDOW,
|
||||||
|
maxTokens: KIMI_CODE_MAX_TOKENS,
|
||||||
|
headers: KIMI_CODE_HEADERS,
|
||||||
|
compat: KIMI_CODE_COMPAT,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ export {
|
|||||||
} from "../agents/synthetic-models.js";
|
} from "../agents/synthetic-models.js";
|
||||||
export {
|
export {
|
||||||
applyAuthProfileConfig,
|
applyAuthProfileConfig,
|
||||||
|
applyKimiCodeConfig,
|
||||||
|
applyKimiCodeProviderConfig,
|
||||||
applyMoonshotConfig,
|
applyMoonshotConfig,
|
||||||
applyMoonshotProviderConfig,
|
applyMoonshotProviderConfig,
|
||||||
applyOpenrouterConfig,
|
applyOpenrouterConfig,
|
||||||
@@ -31,6 +33,7 @@ export {
|
|||||||
OPENROUTER_DEFAULT_MODEL_REF,
|
OPENROUTER_DEFAULT_MODEL_REF,
|
||||||
setAnthropicApiKey,
|
setAnthropicApiKey,
|
||||||
setGeminiApiKey,
|
setGeminiApiKey,
|
||||||
|
setKimiCodeApiKey,
|
||||||
setMinimaxApiKey,
|
setMinimaxApiKey,
|
||||||
setMoonshotApiKey,
|
setMoonshotApiKey,
|
||||||
setOpencodeZenApiKey,
|
setOpencodeZenApiKey,
|
||||||
@@ -43,10 +46,14 @@ export {
|
|||||||
ZAI_DEFAULT_MODEL_REF,
|
ZAI_DEFAULT_MODEL_REF,
|
||||||
} from "./onboard-auth.credentials.js";
|
} from "./onboard-auth.credentials.js";
|
||||||
export {
|
export {
|
||||||
|
buildKimiCodeModelDefinition,
|
||||||
buildMinimaxApiModelDefinition,
|
buildMinimaxApiModelDefinition,
|
||||||
buildMinimaxModelDefinition,
|
buildMinimaxModelDefinition,
|
||||||
buildMoonshotModelDefinition,
|
buildMoonshotModelDefinition,
|
||||||
DEFAULT_MINIMAX_BASE_URL,
|
DEFAULT_MINIMAX_BASE_URL,
|
||||||
|
KIMI_CODE_BASE_URL,
|
||||||
|
KIMI_CODE_MODEL_ID,
|
||||||
|
KIMI_CODE_MODEL_REF,
|
||||||
MINIMAX_API_BASE_URL,
|
MINIMAX_API_BASE_URL,
|
||||||
MINIMAX_HOSTED_MODEL_ID,
|
MINIMAX_HOSTED_MODEL_ID,
|
||||||
MINIMAX_HOSTED_MODEL_REF,
|
MINIMAX_HOSTED_MODEL_REF,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { buildTokenProfileId, validateAnthropicSetupToken } from "../../auth-tok
|
|||||||
import { applyGoogleGeminiModelDefault } from "../../google-gemini-model-default.js";
|
import { applyGoogleGeminiModelDefault } from "../../google-gemini-model-default.js";
|
||||||
import {
|
import {
|
||||||
applyAuthProfileConfig,
|
applyAuthProfileConfig,
|
||||||
|
applyKimiCodeConfig,
|
||||||
applyMinimaxApiConfig,
|
applyMinimaxApiConfig,
|
||||||
applyMinimaxConfig,
|
applyMinimaxConfig,
|
||||||
applyMoonshotConfig,
|
applyMoonshotConfig,
|
||||||
@@ -23,6 +24,7 @@ import {
|
|||||||
applyZaiConfig,
|
applyZaiConfig,
|
||||||
setAnthropicApiKey,
|
setAnthropicApiKey,
|
||||||
setGeminiApiKey,
|
setGeminiApiKey,
|
||||||
|
setKimiCodeApiKey,
|
||||||
setMinimaxApiKey,
|
setMinimaxApiKey,
|
||||||
setMoonshotApiKey,
|
setMoonshotApiKey,
|
||||||
setOpencodeZenApiKey,
|
setOpencodeZenApiKey,
|
||||||
@@ -231,6 +233,25 @@ export async function applyNonInteractiveAuthChoice(params: {
|
|||||||
return applyMoonshotConfig(nextConfig);
|
return applyMoonshotConfig(nextConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authChoice === "kimi-code-api-key") {
|
||||||
|
const resolved = await resolveNonInteractiveApiKey({
|
||||||
|
provider: "kimi-code",
|
||||||
|
cfg: baseConfig,
|
||||||
|
flagValue: opts.kimiCodeApiKey,
|
||||||
|
flagName: "--kimi-code-api-key",
|
||||||
|
envVar: "KIMICODE_API_KEY",
|
||||||
|
runtime,
|
||||||
|
});
|
||||||
|
if (!resolved) return null;
|
||||||
|
if (resolved.source !== "profile") await setKimiCodeApiKey(resolved.key);
|
||||||
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||||
|
profileId: "kimi-code:default",
|
||||||
|
provider: "kimi-code",
|
||||||
|
mode: "api_key",
|
||||||
|
});
|
||||||
|
return applyKimiCodeConfig(nextConfig);
|
||||||
|
}
|
||||||
|
|
||||||
if (authChoice === "synthetic-api-key") {
|
if (authChoice === "synthetic-api-key") {
|
||||||
const resolved = await resolveNonInteractiveApiKey({
|
const resolved = await resolveNonInteractiveApiKey({
|
||||||
provider: "synthetic",
|
provider: "synthetic",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type AuthChoice =
|
|||||||
| "openrouter-api-key"
|
| "openrouter-api-key"
|
||||||
| "ai-gateway-api-key"
|
| "ai-gateway-api-key"
|
||||||
| "moonshot-api-key"
|
| "moonshot-api-key"
|
||||||
|
| "kimi-code-api-key"
|
||||||
| "synthetic-api-key"
|
| "synthetic-api-key"
|
||||||
| "codex-cli"
|
| "codex-cli"
|
||||||
| "apiKey"
|
| "apiKey"
|
||||||
@@ -57,6 +58,7 @@ export type OnboardOptions = {
|
|||||||
openrouterApiKey?: string;
|
openrouterApiKey?: string;
|
||||||
aiGatewayApiKey?: string;
|
aiGatewayApiKey?: string;
|
||||||
moonshotApiKey?: string;
|
moonshotApiKey?: string;
|
||||||
|
kimiCodeApiKey?: string;
|
||||||
geminiApiKey?: string;
|
geminiApiKey?: string;
|
||||||
zaiApiKey?: string;
|
zaiApiKey?: string;
|
||||||
minimaxApiKey?: string;
|
minimaxApiKey?: string;
|
||||||
|
|||||||
@@ -163,6 +163,35 @@ function decodeDiscordCommandArgValue(value: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDiscordUnknownInteraction(error: unknown): boolean {
|
||||||
|
if (!error || typeof error !== "object") return false;
|
||||||
|
const err = error as {
|
||||||
|
discordCode?: number;
|
||||||
|
status?: number;
|
||||||
|
message?: string;
|
||||||
|
rawBody?: { code?: number; message?: string };
|
||||||
|
};
|
||||||
|
if (err.discordCode === 10062 || err.rawBody?.code === 10062) return true;
|
||||||
|
if (err.status === 404 && /Unknown interaction/i.test(err.message ?? "")) return true;
|
||||||
|
if (/Unknown interaction/i.test(err.rawBody?.message ?? "")) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function safeDiscordInteractionCall<T>(
|
||||||
|
label: string,
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
): Promise<T | null> {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (error) {
|
||||||
|
if (isDiscordUnknownInteraction(error)) {
|
||||||
|
console.warn(`discord: ${label} skipped (interaction expired)`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildDiscordCommandArgCustomId(params: {
|
function buildDiscordCommandArgCustomId(params: {
|
||||||
command: string;
|
command: string;
|
||||||
arg: string;
|
arg: string;
|
||||||
@@ -196,6 +225,73 @@ function parseDiscordCommandArgData(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DiscordCommandArgContext = {
|
||||||
|
cfg: ReturnType<typeof loadConfig>;
|
||||||
|
discordConfig: DiscordConfig;
|
||||||
|
accountId: string;
|
||||||
|
sessionPrefix: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handleDiscordCommandArgInteraction(
|
||||||
|
interaction: ButtonInteraction,
|
||||||
|
data: ComponentData,
|
||||||
|
ctx: DiscordCommandArgContext,
|
||||||
|
) {
|
||||||
|
const parsed = parseDiscordCommandArgData(data);
|
||||||
|
if (!parsed) {
|
||||||
|
await safeDiscordInteractionCall("command arg update", () =>
|
||||||
|
interaction.update({
|
||||||
|
content: "Sorry, that selection is no longer available.",
|
||||||
|
components: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (interaction.user?.id && interaction.user.id !== parsed.userId) {
|
||||||
|
await safeDiscordInteractionCall("command arg ack", () => interaction.acknowledge());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const commandDefinition =
|
||||||
|
findCommandByNativeName(parsed.command) ??
|
||||||
|
listChatCommands().find((entry) => entry.key === parsed.command);
|
||||||
|
if (!commandDefinition) {
|
||||||
|
await safeDiscordInteractionCall("command arg update", () =>
|
||||||
|
interaction.update({
|
||||||
|
content: "Sorry, that command is no longer available.",
|
||||||
|
components: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const updated = await safeDiscordInteractionCall("command arg update", () =>
|
||||||
|
interaction.update({
|
||||||
|
content: `✅ Selected ${parsed.value}.`,
|
||||||
|
components: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (!updated) return;
|
||||||
|
const commandArgs = createCommandArgsWithValue({
|
||||||
|
argName: parsed.arg,
|
||||||
|
value: parsed.value,
|
||||||
|
});
|
||||||
|
const commandArgsWithRaw: CommandArgs = {
|
||||||
|
...commandArgs,
|
||||||
|
raw: serializeCommandArgs(commandDefinition, commandArgs),
|
||||||
|
};
|
||||||
|
const prompt = buildCommandTextFromArgs(commandDefinition, commandArgsWithRaw);
|
||||||
|
await dispatchDiscordCommandInteraction({
|
||||||
|
interaction,
|
||||||
|
prompt,
|
||||||
|
command: commandDefinition,
|
||||||
|
commandArgs: commandArgsWithRaw,
|
||||||
|
cfg: ctx.cfg,
|
||||||
|
discordConfig: ctx.discordConfig,
|
||||||
|
accountId: ctx.accountId,
|
||||||
|
sessionPrefix: ctx.sessionPrefix,
|
||||||
|
preferFollowUp: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class DiscordCommandArgButton extends Button {
|
class DiscordCommandArgButton extends Button {
|
||||||
label: string;
|
label: string;
|
||||||
customId: string;
|
customId: string;
|
||||||
@@ -223,55 +319,34 @@ class DiscordCommandArgButton extends Button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async run(interaction: ButtonInteraction, data: ComponentData) {
|
async run(interaction: ButtonInteraction, data: ComponentData) {
|
||||||
const parsed = parseDiscordCommandArgData(data);
|
await handleDiscordCommandArgInteraction(interaction, data, {
|
||||||
if (!parsed) {
|
|
||||||
await interaction.update({
|
|
||||||
content: "Sorry, that selection is no longer available.",
|
|
||||||
components: [],
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (interaction.user?.id && interaction.user.id !== parsed.userId) {
|
|
||||||
await interaction.acknowledge();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const commandDefinition =
|
|
||||||
findCommandByNativeName(parsed.command) ??
|
|
||||||
listChatCommands().find((entry) => entry.key === parsed.command);
|
|
||||||
if (!commandDefinition) {
|
|
||||||
await interaction.update({
|
|
||||||
content: "Sorry, that command is no longer available.",
|
|
||||||
components: [],
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await interaction.update({
|
|
||||||
content: `✅ Selected ${parsed.value}.`,
|
|
||||||
components: [],
|
|
||||||
});
|
|
||||||
const commandArgs = createCommandArgsWithValue({
|
|
||||||
argName: parsed.arg,
|
|
||||||
value: parsed.value,
|
|
||||||
});
|
|
||||||
const commandArgsWithRaw: CommandArgs = {
|
|
||||||
...commandArgs,
|
|
||||||
raw: serializeCommandArgs(commandDefinition, commandArgs),
|
|
||||||
};
|
|
||||||
const prompt = buildCommandTextFromArgs(commandDefinition, commandArgsWithRaw);
|
|
||||||
await dispatchDiscordCommandInteraction({
|
|
||||||
interaction,
|
|
||||||
prompt,
|
|
||||||
command: commandDefinition,
|
|
||||||
commandArgs: commandArgsWithRaw,
|
|
||||||
cfg: this.cfg,
|
cfg: this.cfg,
|
||||||
discordConfig: this.discordConfig,
|
discordConfig: this.discordConfig,
|
||||||
accountId: this.accountId,
|
accountId: this.accountId,
|
||||||
sessionPrefix: this.sessionPrefix,
|
sessionPrefix: this.sessionPrefix,
|
||||||
preferFollowUp: true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DiscordCommandArgFallbackButton extends Button {
|
||||||
|
label = "cmdarg";
|
||||||
|
customId = "cmdarg:seed=1";
|
||||||
|
private ctx: DiscordCommandArgContext;
|
||||||
|
|
||||||
|
constructor(ctx: DiscordCommandArgContext) {
|
||||||
|
super();
|
||||||
|
this.ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(interaction: ButtonInteraction, data: ComponentData) {
|
||||||
|
await handleDiscordCommandArgInteraction(interaction, data, this.ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDiscordCommandArgFallbackButton(params: DiscordCommandArgContext): Button {
|
||||||
|
return new DiscordCommandArgFallbackButton(params);
|
||||||
|
}
|
||||||
|
|
||||||
function buildDiscordCommandArgMenu(params: {
|
function buildDiscordCommandArgMenu(params: {
|
||||||
command: ChatCommandDefinition;
|
command: ChatCommandDefinition;
|
||||||
menu: { arg: CommandArgDefinition; choices: string[]; title?: string };
|
menu: { arg: CommandArgDefinition; choices: string[]; title?: string };
|
||||||
@@ -408,11 +483,13 @@ async function dispatchDiscordCommandInteraction(params: {
|
|||||||
content,
|
content,
|
||||||
...(options?.ephemeral !== undefined ? { ephemeral: options.ephemeral } : {}),
|
...(options?.ephemeral !== undefined ? { ephemeral: options.ephemeral } : {}),
|
||||||
};
|
};
|
||||||
if (preferFollowUp) {
|
await safeDiscordInteractionCall("interaction reply", async () => {
|
||||||
await interaction.followUp(payload);
|
if (preferFollowUp) {
|
||||||
return;
|
await interaction.followUp(payload);
|
||||||
}
|
return;
|
||||||
await interaction.reply(payload);
|
}
|
||||||
|
await interaction.reply(payload);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
||||||
@@ -567,18 +644,22 @@ async function dispatchDiscordCommandInteraction(params: {
|
|||||||
sessionPrefix,
|
sessionPrefix,
|
||||||
});
|
});
|
||||||
if (preferFollowUp) {
|
if (preferFollowUp) {
|
||||||
await interaction.followUp({
|
await safeDiscordInteractionCall("interaction follow-up", () =>
|
||||||
|
interaction.followUp({
|
||||||
|
content: menuPayload.content,
|
||||||
|
components: menuPayload.components,
|
||||||
|
ephemeral: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await safeDiscordInteractionCall("interaction reply", () =>
|
||||||
|
interaction.reply({
|
||||||
content: menuPayload.content,
|
content: menuPayload.content,
|
||||||
components: menuPayload.components,
|
components: menuPayload.components,
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
});
|
}),
|
||||||
return;
|
);
|
||||||
}
|
|
||||||
await interaction.reply({
|
|
||||||
content: menuPayload.content,
|
|
||||||
components: menuPayload.components,
|
|
||||||
ephemeral: true,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,15 +727,23 @@ async function dispatchDiscordCommandInteraction(params: {
|
|||||||
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix,
|
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix,
|
||||||
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
|
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
|
||||||
deliver: async (payload) => {
|
deliver: async (payload) => {
|
||||||
await deliverDiscordInteractionReply({
|
try {
|
||||||
interaction,
|
await deliverDiscordInteractionReply({
|
||||||
payload,
|
interaction,
|
||||||
textLimit: resolveTextChunkLimit(cfg, "discord", accountId, {
|
payload,
|
||||||
fallbackLimit: 2000,
|
textLimit: resolveTextChunkLimit(cfg, "discord", accountId, {
|
||||||
}),
|
fallbackLimit: 2000,
|
||||||
maxLinesPerMessage: discordConfig?.maxLinesPerMessage,
|
}),
|
||||||
preferFollowUp: preferFollowUp || didReply,
|
maxLinesPerMessage: discordConfig?.maxLinesPerMessage,
|
||||||
});
|
preferFollowUp: preferFollowUp || didReply,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (isDiscordUnknownInteraction(error)) {
|
||||||
|
console.warn("discord: interaction reply skipped (interaction expired)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
didReply = true;
|
didReply = true;
|
||||||
},
|
},
|
||||||
onError: (err, info) => {
|
onError: (err, info) => {
|
||||||
@@ -697,13 +786,15 @@ async function deliverDiscordInteractionReply(params: {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
: { content };
|
: { content };
|
||||||
if (!preferFollowUp && !hasReplied) {
|
await safeDiscordInteractionCall("interaction send", async () => {
|
||||||
await interaction.reply(payload);
|
if (!preferFollowUp && !hasReplied) {
|
||||||
|
await interaction.reply(payload);
|
||||||
|
hasReplied = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await interaction.followUp(payload);
|
||||||
hasReplied = true;
|
hasReplied = true;
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
await interaction.followUp(payload);
|
|
||||||
hasReplied = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mediaList.length > 0) {
|
if (mediaList.length > 0) {
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ import {
|
|||||||
registerDiscordListener,
|
registerDiscordListener,
|
||||||
} from "./listeners.js";
|
} from "./listeners.js";
|
||||||
import { createDiscordMessageHandler } from "./message-handler.js";
|
import { createDiscordMessageHandler } from "./message-handler.js";
|
||||||
import { createDiscordNativeCommand } from "./native-command.js";
|
import {
|
||||||
|
createDiscordCommandArgFallbackButton,
|
||||||
|
createDiscordNativeCommand,
|
||||||
|
} from "./native-command.js";
|
||||||
|
|
||||||
export type MonitorDiscordOpts = {
|
export type MonitorDiscordOpts = {
|
||||||
token?: string;
|
token?: string;
|
||||||
@@ -149,6 +152,14 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
{
|
{
|
||||||
commands,
|
commands,
|
||||||
listeners: [],
|
listeners: [],
|
||||||
|
components: [
|
||||||
|
createDiscordCommandArgFallbackButton({
|
||||||
|
cfg,
|
||||||
|
discordConfig: discordCfg,
|
||||||
|
accountId: account.accountId,
|
||||||
|
sessionPrefix,
|
||||||
|
}),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
new GatewayPlugin({
|
new GatewayPlugin({
|
||||||
|
|||||||
Reference in new issue
Block a user