@lattices/cli 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +85 -9
  2. package/app/Info.plist +30 -0
  3. package/app/Lattices.app/Contents/Info.plist +8 -2
  4. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/app/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  6. package/app/Lattices.app/Contents/Resources/tap.wav +0 -0
  7. package/app/Lattices.app/Contents/_CodeSignature/CodeResources +139 -0
  8. package/app/Lattices.entitlements +15 -0
  9. package/app/Package.swift +8 -1
  10. package/app/Resources/tap.wav +0 -0
  11. package/app/Sources/AdvisorLearningStore.swift +90 -0
  12. package/app/Sources/AgentSession.swift +377 -0
  13. package/app/Sources/AppDelegate.swift +45 -12
  14. package/app/Sources/AppShellView.swift +81 -8
  15. package/app/Sources/AudioProvider.swift +386 -0
  16. package/app/Sources/CheatSheetHUD.swift +261 -19
  17. package/app/Sources/DaemonProtocol.swift +13 -0
  18. package/app/Sources/DaemonServer.swift +8 -0
  19. package/app/Sources/DesktopModel.swift +189 -6
  20. package/app/Sources/DesktopModelTypes.swift +2 -0
  21. package/app/Sources/DiagnosticLog.swift +104 -2
  22. package/app/Sources/EventBus.swift +1 -0
  23. package/app/Sources/HUDBottomBar.swift +279 -0
  24. package/app/Sources/HUDController.swift +1158 -0
  25. package/app/Sources/HUDLeftBar.swift +849 -0
  26. package/app/Sources/HUDMinimap.swift +179 -0
  27. package/app/Sources/HUDRightBar.swift +774 -0
  28. package/app/Sources/HUDState.swift +367 -0
  29. package/app/Sources/HUDTopBar.swift +243 -0
  30. package/app/Sources/HandsOffSession.swift +802 -0
  31. package/app/Sources/HomeDashboardView.swift +125 -0
  32. package/app/Sources/HotkeyManager.swift +2 -0
  33. package/app/Sources/HotkeyStore.swift +49 -9
  34. package/app/Sources/IntentEngine.swift +962 -0
  35. package/app/Sources/Intents/CreateLayerIntent.swift +54 -0
  36. package/app/Sources/Intents/DistributeIntent.swift +56 -0
  37. package/app/Sources/Intents/FocusIntent.swift +69 -0
  38. package/app/Sources/Intents/HelpIntent.swift +41 -0
  39. package/app/Sources/Intents/KillIntent.swift +47 -0
  40. package/app/Sources/Intents/LatticeIntent.swift +78 -0
  41. package/app/Sources/Intents/LaunchIntent.swift +67 -0
  42. package/app/Sources/Intents/ListSessionsIntent.swift +32 -0
  43. package/app/Sources/Intents/ListWindowsIntent.swift +30 -0
  44. package/app/Sources/Intents/ScanIntent.swift +52 -0
  45. package/app/Sources/Intents/SearchIntent.swift +190 -0
  46. package/app/Sources/Intents/SwitchLayerIntent.swift +50 -0
  47. package/app/Sources/Intents/TileIntent.swift +61 -0
  48. package/app/Sources/LatticesApi.swift +1275 -30
  49. package/app/Sources/LauncherHUD.swift +348 -0
  50. package/app/Sources/MainView.swift +147 -44
  51. package/app/Sources/MouseFinder.swift +222 -0
  52. package/app/Sources/OcrModel.swift +34 -1
  53. package/app/Sources/OmniSearchState.swift +99 -102
  54. package/app/Sources/OnboardingView.swift +457 -0
  55. package/app/Sources/PermissionChecker.swift +2 -12
  56. package/app/Sources/PiChatDock.swift +454 -0
  57. package/app/Sources/PiChatSession.swift +815 -0
  58. package/app/Sources/PiWorkspaceView.swift +364 -0
  59. package/app/Sources/PlacementSpec.swift +195 -0
  60. package/app/Sources/Preferences.swift +59 -0
  61. package/app/Sources/ProjectScanner.swift +58 -45
  62. package/app/Sources/ScreenMapState.swift +701 -55
  63. package/app/Sources/ScreenMapView.swift +843 -103
  64. package/app/Sources/ScreenMapWindowController.swift +22 -0
  65. package/app/Sources/SessionLayerStore.swift +285 -0
  66. package/app/Sources/SessionManager.swift +4 -1
  67. package/app/Sources/SettingsView.swift +186 -3
  68. package/app/Sources/Theme.swift +9 -8
  69. package/app/Sources/TmuxModel.swift +7 -0
  70. package/app/Sources/TmuxQuery.swift +27 -3
  71. package/app/Sources/VoiceChatView.swift +192 -0
  72. package/app/Sources/VoiceCommandWindow.swift +1594 -0
  73. package/app/Sources/VoiceIntentResolver.swift +671 -0
  74. package/app/Sources/VoxClient.swift +454 -0
  75. package/app/Sources/WindowTiler.swift +348 -87
  76. package/app/Sources/WorkspaceManager.swift +127 -18
  77. package/app/Tests/StageDragTests.swift +333 -0
  78. package/app/Tests/StageJoinTests.swift +313 -0
  79. package/app/Tests/StageManagerTests.swift +280 -0
  80. package/app/Tests/StageTileTests.swift +353 -0
  81. package/assets/AppIcon.icns +0 -0
  82. package/bin/client.ts +16 -0
  83. package/bin/{daemon-client.js → daemon-client.ts} +49 -30
  84. package/bin/handsoff-infer.ts +280 -0
  85. package/bin/handsoff-worker.ts +740 -0
  86. package/bin/lattices-app.ts +338 -0
  87. package/bin/lattices-dev +208 -0
  88. package/bin/{lattices.js → lattices.ts} +777 -140
  89. package/bin/project-twin.ts +645 -0
  90. package/docs/agent-execution-plan.md +562 -0
  91. package/docs/agent-layer-guide.md +207 -0
  92. package/docs/agents.md +142 -0
  93. package/docs/api.md +153 -34
  94. package/docs/app.md +29 -1
  95. package/docs/config.md +5 -1
  96. package/docs/handsoff-test-scenarios.md +84 -0
  97. package/docs/layers.md +20 -20
  98. package/docs/ocr.md +14 -5
  99. package/docs/overview.md +5 -1
  100. package/docs/presentation-execution-review.md +491 -0
  101. package/docs/prompts/hands-off-system.md +374 -0
  102. package/docs/prompts/hands-off-turn.md +30 -0
  103. package/docs/prompts/voice-advisor.md +31 -0
  104. package/docs/prompts/voice-fallback.md +23 -0
  105. package/docs/tiling-reference.md +167 -0
  106. package/docs/twins.md +138 -0
  107. package/docs/voice-command-protocol.md +278 -0
  108. package/docs/voice.md +219 -0
  109. package/package.json +29 -11
  110. package/bin/client.js +0 -4
  111. package/bin/lattices-app.js +0 -221
