Skip to content

Commit afa434d

Browse files
authored
Merge pull request #34 from blackblue-labs/feat/v0.7-local-mcp
feat: ship Kaji v0.7 Local MCP
2 parents d78c3bf + 140365e commit afa434d

8 files changed

Lines changed: 827 additions & 2 deletions

File tree

Sources/Kaji/AppDelegate.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
2929
private lazy var workSession = WorkSessionController(prefs: prefs)
3030
private let systemMonitor = SystemMonitor()
3131
private let dailyGoals = DailyGoalStore()
32+
private lazy var mcpServer = KajiMCPServer(goals: dailyGoals)
3233
private let fixedPlanStore = FixedPlanStore()
3334
private let popoverNavigation = PopoverNavigation()
3435
private let aiNewsStore = AIHotNewsStore()
@@ -45,6 +46,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
4546
sleepController.refresh()
4647
store.start()
4748
applyModuleLifecycle(prefs.enabledModules)
49+
mcpServer.setEnabled(prefs.mcpEnabled)
4850
refreshPetCatalogSelection()
4951

5052
setupStatusItem()
@@ -145,6 +147,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
145147
}
146148
}
147149
.store(in: &cancellables)
150+
prefs.$mcpEnabled
151+
.removeDuplicates()
152+
.receive(on: RunLoop.main)
153+
.sink { [weak self] enabled in
154+
self?.mcpServer.setEnabled(enabled)
155+
}
156+
.store(in: &cancellables)
148157
// Update availability re-renders the glyph (adds/removes the badge dot).
149158
updateChecker.$available
150159
.receive(on: RunLoop.main)
@@ -189,6 +198,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
189198
petStateTimer?.invalidate()
190199
closeBreakOverlay()
191200
aiNewsStore.stop()
201+
mcpServer.stop()
192202
}
193203

194204
func applicationDidBecomeActive(_ notification: Notification) {
@@ -477,7 +487,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
477487
let controller = KajiHostingController(rootView: SettingsView(prefs: prefs,
478488
sleepController: sleepController,
479489
petCatalog: petCatalog,
480-
fixedPlanStore: fixedPlanStore))
490+
fixedPlanStore: fixedPlanStore,
491+
mcpServer: mcpServer))
481492
controller.view.configureKajiHost()
482493
let window = NSWindow(contentViewController: controller)
483494
window.title = "Kaji Settings"

Sources/Kaji/DailyGoalStore.swift

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,79 @@ final class DailyGoalStore: ObservableObject {
105105
}
106106
}
107107

108+
@discardableResult
109+
func addGoal(
110+
title: String,
111+
tag: String,
112+
note: String,
113+
in horizon: GoalHorizon
114+
) throws -> DailyGoal {
115+
let normalizedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
116+
guard !normalizedTitle.isEmpty else {
117+
throw GoalStoreMutationError.emptyTitle
118+
}
119+
let goal = DailyGoal(
120+
id: UUID(),
121+
title: normalizedTitle,
122+
isDone: false,
123+
tag: GoalTagLogic.resolve(tag, title: normalizedTitle).rawValue,
124+
note: note
125+
)
126+
mutate(horizon) { $0.append(goal) }
127+
return goal
128+
}
129+
130+
func updateGoal(
131+
id: UUID,
132+
title: String?,
133+
tag: String?,
134+
note: String?,
135+
in horizon: GoalHorizon
136+
) throws {
137+
var next = state
138+
guard let index = next[horizon].firstIndex(where: { $0.id == id }) else {
139+
throw GoalStoreMutationError.goalNotFound
140+
}
141+
if let title {
142+
let normalizedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
143+
guard !normalizedTitle.isEmpty else {
144+
throw GoalStoreMutationError.emptyTitle
145+
}
146+
next[horizon][index].title = normalizedTitle
147+
}
148+
if let tag {
149+
next[horizon][index].tag = GoalTagLogic.resolve(
150+
tag,
151+
title: next[horizon][index].title
152+
).rawValue
153+
}
154+
if let note {
155+
next[horizon][index].note = note
156+
}
157+
state = next
158+
if horizon == .today { recordToday() }
159+
}
160+
161+
func setGoalCompleted(id: UUID, isDone: Bool, in horizon: GoalHorizon) throws {
162+
var next = state
163+
guard let index = next[horizon].firstIndex(where: { $0.id == id }) else {
164+
throw GoalStoreMutationError.goalNotFound
165+
}
166+
next[horizon][index].isDone = isDone
167+
state = next
168+
if horizon == .today { recordToday() }
169+
}
170+
171+
func deleteGoal(id: UUID, in horizon: GoalHorizon) throws {
172+
var next = state
173+
guard next[horizon].contains(where: { $0.id == id }) else {
174+
throw GoalStoreMutationError.goalNotFound
175+
}
176+
next[horizon].removeAll { $0.id == id }
177+
state = next
178+
if horizon == .today { recordToday() }
179+
}
180+
108181
func move(_ goal: DailyGoal, in horizon: GoalHorizon, offset: Int) {
109182
mutate(horizon) { goals in
110183
guard let source = goals.firstIndex(where: { $0.id == goal.id }) else { return }
@@ -209,3 +282,8 @@ final class DailyGoalStore: ObservableObject {
209282
}
210283

211284
}
285+
286+
enum GoalStoreMutationError: Error {
287+
case goalNotFound
288+
case emptyTitle
289+
}

0 commit comments

Comments
 (0)