Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc4e1a434f | ||
|
|
e9d1f3b030 |
No files matched your search
+1
-1
@@ -6,7 +6,7 @@ Docs: https://docs.clawd.bot
|
|||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
- macOS: strip prerelease/build suffixes when parsing gateway semver patches. (#1110) — thanks @zerone0x.
|
- 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.
|
- Docs: remove duplicate logging nav entry. (#1106) — thanks @gumadeiras.
|
||||||
|
|
||||||
## 2026.1.16-2
|
## 2026.1.16-2
|
||||||
|
|
||||||
|
|||||||
@@ -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.expectedGatewayVersionString() ?? "latest"
|
let expected = GatewayEnvironment.expectedGatewayVersion()?.description ?? "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(self.schema, path: self.path)
|
self.renderNode(schema, path: path)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func renderNode(_ schema: ConfigSchemaNode, path: ConfigPath) -> AnyView {
|
private func renderNode(_ schema: ConfigSchemaNode, path: ConfigPath) -> AnyView {
|
||||||
let storedValue = self.store.configValue(at: path)
|
let storedValue = 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(\.literalValue)
|
let literals = nonNull.compactMap { $0.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,20 +31,15 @@ struct ConfigSchemaForm: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
Picker(
|
Picker("", selection: self.enumBinding(path, options: literals, defaultValue: schema.explicitDefault)) {
|
||||||
"",
|
|
||||||
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)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +71,8 @@ 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":
|
||||||
@@ -84,7 +80,8 @@ 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":
|
||||||
@@ -96,7 +93,8 @@ struct ConfigSchemaForm: View {
|
|||||||
Text("Unsupported field type.")
|
Text("Unsupported field type.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +155,9 @@ 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)
|
||||||
self.store.updateConfigValue(path: path, value: next)
|
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("")
|
||||||
}
|
}
|
||||||
self.store.updateConfigValue(path: path, value: next)
|
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)
|
||||||
self.store.updateConfigValue(path: path, value: next)
|
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
|
||||||
self.store.updateConfigValue(path: path, value: next)
|
store.updateConfigValue(path: path, value: next)
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.controlSize(.small)
|
.controlSize(.small)
|
||||||
@@ -270,8 +270,9 @@ struct ConfigSchemaForm: View {
|
|||||||
},
|
},
|
||||||
set: { newValue in
|
set: { newValue in
|
||||||
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
self.store.updateConfigValue(path: path, value: trimmed.isEmpty ? nil : trimmed)
|
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> {
|
||||||
@@ -281,15 +282,16 @@ struct ConfigSchemaForm: View {
|
|||||||
return defaultValue ?? false
|
return defaultValue ?? false
|
||||||
},
|
},
|
||||||
set: { newValue in
|
set: { newValue in
|
||||||
self.store.updateConfigValue(path: path, value: newValue)
|
store.updateConfigValue(path: path, value: newValue)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func numberBinding(
|
private func numberBinding(
|
||||||
_ path: ConfigPath,
|
_ path: ConfigPath,
|
||||||
isInteger: Bool,
|
isInteger: Bool,
|
||||||
defaultValue: Double?) -> Binding<String>
|
defaultValue: Double?
|
||||||
{
|
) -> 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) }
|
||||||
@@ -299,21 +301,22 @@ 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 {
|
||||||
self.store.updateConfigValue(path: path, value: nil)
|
store.updateConfigValue(path: path, value: nil)
|
||||||
} else if let value = Double(trimmed) {
|
} else if let value = Double(trimmed) {
|
||||||
self.store.updateConfigValue(path: path, value: isInteger ? Int(value) : value)
|
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?) -> Binding<Int>
|
defaultValue: Any?
|
||||||
{
|
) -> Binding<Int> {
|
||||||
Binding(
|
Binding(
|
||||||
get: {
|
get: {
|
||||||
let value = self.store.configValue(at: path) ?? defaultValue
|
let value = 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)
|
||||||
@@ -321,11 +324,12 @@ struct ConfigSchemaForm: View {
|
|||||||
},
|
},
|
||||||
set: { index in
|
set: { index in
|
||||||
guard index >= 0, index < options.count else {
|
guard index >= 0, index < options.count else {
|
||||||
self.store.updateConfigValue(path: path, value: nil)
|
store.updateConfigValue(path: path, value: nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.store.updateConfigValue(path: path, value: options[index])
|
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> {
|
||||||
@@ -335,13 +339,14 @@ 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 = self.store.configValue(at: path) as? [String: Any] ?? [:]
|
let current = 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)
|
||||||
self.store.updateConfigValue(path: path, value: next)
|
store.updateConfigValue(path: path, value: next)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,10 +355,10 @@ struct ChannelConfigForm: View {
|
|||||||
let channelId: String
|
let channelId: String
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if self.store.configSchemaLoading {
|
if 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: self.store, schema: schema, path: [.key("channels"), .key(self.channelId)])
|
ConfigSchemaForm(store: store, schema: schema, path: [.key("channels"), .key(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": "WhatsApp Web"
|
case "whatsapp": return "WhatsApp Web"
|
||||||
case "telegram": "Telegram Bot"
|
case "telegram": return "Telegram Bot"
|
||||||
case "discord": "Discord Bot"
|
case "discord": return "Discord Bot"
|
||||||
case "slack": "Slack Bot"
|
case "slack": return "Slack Bot"
|
||||||
case "signal": "Signal REST"
|
case "signal": return "Signal REST"
|
||||||
case "imessage": "iMessage"
|
case "imessage": return "iMessage"
|
||||||
default: self.resolveChannelTitle(id)
|
default: return self.resolveChannelTitle(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func resolveChannelSystemImage(_ id: String) -> String {
|
private func resolveChannelSystemImage(_ id: String) -> String {
|
||||||
switch id {
|
switch id {
|
||||||
case "whatsapp": "message"
|
case "whatsapp": return "message"
|
||||||
case "telegram": "paperplane"
|
case "telegram": return "paperplane"
|
||||||
case "discord": "bubble.left.and.bubble.right"
|
case "discord": return "bubble.left.and.bubble.right"
|
||||||
case "slack": "number"
|
case "slack": return "number"
|
||||||
case "signal": "antenna.radiowaves.left.and.right"
|
case "signal": return "antenna.radiowaves.left.and.right"
|
||||||
case "imessage": "message.fill"
|
case "imessage": return "message.fill"
|
||||||
default: "message"
|
default: return "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 let .key(key):
|
case .key(let 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 let .index(index):
|
case .index(let 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 let .key(key):
|
case .key(let 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 let .index(index):
|
case .index(let 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 let .key(key):
|
case .key(let 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 let .key(key): return key
|
case .key(let 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)
|
||||||
|
|||||||
@@ -80,13 +80,8 @@ 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
|
||||||
let trimmed = bundleVersion?.trimmingCharacters(in: .whitespacesAndNewlines)
|
return Semver.parse(bundleVersion)
|
||||||
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.
|
||||||
@@ -105,7 +100,6 @@ 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)
|
||||||
@@ -116,8 +110,8 @@ enum GatewayEnvironment {
|
|||||||
kind: .missingNode,
|
kind: .missingNode,
|
||||||
nodeVersion: nil,
|
nodeVersion: nil,
|
||||||
gatewayVersion: nil,
|
gatewayVersion: nil,
|
||||||
requiredGateway: expectedString,
|
requiredGateway: expected?.description,
|
||||||
message: RuntimeLocator.describeFailure(err))
|
message: RuntimeLocator.describeFailure(err))
|
||||||
case let .success(runtime):
|
case let .success(runtime):
|
||||||
let gatewayBin = CommandResolver.clawdbotExecutable()
|
let gatewayBin = CommandResolver.clawdbotExecutable()
|
||||||
|
|
||||||
@@ -126,7 +120,7 @@ enum GatewayEnvironment {
|
|||||||
kind: .missingGateway,
|
kind: .missingGateway,
|
||||||
nodeVersion: runtime.version.description,
|
nodeVersion: runtime.version.description,
|
||||||
gatewayVersion: nil,
|
gatewayVersion: nil,
|
||||||
requiredGateway: expectedString,
|
requiredGateway: expected?.description,
|
||||||
message: "clawdbot CLI not found in PATH; install the CLI.")
|
message: "clawdbot CLI not found in PATH; install the CLI.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,14 +128,13 @@ 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: expectedText),
|
kind: .incompatible(found: installed.description, required: expected.description),
|
||||||
nodeVersion: runtime.version.description,
|
nodeVersion: runtime.version.description,
|
||||||
gatewayVersion: installed.description,
|
gatewayVersion: installed.description,
|
||||||
requiredGateway: expectedText,
|
requiredGateway: expected.description,
|
||||||
message: """
|
message: """
|
||||||
Gateway version \(installed.description) is incompatible with app \(expectedText);
|
Gateway version \(installed.description) is incompatible with app \(expected.description);
|
||||||
install or update the global package.
|
install or update the global package.
|
||||||
""")
|
""")
|
||||||
}
|
}
|
||||||
@@ -159,7 +152,7 @@ enum GatewayEnvironment {
|
|||||||
kind: .ok,
|
kind: .ok,
|
||||||
nodeVersion: runtime.version.description,
|
nodeVersion: runtime.version.description,
|
||||||
gatewayVersion: gatewayVersionText,
|
gatewayVersion: gatewayVersionText,
|
||||||
requiredGateway: expectedString,
|
requiredGateway: expected?.description,
|
||||||
message: "Node \(runtime.version.description); gateway \(gatewayVersionText) \(gatewayLabelText)")
|
message: "Node \(runtime.version.description); gateway \(gatewayVersionText) \(gatewayLabelText)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,18 +220,8 @@ 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 trimmed = versionString?.trimmingCharacters(in: .whitespacesAndNewlines)
|
let target = version?.description ?? "latest"
|
||||||
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.expectedGatewayVersionString() ?? "latest"
|
let version = GatewayEnvironment.expectedGatewayVersion()?.description ?? "latest"
|
||||||
return "npm install -g clawdbot@\(version)"
|
return "npm install -g clawdbot@\(version)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,8 @@ import Testing
|
|||||||
#expect(Semver.parse("3.4.5-beta.1") == Semver(major: 3, minor: 4, patch: 5)) // prerelease suffix stripped
|
#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("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.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() {
|
||||||
@@ -44,7 +38,6 @@ 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,19 +360,6 @@ 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
|
||||||
|
|||||||
+1
-2
@@ -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|kimi-code-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|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,7 +302,6 @@ 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,34 +155,6 @@ 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:
|
||||||
|
|||||||
@@ -2234,49 +2234,6 @@ 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 + Kimi Code)](/providers/moonshot)
|
- [Moonshot AI (Kimi)](/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 + Kimi Code)](/providers/moonshot)
|
- [Moonshot AI (Kimi)](/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,16 +1,13 @@
|
|||||||
---
|
---
|
||||||
summary: "Configure Moonshot K2 vs Kimi Code (separate providers + keys)"
|
summary: "Use Moonshot AI (Kimi K2) with Clawdbot"
|
||||||
read_when:
|
read_when:
|
||||||
- You want Moonshot K2 (Moonshot Open Platform) vs Kimi Code setup
|
- You want to use Moonshot/Kimi models in Clawdbot
|
||||||
- You need to understand separate endpoints, keys, and model refs
|
- You need the Moonshot auth + config example
|
||||||
- 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`, or use
|
provider and set the default model to `moonshot/kimi-k2-0905-preview`.
|
||||||
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 */}
|
||||||
@@ -24,15 +21,7 @@ Current Kimi K2 model IDs:
|
|||||||
clawdbot onboard --auth-choice moonshot-api-key
|
clawdbot onboard --auth-choice moonshot-api-key
|
||||||
```
|
```
|
||||||
|
|
||||||
Kimi Code:
|
## Config snippet
|
||||||
|
|
||||||
```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
|
||||||
{
|
{
|
||||||
@@ -103,48 +92,9 @@ Note: Moonshot and Kimi Code are separate providers. Keys are not interchangeabl
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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
|
||||||
|
|
||||||
- Moonshot model refs use `moonshot/<modelId>`. Kimi Code model refs use `kimi-code/<modelId>`.
|
- Model refs use `moonshot/<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.
|
||||||
|
|||||||
@@ -95,8 +95,7 @@ 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.
|
||||||
- **Kimi Code**: config is auto-written.
|
- More detail: [Moonshot AI](/providers/moonshot)
|
||||||
- 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, logVerbose } from "../../../../../src/globals.js";
|
import { danger } 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,12 +18,7 @@ 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) {
|
||||||
const hasMedia = Boolean(reply?.mediaUrl) || (reply?.mediaUrls?.length ?? 0) > 0;
|
if (!reply?.text && !reply?.mediaUrl && !(reply?.mediaUrls?.length ?? 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;
|
||||||
}
|
}
|
||||||
@@ -62,7 +57,6 @@ 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;
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
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,17 +7,15 @@
|
|||||||
* - 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" as const;
|
export const M_POLL_START = "m.poll.start";
|
||||||
export const M_POLL_RESPONSE = "m.poll.response" as const;
|
export const M_POLL_RESPONSE = "m.poll.response";
|
||||||
export const M_POLL_END = "m.poll.end" as const;
|
export const M_POLL_END = "m.poll.end";
|
||||||
|
|
||||||
export const ORG_POLL_START = "org.matrix.msc3381.poll.start" as const;
|
export const ORG_POLL_START = "org.matrix.msc3381.poll.start";
|
||||||
export const ORG_POLL_RESPONSE = "org.matrix.msc3381.poll.response" as const;
|
export const ORG_POLL_RESPONSE = "org.matrix.msc3381.poll.response";
|
||||||
export const ORG_POLL_END = "org.matrix.msc3381.poll.end" as const;
|
export const ORG_POLL_END = "org.matrix.msc3381.poll.end";
|
||||||
|
|
||||||
export const POLL_EVENT_TYPES = [
|
export const POLL_EVENT_TYPES = [
|
||||||
M_POLL_START,
|
M_POLL_START,
|
||||||
@@ -34,7 +32,9 @@ 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 = ExtensibleAnyMessageEventContent & {
|
export type TextContent = {
|
||||||
|
"m.text"?: string;
|
||||||
|
"org.matrix.msc1767.text"?: string;
|
||||||
body?: string;
|
body?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,19 +42,25 @@ export type PollAnswer = {
|
|||||||
id: string;
|
id: string;
|
||||||
} & TextContent;
|
} & TextContent;
|
||||||
|
|
||||||
export type PollStartSubtype = {
|
export type PollStartContent = {
|
||||||
question: TextContent;
|
"m.poll"?: {
|
||||||
kind?: PollKind;
|
question: TextContent;
|
||||||
max_selections?: number;
|
kind?: PollKind;
|
||||||
answers: PollAnswer[];
|
max_selections?: number;
|
||||||
|
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;
|
||||||
@@ -76,9 +82,7 @@ export function getTextContent(text?: TextContent): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function parsePollStartContent(content: PollStartContent): PollSummary | null {
|
export function parsePollStartContent(content: PollStartContent): PollSummary | null {
|
||||||
const poll = (content as Record<string, PollStartSubtype | undefined>)[M_POLL_START]
|
const poll = content["m.poll"] ?? content["org.matrix.msc3381.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);
|
||||||
@@ -117,11 +121,6 @@ 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
|
||||||
@@ -133,19 +132,13 @@ 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_START]: {
|
"m.poll": {
|
||||||
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,11 +31,6 @@ 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 = () => {
|
||||||
@@ -70,13 +65,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]?.[1] as {
|
const content = sendMessage.mock.calls[0]?.[2] as {
|
||||||
url?: string;
|
url?: string;
|
||||||
msgtype?: string;
|
msgtype?: string;
|
||||||
format?: string;
|
format?: string;
|
||||||
formatted_body?: string;
|
formatted_body?: string;
|
||||||
};
|
};
|
||||||
expect(content.msgtype).toBe("m.image");
|
expect(content.msgtype).toBe("m.file");
|
||||||
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,15 +1,12 @@
|
|||||||
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 {
|
||||||
RoomMessageEventContent,
|
|
||||||
ReactionEventContent,
|
ReactionEventContent,
|
||||||
|
RoomMessageEventContent,
|
||||||
} 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";
|
||||||
@@ -50,8 +47,6 @@ 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() {
|
||||||
@@ -76,12 +71,6 @@ 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("@")) {
|
||||||
@@ -130,18 +119,6 @@ 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;
|
||||||
@@ -150,24 +127,8 @@ 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: Record<string, unknown> = { mimetype: params.mimetype, size: params.size };
|
const info = { 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,
|
||||||
@@ -175,12 +136,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -216,75 +171,6 @@ 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,
|
||||||
@@ -352,10 +238,14 @@ 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 threadId = normalizeThreadId(opts.threadId);
|
const rawThreadId = 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) =>
|
||||||
threadId ? client.sendMessage(roomId, threadId, content) : client.sendMessage(roomId, content);
|
client.sendMessage(roomId, threadId ?? undefined, content);
|
||||||
|
|
||||||
let lastMessageId = "";
|
let lastMessageId = "";
|
||||||
if (opts.mediaUrl) {
|
if (opts.mediaUrl) {
|
||||||
@@ -365,17 +255,9 @@ export async function sendMessageMatrix(
|
|||||||
contentType: media.contentType,
|
contentType: media.contentType,
|
||||||
filename: media.fileName,
|
filename: media.fileName,
|
||||||
});
|
});
|
||||||
const baseMsgType = resolveMatrixMsgType(media.contentType, media.fileName);
|
const msgtype = MsgType.File;
|
||||||
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 = useVoice ? "Voice message" : (firstChunk ?? media.fileName ?? "(file)");
|
const body = firstChunk ?? media.fileName ?? "(file)";
|
||||||
const content = buildMediaContent({
|
const content = buildMediaContent({
|
||||||
msgtype,
|
msgtype,
|
||||||
body,
|
body,
|
||||||
@@ -384,13 +266,10 @@ 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;
|
||||||
const textChunks = useVoice ? chunks : rest;
|
for (const chunk of 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);
|
||||||
@@ -437,19 +316,17 @@ 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 threadId = normalizeThreadId(opts.threadId);
|
const rawThreadId = opts.threadId;
|
||||||
const response = threadId
|
const threadId =
|
||||||
? await client.sendEvent(
|
rawThreadId !== undefined && rawThreadId !== null
|
||||||
roomId,
|
? String(rawThreadId).trim()
|
||||||
threadId,
|
: null;
|
||||||
M_POLL_START,
|
const response = await client.sendEvent(
|
||||||
pollContent,
|
roomId,
|
||||||
)
|
threadId ?? undefined,
|
||||||
: await client.sendEvent(
|
M_POLL_START as EventType.RoomMessage,
|
||||||
roomId,
|
pollContent as unknown as RoomMessageEventContent,
|
||||||
M_POLL_START,
|
);
|
||||||
pollContent,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
eventId: response.event_id ?? "unknown",
|
eventId: response.event_id ?? "unknown",
|
||||||
|
|||||||
@@ -157,7 +157,6 @@ 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,18 +37,6 @@ 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();
|
||||||
@@ -196,26 +184,6 @@ 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,
|
||||||
@@ -244,13 +212,6 @@ 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,7 +39,6 @@ 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,7 +175,6 @@ 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:17Z");
|
const timestamp = new Date("2026-01-12T20:19:17");
|
||||||
vi.setSystemTime(timestamp);
|
vi.setSystemTime(timestamp);
|
||||||
|
|
||||||
enqueueSystemEvent("Model switched.", { sessionKey: "agent:main:main" });
|
enqueueSystemEvent("Model switched.", { sessionKey: "agent:main:main" });
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
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,29 +10,6 @@ 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(
|
||||||
@@ -64,12 +41,25 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
|||||||
},
|
},
|
||||||
handleAction: async ({ action, params, cfg, accountId }) => {
|
handleAction: async ({ action, params, cfg, accountId }) => {
|
||||||
if (action === "send") {
|
if (action === "send") {
|
||||||
const sendParams = readTelegramSendParams(params);
|
const to = readStringParam(params, "to", { required: true });
|
||||||
|
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",
|
||||||
...sendParams,
|
to,
|
||||||
|
content,
|
||||||
|
mediaUrl: mediaUrl ?? undefined,
|
||||||
|
replyToMessageId: replyTo ?? undefined,
|
||||||
|
messageThreadId: threadId ?? undefined,
|
||||||
accountId: accountId ?? undefined,
|
accountId: accountId ?? undefined,
|
||||||
|
buttons,
|
||||||
},
|
},
|
||||||
cfg,
|
cfg,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -180,29 +180,6 @@ 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|kimi-code-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|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,7 +68,6 @@ 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")
|
||||||
@@ -117,7 +116,6 @@ 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,
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ 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 + Kimi Code",
|
hint: "Kimi K2 preview",
|
||||||
choices: ["moonshot-api-key", "kimi-code-api-key"],
|
choices: ["moonshot-api-key"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "zai",
|
value: "zai",
|
||||||
@@ -180,7 +180,6 @@ 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,8 +13,6 @@ 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,
|
||||||
@@ -26,13 +24,11 @@ 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,
|
||||||
@@ -212,55 +208,6 @@ 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,7 +13,6 @@ 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,11 +11,7 @@ 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,
|
||||||
@@ -196,71 +192,6 @@ 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,19 +73,6 @@ 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,13 +12,6 @@ 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 = {
|
||||||
@@ -45,12 +38,6 @@ 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 },
|
||||||
@@ -102,17 +89,3 @@ 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,8 +4,6 @@ export {
|
|||||||
} from "../agents/synthetic-models.js";
|
} from "../agents/synthetic-models.js";
|
||||||
export {
|
export {
|
||||||
applyAuthProfileConfig,
|
applyAuthProfileConfig,
|
||||||
applyKimiCodeConfig,
|
|
||||||
applyKimiCodeProviderConfig,
|
|
||||||
applyMoonshotConfig,
|
applyMoonshotConfig,
|
||||||
applyMoonshotProviderConfig,
|
applyMoonshotProviderConfig,
|
||||||
applyOpenrouterConfig,
|
applyOpenrouterConfig,
|
||||||
@@ -33,7 +31,6 @@ export {
|
|||||||
OPENROUTER_DEFAULT_MODEL_REF,
|
OPENROUTER_DEFAULT_MODEL_REF,
|
||||||
setAnthropicApiKey,
|
setAnthropicApiKey,
|
||||||
setGeminiApiKey,
|
setGeminiApiKey,
|
||||||
setKimiCodeApiKey,
|
|
||||||
setMinimaxApiKey,
|
setMinimaxApiKey,
|
||||||
setMoonshotApiKey,
|
setMoonshotApiKey,
|
||||||
setOpencodeZenApiKey,
|
setOpencodeZenApiKey,
|
||||||
@@ -46,14 +43,10 @@ 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,7 +13,6 @@ 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,
|
||||||
@@ -24,7 +23,6 @@ import {
|
|||||||
applyZaiConfig,
|
applyZaiConfig,
|
||||||
setAnthropicApiKey,
|
setAnthropicApiKey,
|
||||||
setGeminiApiKey,
|
setGeminiApiKey,
|
||||||
setKimiCodeApiKey,
|
|
||||||
setMinimaxApiKey,
|
setMinimaxApiKey,
|
||||||
setMoonshotApiKey,
|
setMoonshotApiKey,
|
||||||
setOpencodeZenApiKey,
|
setOpencodeZenApiKey,
|
||||||
@@ -233,25 +231,6 @@ 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,7 +14,6 @@ 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"
|
||||||
@@ -58,7 +57,6 @@ 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,35 +163,6 @@ 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;
|
||||||
@@ -225,73 +196,6 @@ 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;
|
||||||
@@ -319,34 +223,55 @@ class DiscordCommandArgButton extends Button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async run(interaction: ButtonInteraction, data: ComponentData) {
|
async run(interaction: ButtonInteraction, data: ComponentData) {
|
||||||
await handleDiscordCommandArgInteraction(interaction, data, {
|
const parsed = parseDiscordCommandArgData(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 };
|
||||||
@@ -483,13 +408,11 @@ async function dispatchDiscordCommandInteraction(params: {
|
|||||||
content,
|
content,
|
||||||
...(options?.ephemeral !== undefined ? { ephemeral: options.ephemeral } : {}),
|
...(options?.ephemeral !== undefined ? { ephemeral: options.ephemeral } : {}),
|
||||||
};
|
};
|
||||||
await safeDiscordInteractionCall("interaction reply", async () => {
|
if (preferFollowUp) {
|
||||||
if (preferFollowUp) {
|
await interaction.followUp(payload);
|
||||||
await interaction.followUp(payload);
|
return;
|
||||||
return;
|
}
|
||||||
}
|
await interaction.reply(payload);
|
||||||
await interaction.reply(payload);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
||||||
@@ -644,22 +567,18 @@ async function dispatchDiscordCommandInteraction(params: {
|
|||||||
sessionPrefix,
|
sessionPrefix,
|
||||||
});
|
});
|
||||||
if (preferFollowUp) {
|
if (preferFollowUp) {
|
||||||
await safeDiscordInteractionCall("interaction follow-up", () =>
|
await interaction.followUp({
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -727,23 +646,15 @@ 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) => {
|
||||||
try {
|
await deliverDiscordInteractionReply({
|
||||||
await deliverDiscordInteractionReply({
|
interaction,
|
||||||
interaction,
|
payload,
|
||||||
payload,
|
textLimit: resolveTextChunkLimit(cfg, "discord", accountId, {
|
||||||
textLimit: resolveTextChunkLimit(cfg, "discord", accountId, {
|
fallbackLimit: 2000,
|
||||||
fallbackLimit: 2000,
|
}),
|
||||||
}),
|
maxLinesPerMessage: discordConfig?.maxLinesPerMessage,
|
||||||
maxLinesPerMessage: discordConfig?.maxLinesPerMessage,
|
preferFollowUp: preferFollowUp || didReply,
|
||||||
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) => {
|
||||||
@@ -786,15 +697,13 @@ async function deliverDiscordInteractionReply(params: {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
: { content };
|
: { content };
|
||||||
await safeDiscordInteractionCall("interaction send", async () => {
|
if (!preferFollowUp && !hasReplied) {
|
||||||
if (!preferFollowUp && !hasReplied) {
|
await interaction.reply(payload);
|
||||||
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,10 +27,7 @@ import {
|
|||||||
registerDiscordListener,
|
registerDiscordListener,
|
||||||
} from "./listeners.js";
|
} from "./listeners.js";
|
||||||
import { createDiscordMessageHandler } from "./message-handler.js";
|
import { createDiscordMessageHandler } from "./message-handler.js";
|
||||||
import {
|
import { createDiscordNativeCommand } from "./native-command.js";
|
||||||
createDiscordCommandArgFallbackButton,
|
|
||||||
createDiscordNativeCommand,
|
|
||||||
} from "./native-command.js";
|
|
||||||
|
|
||||||
export type MonitorDiscordOpts = {
|
export type MonitorDiscordOpts = {
|
||||||
token?: string;
|
token?: string;
|
||||||
@@ -152,14 +149,6 @@ 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