samagotchi 0.6.0 → 0.7.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 220ee751308788beff6b5185350bafe29023706f2e63daf45c8bffab023e173e
4
- data.tar.gz: 184c4bc6f92476939901813d8a9b20353a73bb64eb5725d3564ca302bf27559e
3
+ metadata.gz: ca1d3be1642f5b3a7eb95aad166a1d613e125d5f82d6c79e27f54860c676650f
4
+ data.tar.gz: 839db8c02d67e9dab21143e00d19aa04ff09eddf974598f24a5811719001c70d
5
5
  SHA512:
6
- metadata.gz: 7349948ddc41e8ccce71e742d4fc81f51f52290b56a2943efc690adc6e73e4c41fe37fa94dee41c50e20a0c910b3c66a55176955048bf3efa2003e9520c36642
7
- data.tar.gz: a2059d2df83be31064bfb05f311c7088e777d2901fa00695f4f52208af3b21038471d7180ad26761b6f3f280a7b2f347f560ca39517e0bfcd52715247ba88477
6
+ metadata.gz: 06d07b5787fa0f5fdcc71c43ad19bbc22f36670030907d841c78ea5ad103569b514c356e493a55b8d669925a35b4ce63ccb6217dcb1209f3e54530477cc1de45
7
+ data.tar.gz: 36edb5314aba266d1724b270ccc6f1645372431b1db3a173519f71ca641b31636531a63fecad7f5ff3af6218fffbca9268a46d0d1d1eb4e030851e519c992ae9
data/CHANGELOG.md CHANGED
@@ -8,6 +8,18 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.7.0] - 2026-09-30
12
+
13
+ ### Added
14
+
15
+ - The desktop helper also sends to agent CLIs (claude, codex, …) in kitty windows: with `kitty.listen_on` copied
16
+ from kitty.conf (and `chi desktop upgrade`), the panel lists them in a "kitty" group next to the sessions; ⏎
17
+ pastes the quote, message and image paths and presses Enter, the new ⌥⏎ only pastes (so several screenshots
18
+ can pile up), and one send can go to chi sessions and kitty windows together. ⌥⏎ no longer inserts a newline
19
+ in the panel (⇧⏎ does). See docs/desktop.md "Agents in kitty".
20
+
21
+ Update with `chi update`. No bundle versions changed.
22
+
11
23
  ## [0.6.0] - 2026-09-30
12
24
 
13
25
  ### Added
@@ -393,7 +405,8 @@ and long-lived sessions.
393
405
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
394
406
  hotkey panel that send selected text or the clipboard to your sessions.
395
407
 
396
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.6.0...HEAD
408
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.7.0...HEAD
409
+ [0.7.0]: https://github.com/dm1try/samagotchi/compare/v0.6.0...v0.7.0
397
410
  [0.6.0]: https://github.com/dm1try/samagotchi/compare/v0.5.1...v0.6.0
398
411
  [0.5.1]: https://github.com/dm1try/samagotchi/compare/v0.5.0...v0.5.1
399
412
  [0.5.0]: https://github.com/dm1try/samagotchi/compare/v0.4.0...v0.5.0
@@ -687,6 +687,23 @@ hosts:
687
687
  `models:` wins over `hosts:`. On a native host, `vision: true` skips only the
688
688
  modalities check: without a media marker the prompt can't carry an image.
689
689
 