package/README.md CHANGED
@@ -4,16 +4,62 @@
4
4
 
5
5
  # lattices
6
6
 
7
- A workspace control plane for macOS. Manage persistent terminal sessions,
8
- tile and organize your windows, and index the text on your screen — all
9
- controllable from the CLI or a 30-method daemon API.
7
+ The agentic window manager for macOS.
8
+
9
+ Tile windows with hotkeys, manage persistent tmux sessions, index screen
10
+ text with OCR, and give AI agents a 35-method desktop API — all from a
11
+ native menu bar app and CLI.
12
+
13
+ **[lattices.dev](https://lattices.dev)** · [Docs](https://lattices.dev/docs/overview) · [Download](https://github.com/arach/lattices/releases/latest)
10
14
 
11
15
  ## Install
12
16
 
17
+ ### Download the app
18
+
19
+ Grab the signed DMG from the [latest release](https://github.com/arach/lattices/releases/latest):
20
+
21
+ ```sh
22
+ # Or direct download:
23
+ curl -LO https://github.com/arach/lattices/releases/latest/download/Lattices.dmg
24
+ open Lattices.dmg
25
+ ```
26
+
27
+ Drag **Lattices.app** into Applications. On first launch, a setup wizard
28
+ walks you through granting Accessibility, Screen Recording, and choosing
29
+ your project directory.
30
+
31
+ ### Install the CLI
32
+
13
33
  ```sh
14
34
  npm install -g @lattices/cli
15
35
  ```
16
36
 
37
+ The CLI and app work independently — use either or both.
38
+
39
+ ### Build from source
40
+
41
+ ```sh
42
+ git clone https://github.com/arach/lattices.git
43
+ cd lattices
44
+
45
+ # Build the menu bar app (requires Swift 5.9+ / Xcode 15+)
46
+ cd app && swift build -c release && cd ..
47
+
48
+ # Install CLI dependencies
49
+ npm install
50
+
51
+ # Launch
52
+ node bin/lattices-app.js build # bundle the .app
53
+ node bin/lattices-app.js # launch it
54
+ ```
55
+
56
+ To build a signed, notarized DMG for distribution:
57
+
58
+ ```sh
59
+ # Requires a Developer ID certificate and notarytool keychain profile
60
+ ./scripts/build-dmg.sh
61
+ ```
62
+
17
63
  ## Quick start
18
64
 
19
65
  ```sh
@@ -77,8 +123,8 @@ Bundle related repos as tabs in one session. Each tab gets its own
77
123
  pane layout from its `.lattices.json`.
78
124
 
79
125
  ```sh
80
- lattices group talkie # Launch iOS, macOS, Web, API as tabs
81
- lattices tab talkie iOS # Switch to the iOS tab
126
+ lattices group vox # Launch iOS, macOS, Web, API as tabs
127
+ lattices tab vox iOS # Switch to the iOS tab
82
128
  ```
83
129
 
84
130
  ## Window tiling and awareness
@@ -97,16 +143,31 @@ lattices scan recent # Browse scan history
97
143
  lattices scan deep # Trigger a Vision OCR scan now
98
144
  ```
99
145
 
146
+ ## Voice commands (beta)
147
+
148
+ Speak to control your workspace — tile windows, search, focus apps,
149
+ and launch projects with natural language. Powered by
150
+ [Vox](https://github.com/arach/vox) for transcription and
151
+ local NLEmbedding for intent matching, with Claude fallback for
152
+ ambiguous commands.
153
+
154
+ Trigger with `Hyper+3` (configurable). Press Space to speak, Space to
155
+ stop. The panel shows what was heard, the matched intent, extracted
156
+ parameters, and execution results.
157
+
100
158
  ## A programmable desktop
101
159
 
102
- The menu bar app runs a daemon with 30 RPC methods and 5 real-time
160
+ The menu bar app runs a daemon with 35 RPC methods and 5 real-time
103
161
  events over WebSocket. Anything you can do from the app, an agent or
104
162
  script can do over the API.
105
163
 
106
164
  ```js
107
165
  import { daemonCall } from '@lattices/cli/daemon-client'
108
166
 
109
- const windows = await daemonCall('windows.list')
167
+ // Search windows by content — title, app, session tags, OCR
168
+ const results = await daemonCall('windows.search', { query: 'myproject' })
169
+
170
+ // Launch and tile
110
171
  await daemonCall('session.launch', { path: '/Users/you/dev/frontend' })
111
172
  await daemonCall('window.tile', { session: 'frontend-a1b2c3', position: 'left' })
112
173
 
@@ -115,6 +176,14 @@ await daemonCall('ocr.scan')
115
176
  const errors = await daemonCall('ocr.search', { query: 'error OR failed' })
116
177
  ```
117
178
 
179
+ Or from the CLI:
180
+
181
+ ```sh
182
+ lattices search myproject # Find windows by content
183
+ lattices search myproject --deep # Include terminal tab/process data
184
+ lattices place myproject left # Search + focus + tile in one step
185
+ ```
186
+
118
187
  Claude Code skills, MCP servers, or your own scripts can drive your
119
188
  desktop the same way you do.
120
189
 
@@ -125,12 +194,15 @@ lattices Create or reattach to session
125
194
  lattices init Generate .lattices.json
126
195
  lattices ls List active sessions
127
196
  lattices kill [name] Kill a session
197
+ lattices search <query> Search windows by title, app, session, OCR
198
+ lattices search <q> --deep Deep search: index + terminal inspection
199
+ lattices place <query> [pos] Deep search + focus + tile
200
+ lattices focus <session> Raise a session's window
128
201
  lattices tile <position> Tile frontmost window
129
202
  lattices group [id] Launch or attach a tab group
130
203
  lattices tab <group> [tab] Switch tab within a group
131
204
  lattices scan View current screen text
132
205
  lattices scan search <q> Search indexed text
133
- lattices scan recent [n] Browse scan history
134
206
  lattices scan deep Trigger Vision OCR now
135
207
  lattices app Launch the menu bar app
136
208
  lattices help Show help
@@ -148,7 +220,11 @@ lattices help Show help
148
220
 
149
221
  ## Docs
150
222
 
151
- [lattices.dev/docs](https://lattices.dev/docs/overview)
223
+ Full documentation at [lattices.dev/docs](https://lattices.dev/docs/overview), including:
224
+
225
+ - [API reference](https://lattices.dev/docs/api) — all 35 daemon methods
226
+ - [Layers](https://lattices.dev/docs/layers) — workspace layers and tab groups
227
+ - [Voice commands](https://lattices.dev/docs/voice) — Vox integration
152
228
 
153
229
  ## License
154
230
 
package/app/Info.plist ADDED
@@ -0,0 +1,30 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleIdentifier</key>
6
+ <string>com.arach.lattices</string>
7
+ <key>CFBundleName</key>
8
+ <string>Lattices</string>
9
+ <key>CFBundleDisplayName</key>
10
+ <string>Lattices</string>
11
+ <key>CFBundleExecutable</key>
12
+ <string>Lattices</string>
13
+ <key>CFBundleIconFile</key>
14
+ <string>AppIcon</string>
15
+ <key>CFBundlePackageType</key>
16
+ <string>APPL</string>
17
+ <key>CFBundleVersion</key>
18
+ <string>0.4.1</string>
19
+ <key>CFBundleShortVersionString</key>
20
+ <string>0.4.1</string>
21
+ <key>LSMinimumSystemVersion</key>
22
+ <string>13.0</string>
23
+ <key>LSUIElement</key>
24
+ <true/>
25
+ <key>NSHighResolutionCapable</key>
26
+ <true/>
27
+ <key>NSSupportsAutomaticTermination</key>
28
+ <true/>
29
+ </dict>
30
+ </plist>
@@ -6,6 +6,8 @@
6
6
  <string>com.arach.lattices</string>
7
7
  <key>CFBundleName</key>
8
8
  <string>Lattices</string>
9
+ <key>CFBundleDisplayName</key>
10
+ <string>Lattices</string>
9
11
  <key>CFBundleExecutable</key>
10
12
  <string>Lattices</string>
11
13
  <key>CFBundleIconFile</key>
@@ -13,11 +15,15 @@
13
15
  <key>CFBundlePackageType</key>
14
16
  <string>APPL</string>
15
17
  <key>CFBundleVersion</key>
16
- <string>1</string>
18
+ <string>0.4.1</string>
17
19
  <key>CFBundleShortVersionString</key>
18
- <string>0.1.0</string>
20
+ <string>0.4.1</string>
21
+ <key>LSMinimumSystemVersion</key>
22
+ <string>13.0</string>
19
23
  <key>LSUIElement</key>
20
24
  <true/>
25
+ <key>NSHighResolutionCapable</key>
26
+ <true/>
21
27
  <key>NSSupportsAutomaticTermination</key>
22
28
  <true/>
23
29
  </dict>
@@ -0,0 +1,139 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>files</key>
6
+ <dict>
7
+ <key>Resources/AppIcon.icns</key>
8
+ <data>
9
+ 3sIZmtGJHMo2S/XvIMl46SOHcFY=
10
+ </data>
11
+ <key>Resources/tap.wav</key>
12
+ <data>
13
+ eOpp5td/ovQGMumXPpwy4Vyt/uc=
14
+ </data>
15
+ </dict>
16
+ <key>files2</key>
17
+ <dict>
18
+ <key>Resources/AppIcon.icns</key>
19
+ <dict>
20
+ <key>hash2</key>
21
+ <data>
22
+ LZsztS/9I1hmuQmDOk+anfxOpqVryB3y4a1kwSaUK4s=
23
+ </data>
24
+ </dict>
25
+ <key>Resources/tap.wav</key>
26
+ <dict>
27
+ <key>hash2</key>
28
+ <data>
29
+ K4QV08FuKEJR29hhgUbEG7Em3J6zHYpGKmGWdnZopzs=
30
+ </data>
31
+ </dict>
32
+ </dict>
33
+ <key>rules</key>
34
+ <dict>
35
+ <key>^Resources/</key>
36
+ <true/>
37
+ <key>^Resources/.*\.lproj/</key>
38
+ <dict>
39
+ <key>optional</key>
40
+ <true/>
41
+ <key>weight</key>
42
+ <real>1000</real>
43
+ </dict>
44
+ <key>^Resources/.*\.lproj/locversion.plist$</key>
45
+ <dict>
46
+ <key>omit</key>
47
+ <true/>
48
+ <key>weight</key>
49
+ <real>1100</real>
50
+ </dict>
51
+ <key>^Resources/Base\.lproj/</key>
52
+ <dict>
53
+ <key>weight</key>
54
+ <real>1010</real>
55
+ </dict>
56
+ <key>^version.plist$</key>
57
+ <true/>
58
+ </dict>
59
+ <key>rules2</key>
60
+ <dict>
61
+ <key>.*\.dSYM($|/)</key>
62
+ <dict>
63
+ <key>weight</key>
64
+ <real>11</real>
65
+ </dict>
66
+ <key>^(.*/)?\.DS_Store$</key>
67
+ <dict>
68
+ <key>omit</key>
69
+ <true/>
70
+ <key>weight</key>
71
+ <real>2000</real>
72
+ </dict>
73
+ <key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
74
+ <dict>
75
+ <key>nested</key>
76
+ <true/>
77
+ <key>weight</key>
78
+ <real>10</real>
79
+ </dict>
80
+ <key>^.*</key>
81
+ <true/>
82
+ <key>^Info\.plist$</key>
83
+ <dict>
84
+ <key>omit</key>
85
+ <true/>
86
+ <key>weight</key>
87
+ <real>20</real>
88
+ </dict>
89
+ <key>^PkgInfo$</key>
90
+ <dict>
91
+ <key>omit</key>
92
+ <true/>
93
+ <key>weight</key>
94
+ <real>20</real>
95
+ </dict>
96
+ <key>^Resources/</key>
97
+ <dict>
98
+ <key>weight</key>
99
+ <real>20</real>
100
+ </dict>
101
+ <key>^Resources/.*\.lproj/</key>
102
+ <dict>
103
+ <key>optional</key>
104
+ <true/>
105
+ <key>weight</key>
106
+ <real>1000</real>
107
+ </dict>
108
+ <key>^Resources/.*\.lproj/locversion.plist$</key>
109
+ <dict>
110
+ <key>omit</key>
111
+ <true/>
112
+ <key>weight</key>
113
+ <real>1100</real>
114
+ </dict>
115
+ <key>^Resources/Base\.lproj/</key>
116
+ <dict>
117
+ <key>weight</key>
118
+ <real>1010</real>
119
+ </dict>
120
+ <key>^[^/]+$</key>
121
+ <dict>
122
+ <key>nested</key>
123
+ <true/>
124
+ <key>weight</key>
125
+ <real>10</real>
126
+ </dict>
127
+ <key>^embedded\.provisionprofile$</key>
128
+ <dict>
129
+ <key>weight</key>
130
+ <real>20</real>
131
+ </dict>
132
+ <key>^version\.plist$</key>
133
+ <dict>
134
+ <key>weight</key>
135
+ <real>20</real>
136
+ </dict>
137
+ </dict>
138
+ </dict>
139
+ </plist>
@@ -0,0 +1,15 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <!-- App sandbox disabled — Lattices needs direct access to tmux, processes, and the filesystem -->
6
+ <key>com.apple.security.app-sandbox</key>
7
+ <false/>
8
+
9
+ <!-- Network: localhost WebSocket daemon for CLI/agent communication -->
10
+ <key>com.apple.security.network.server</key>
11
+ <true/>
12
+ <key>com.apple.security.network.client</key>
13
+ <true/>
14
+ </dict>
15
+ </plist>
package/app/Package.swift CHANGED
@@ -7,7 +7,14 @@ let package = Package(
7
7
  targets: [
8
8
  .executableTarget(
9
9
  name: "Lattices",
10
- path: "Sources"
10
+ path: "Sources",
11
+ resources: [
12
+ .copy("../Resources/tap.wav"),
13
+ ]
14
+ ),
15
+ .testTarget(
16
+ name: "LatticesTests",
17
+ path: "Tests"
11
18
  )
12
19
  ]
13
20
  )
Binary file
@@ -0,0 +1,90 @@
1
+ import Foundation
2
+
3
+ /// Captures moments where the Claude advisor resolved something the local matcher couldn't.
4
+ /// Each entry records the transcript, what the local system matched (or missed), and what
5
+ /// the advisor suggested that the user accepted.
6
+ ///
7
+ /// For now this is append-only — just growing the dataset. Future work can use it to
8
+ /// improve local matching without needing the advisor.
9
+
10
+ final class AdvisorLearningStore {
11
+ static let shared = AdvisorLearningStore()
12
+
13
+ struct Entry: Codable {
14
+ let timestamp: String
15
+ let transcript: String
16
+ let localIntent: String?
17
+ let localSlots: [String: String]
18
+ let localResultCount: Int
19
+ let advisorIntent: String
20
+ let advisorSlots: [String: String]
21
+ let advisorLabel: String
22
+ }
23
+
24
+ private let fileURL: URL
25
+ private let queue = DispatchQueue(label: "com.lattices.advisor-learning")
26
+ private static let isoFmt: ISO8601DateFormatter = {
27
+ let f = ISO8601DateFormatter()
28
+ f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
29
+ return f
30
+ }()
31
+
32
+ private init() {
33
+ let dir = FileManager.default.homeDirectoryForCurrentUser
34
+ .appendingPathComponent(".lattices")
35
+ try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
36
+ fileURL = dir.appendingPathComponent("advisor-learning.jsonl")
37
+ }
38
+
39
+ /// Record that the user engaged with an advisor suggestion.
40
+ func record(
41
+ transcript: String,
42
+ localIntent: String?,
43
+ localSlots: [String: String],
44
+ localResultCount: Int,
45
+ advisorIntent: String,
46
+ advisorSlots: [String: String],
47
+ advisorLabel: String
48
+ ) {
49
+ let entry = Entry(
50
+ timestamp: Self.isoFmt.string(from: Date()),
51
+ transcript: transcript,
52
+ localIntent: localIntent,
53
+ localSlots: localSlots,
54
+ localResultCount: localResultCount,
55
+ advisorIntent: advisorIntent,
56
+ advisorSlots: advisorSlots,
57
+ advisorLabel: advisorLabel
58
+ )
59
+
60
+ queue.async {
61
+ guard let data = try? JSONEncoder().encode(entry),
62
+ var line = String(data: data, encoding: .utf8) else { return }
63
+ line += "\n"
64
+
65
+ if let handle = try? FileHandle(forWritingTo: self.fileURL) {
66
+ handle.seekToEndOfFile()
67
+ handle.write(line.data(using: .utf8)!)
68
+ handle.closeFile()
69
+ } else {
70
+ try? line.data(using: .utf8)?.write(to: self.fileURL)
71
+ }
72
+
73
+ DiagnosticLog.shared.info("AdvisorLearning: captured [\(transcript)] → \(advisorIntent)(\(advisorSlots))")
74
+ }
75
+ }
76
+
77
+ /// Read all entries (for analysis).
78
+ func allEntries() -> [Entry] {
79
+ guard let data = try? String(contentsOf: fileURL, encoding: .utf8) else { return [] }
80
+ return data.components(separatedBy: "\n").compactMap { line in
81
+ guard !line.isEmpty, let d = line.data(using: .utf8) else { return nil }
82
+ return try? JSONDecoder().decode(Entry.self, from: d)
83
+ }
84
+ }
85
+
86
+ var entryCount: Int {
87
+ guard let data = try? String(contentsOf: fileURL, encoding: .utf8) else { return 0 }
88
+ return data.components(separatedBy: "\n").filter { !$0.isEmpty }.count
89
+ }
90
+ }