690
+ ## Desktop helper: agents in kitty
691
+
692
+ The desktop helper (`chi desktop`) can also paste into agent CLIs (claude,
693
+ codex, …) running in [kitty](https://sw.kovidgoyal.net/kitty/) windows. It
694
+ lists them only with `kitty.listen_on` set: copy the value of `listen_on` from
695
+ your `kitty.conf` as it is (kitty must also have `allow_remote_control` on).
696
+
697
+ ```yaml
698
+ kitty:
699
+ listen_on: unix:/tmp/kitty.${KITTY_PID} # as in kitty.conf; unset = no kitty targets
700
+ binary: /Applications/kitty.app/Contents/MacOS/kitty # the default
701
+ agents: [claude, codex] # the programs listed; "*" = every window
702
+ ```
703
+
704
+ The helper reads these from its launch file: after editing them run
705
+ `chi desktop upgrade` (or `chi update`). See [Desktop helper](desktop.md#agents-in-kitty).
706
+
690
707
  ## Project specific description
691
708
 
692
709
  If an AGENT.md file is present in the project root, samagotchi injects its
@@ -756,6 +773,9 @@ described in their own sections.
756
773
  | `update.gem` | `true` | | `false`: `chi update` never installs a newer gem (`--no-gem` for one run). See [CLI: Updating](cli.md#updating). |
757
774
  | `update.bundles` | `true` | | `false`: `chi update` leaves the shipped bundles to `chi bundle upgrade` (`--no-bundles`). |
758
775
  | `update.desktop` | `true` | | `false`: `chi update` leaves the desktop helper alone (`--no-desktop`). |
776
+ | `kitty.listen_on` | none | | The desktop helper's kitty socket, copied from `kitty.conf`; unset = no kitty targets. See "Desktop helper: agents in kitty". |
777
+ | `kitty.binary` | `/Applications/kitty.app/Contents/MacOS/kitty` | | The kitty the helper runs `kitty @` with. |
778
+ | `kitty.agents` | `claude\|codex\|gemini\|aider\|opencode\|cursor-agent\|amp\|goose` | | Foreground programs whose kitty windows the helper lists, `\|`-separated (a YAML list works too); `*` lists every window. |
759
779
  | `read.truncate_at_bytes` | `65536` | yes | A `read` result larger than this is cut to a preview. |
760
780
  | `read.preview_bytes` | `12288` | yes | Size of that preview. |
761
781
  | `read.hard_max_bytes` | `2097152` | yes | Largest file `read` opens. |
data/docs/desktop.md CHANGED
@@ -20,6 +20,8 @@ sessions. Keys:
20
20
  - **⏎** sends a message: `chi send -m <the line>` with the text as quoted context above it. With the line empty,
21
21
  the text is the message.
22
22
  - **⌘⏎** sends a note: `chi note`, the line (if any) then the text.
23
+ - **⌥⏎** pastes into an agent in a kitty window without pressing Enter (see [Agents in kitty](#agents-in-kitty));
24
+ it is not a newline.
23
25
  - **⇧⏎** is a newline, in either field. Esc closes.
24
26
 
25
27
  The list shows the sessions a worker runs now, then, under a "recent" divider, up to 3 stopped ones (dimmed, with
@@ -64,6 +66,53 @@ thumbnails sit under the message line, 48 px high, the file name as a tooltip; h
64
66
  the panel closes; files from Finder are sent as they are, never touched. A send with images may take up to 30 s
65
67
  (converting, a worker starting) before the panel gives up.
66
68
 
69
+ ## Agents in kitty
70
+
71
+ The panel can also send to agent CLIs (claude, codex, gemini, aider, …) running in
72
+ [kitty](https://sw.kovidgoyal.net/kitty/) windows, next to chi sessions. kitty only, through its remote control;
73
+ off until you set it up:
74
+
75
+ 1. In `kitty.conf`, remote control on and a socket: `allow_remote_control yes` and a `listen_on unix:…` line.
76
+ 2. Copy that `listen_on` value as it is into chi's config.yml:
77
+
78
+ ```yaml
79
+ kitty:
80
+ listen_on: unix:/tmp/kitty.${KITTY_PID}
81
+ ```
82
+
83
+ `kitty.agents` picks the programs listed (default `claude|codex|gemini|aider|opencode|cursor-agent|amp|goose`, a
84
+ YAML list works too; `"*"` lists every window, shells too), `kitty.binary` the kitty to run (default
85
+ `/Applications/kitty.app/Contents/MacOS/kitty`). See [Configuration](configuration.md#desktop-helper-agents-in-kitty).
86
+ 3. Run `chi desktop upgrade` (or `chi update`): the helper reads these from its launch file, so **after every edit of
87
+ `kitty:`** run it again. No rebuild when only the settings changed.
88
+
89
+ The helper finds the sockets itself: kitty adds `-<pid>` to a `listen_on` path from kitty.conf (and fills in
90
+ `{kitty_pid}`), `~` and variables the helper knows are expanded, others (`${KITTY_PID}`) stay as written, as kitty
91
+ keeps them. Several kitty instances all count. Only `unix:` sockets work.
92
+
93
+ The panel lists the windows whose foreground program is one of the agents under a **kitty** divider, between live
94
+ and recent sessions: "claude · <its title>", the folder below, a terminal icon, and a ⌘ number like the sessions.
95
+ With the settings on but no agent running, one dim line says "no agents in kitty". A window counts when a foreground
96
+ process's program is an agent, also as the script of `node …/codex` (that match is untested for codex and gemini).
97
+
98
+ - **⏎** pastes into every chosen window and presses Enter: the text as a `> ` quote, a blank line, your message, then
99
+ one image path per line. Chosen together with chi sessions, each gets the message and the panel shows one line
100
+ ("Sent. · sent to claude · samagotchi").
101
+ - **⌥⏎** only pastes (no Enter), and only when every chosen target is a kitty window: build up several screenshots,
102
+ each with its own line, then press Enter in kitty. The footer shows **Paste ⌥⏎** instead of Note then.
103
+ - **⌘⏎** with a kitty window chosen beeps: "Notes are for chi sessions". ⌥⏎ with a chi session: "Paste only is
104
+ for kitty windows".
105
+ - **Images:** a screenshot or other image data is copied to `$TMPDIR/chi-helper-sent/<uuid>.png` (or `.tiff`) and
106
+ its path pasted; the agent reads it from there (claude attaches it as `[Image #1]`). These files are deleted a week
107
+ later, at the helper's next start. Finder files are pasted with their own path, `'…'`-quoted when they hold spaces.
108
+ - The paste arrives as one bracketed paste, so the agent takes newlines as text (claude shows long ones as
109
+ `[Pasted text]`). With `agents: "*"` beware of a program that doesn't turn bracketed paste on: the newlines would
110
+ act as Enter.
111
+ - No delivery confirmation: before pasting, the helper checks the window is still there ("window closed" if not);
112
+ it can't know whether the agent read it. The 16 KB cap is chi's; a kitty window takes more.
113
+ - Focus goes back to the app you came from, as for sessions. The last choice (kitty windows too) is preselected
114
+ while listed; ⌘ numbers shift as windows come and go.
115
+
67
116
  ## Install
68
117
 
69
118
  ```sh
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.6.0
3
+ version: 0.7.0
4
4
  scope: system
5
5
  description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
6
6
  files:
@@ -135,6 +135,11 @@ module Samagotchi
135
135
  Entry.new(key: "update.gem", yaml_path: %w[update gem], type: :bool, default: true, expose: %i[env config]),
136
136
  Entry.new(key: "update.bundles", yaml_path: %w[update bundles], type: :bool, default: true, expose: %i[env config]),
137
137
  Entry.new(key: "update.desktop", yaml_path: %w[update desktop], type: :bool, default: true, expose: %i[env config]),
138
+ # The desktop helper's kitty targets (Desktop::MacOS.kitty_settings): listen_on copied from kitty.conf (unset = none),
139
+ # the kitty binary, and the foreground programs listed, "|"-separated (a YAML list works too; "*" = every window).
140
+ Entry.new(key: "kitty.listen_on", yaml_path: %w[kitty listen_on], type: :string, default: nil, expose: %i[env config]),
141
+ Entry.new(key: "kitty.binary", yaml_path: %w[kitty binary], type: :string, default: "/Applications/kitty.app/Contents/MacOS/kitty", expose: %i[env config]),
142
+ Entry.new(key: "kitty.agents", yaml_path: %w[kitty agents], type: :string, default: "claude|codex|gemini|aider|opencode|cursor-agent|amp|goose", expose: %i[env config]),
138
143
 
139
144
  Entry.new(key: "read.truncate_at_bytes", yaml_path: %w[read truncate_at_bytes], type: :integer, default: 65_536, expose: %i[env config cli]),
140
145
  Entry.new(key: "read.preview_bytes", yaml_path: %w[read preview_bytes], type: :integer, default: 12_288, expose: %i[env config cli]),
@@ -2,7 +2,8 @@
2
2
  // (text, or images: files in Finder, a picture in Preview), the hotkey with
3
3
  // the clipboard (a screenshot too). No Dock icon (LSUIElement); it stays
4
4
  // running after the first launch. `ChiHelper --login on|off|status` (run
5
- // directly by chi desktop) manages the login item and exits.
5
+ // directly by chi desktop) manages the login item and exits;
6
+ // `ChiHelper --kitty list|send` is the kitty targets' seam (Kitty.swift).
6
7
  import AppKit
7
8
  import ServiceManagement
8
9
 
@@ -97,6 +98,9 @@ struct ChiHelperMain {
97
98
  if args.count >= 2, args[1] == "--login" {
98
99
  exit(loginCommand(args.count >= 3 ? args[2] : ""))
99
100
  }
101
+ if args.count >= 2, args[1] == "--kitty" {
102
+ exit(kittyCommand(Array(args.dropFirst(2))))
103
+ }
100
104
  let app = NSApplication.shared
101
105
  let delegate = AppDelegate()
102
106
  app.delegate = delegate
@@ -7,13 +7,8 @@ struct LaunchConfig: Decodable {
7
7
  let version: String?
8
8
  let argv: [String]
9
9
  let env: [String: String]?
10
- }
11
-
12
- struct ChiResult {
13
- let status: Int32
14
- let stdout: String
15
- let stderr: String
16
- let timedOut: Bool
10
+ /// Agents in kitty windows (Kitty.swift); nil = none.
11
+ let kitty: KittySettings?
17
12
  }
18
13
 
19
14
  enum ChiError: Error, CustomStringConvertible {
@@ -106,9 +101,8 @@ final class ChiRunner {
106
101
  return config
107
102
  }
108
103
 
109
- /// Runs `chi <args>`, with stdin written from a background queue and
110
- /// stdout/stderr drained as they come (no pipe deadlock near the 16 KiB
111
- /// note cap). Calls back on the main queue.
104
+ /// Runs `chi <args>` through ProcessRunner (no pipe deadlock near the
105
+ /// 16 KiB note cap). Calls back on the main queue.
112
106
  /// @param timeout this call's limit instead of the runner's
113
107
  func run(_ args: [String], stdin: Data? = nil, timeout: TimeInterval? = nil,
114
108
  completion: @escaping (Result<ChiResult, ChiError>) -> Void) {
@@ -119,59 +113,9 @@ final class ChiRunner {
119
113
  completion(.failure(.failed("\(error)"))); return
120
114
  }
121
115
 
122
- let process = Process()
123
- process.executableURL = URL(fileURLWithPath: config.argv[0])
124
- process.arguments = Array(config.argv.dropFirst()) + args
125
- process.environment = ProcessInfo.processInfo.environment.merging(config.env ?? [:]) { _, baked in baked }
126
- process.currentDirectoryURL = URL(fileURLWithPath: NSHomeDirectory())
127
- let out = Pipe(), err = Pipe(), input = Pipe()
128
- process.standardOutput = out
129
- process.standardError = err
130
- process.standardInput = stdin == nil ? FileHandle.nullDevice : input
131
-
132
- let lock = NSLock()
133
- var outData = Data(), errData = Data()
134
- out.fileHandleForReading.readabilityHandler = { handle in
135
- let chunk = handle.availableData
136
- lock.lock(); outData.append(chunk); lock.unlock()
137
- }
138
- err.fileHandleForReading.readabilityHandler = { handle in
139
- let chunk = handle.availableData
140
- lock.lock(); errData.append(chunk); lock.unlock()
141
- }
142
-
143
- var timedOut = false
144
- process.terminationHandler = { proc in
145
- out.fileHandleForReading.readabilityHandler = nil
146
- err.fileHandleForReading.readabilityHandler = nil
147
- let restOut = out.fileHandleForReading.readDataToEndOfFile()
148
- let restErr = err.fileHandleForReading.readDataToEndOfFile()
149
- lock.lock()
150
- outData.append(restOut); errData.append(restErr)
151
- let result = ChiResult(status: proc.terminationStatus,
152
- stdout: String(decoding: outData, as: UTF8.self),
153
- stderr: String(decoding: errData, as: UTF8.self),
154
- timedOut: timedOut)
155
- lock.unlock()
156
- DispatchQueue.main.async { completion(.success(result)) }
157
- }
158
-
159
- do { try process.run() } catch {
160
- completion(.failure(.failed("Could not start \(config.argv[0]): \(error.localizedDescription)")))
161
- return
162
- }
163
- if let stdin {
164
- DispatchQueue.global().async {
165
- try? input.fileHandleForWriting.write(contentsOf: stdin)
166
- try? input.fileHandleForWriting.close()
167
- }
168
- }
169
- DispatchQueue.global().asyncAfter(deadline: .now() + (timeout ?? self.timeout)) {
170
- if process.isRunning {
171
- lock.lock(); timedOut = true; lock.unlock()
172
- process.terminate()
173
- }
174
- }
116
+ ProcessRunner.run(executable: config.argv[0], args: Array(config.argv.dropFirst()) + args,
117
+ env: ProcessInfo.processInfo.environment.merging(config.env ?? [:]) { _, baked in baked },
118
+ stdin: stdin, timeout: timeout ?? self.timeout, completion: completion)
175
119
  }
176
120
 
177
121
  /// The live sessions, then up to +recentCount+ stopped ones, newest
@@ -27,7 +27,11 @@ struct PanelImage: Identifiable, Equatable {
27
27
  }
28
28
 
29
29
  enum ImageIntake {
30
- static let tempDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("chi-helper", isDirectory: true)
30
+ /// $TMPDIR when set (launchd sets it; a spec points it elsewhere), else
31
+ /// the user's temp folder.
32
+ static let tempRoot = URL(fileURLWithPath: ProcessInfo.processInfo.environment["TMPDIR"].flatMap { $0.isEmpty ? nil : $0 }
33
+ ?? NSTemporaryDirectory(), isDirectory: true)
34
+ static let tempDir = tempRoot.appendingPathComponent("chi-helper", isDirectory: true)
31
35
 
32
36
  /// A pasteboard's content for the panel, in this order: image files
33
37
  /// (Finder ⌘C, a Service on files) with no text; else its text, as before
@@ -78,9 +82,40 @@ enum ImageIntake {
78
82
  }
79
83
  }
80
84
 
81
- /// At launch: what a helper that quit mid-send left.
82
- static func sweep() {
85
+ /// Images pasted into kitty windows, kept for the agent to read after
86
+ /// the send: never deleted by it, swept at launch after a week.
87
+ static let sentDir = tempRoot.appendingPathComponent("chi-helper-sent", isDirectory: true)
88
+ static let sentMaxAge: TimeInterval = 7 * 86_400
89
+
90
+ /// The paths a kitty paste names: an image the helper wrote is copied to
91
+ /// sentDir first (the send deletes the original), with no spaces in its
92
+ /// name; a file of the user's is named as it is. Call before any send
93
+ /// job starts. An image that can't be copied is left out.
94
+ static func keepForPaste(_ images: [PanelImage]) -> [String] {
95
+ images.compactMap { image in
96
+ guard image.temp else { return image.url.path }
97
+ let ext = image.url.pathExtension.isEmpty ? "png" : image.url.pathExtension
98
+ let copy = sentDir.appendingPathComponent("\(UUID().uuidString).\(ext)")
99
+ do {
100
+ try FileManager.default.createDirectory(at: sentDir, withIntermediateDirectories: true)
101
+ try FileManager.default.copyItem(at: image.url, to: copy)
102
+ return copy.path
103
+ } catch {
104
+ return nil
105
+ }
106
+ }
107
+ }
108
+
109
+ /// At launch: what a helper that quit mid-send left, and pasted images
110
+ /// older than a week.
111
+ static func sweep(now: Date = Date()) {
83
112
  try? FileManager.default.removeItem(at: tempDir)
113
+ let fm = FileManager.default
114
+ for name in (try? fm.contentsOfDirectory(atPath: sentDir.path)) ?? [] {
115
+ let url = sentDir.appendingPathComponent(name)
116
+ let modified = (try? fm.attributesOfItem(atPath: url.path)[.modificationDate] as? Date) ?? now
117
+ if now.timeIntervalSince(modified) > sentMaxAge { try? fm.removeItem(at: url) }
118
+ }
84
119
  }
85
120
  }
86
121