instar 0.28.47 → 0.28.49

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 (44) hide show
  1. package/README.md +3 -0
  2. package/dist/cli.js +0 -0
  3. package/dist/commands/init.js +19 -19
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/commands/server.js +1 -1
  6. package/dist/commands/server.js.map +1 -1
  7. package/dist/core/Config.d.ts.map +1 -1
  8. package/dist/core/Config.js +3 -0
  9. package/dist/core/Config.js.map +1 -1
  10. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  11. package/dist/core/PostUpdateMigrator.js +15 -7
  12. package/dist/core/PostUpdateMigrator.js.map +1 -1
  13. package/dist/core/SessionManager.d.ts.map +1 -1
  14. package/dist/core/SessionManager.js +14 -2
  15. package/dist/core/SessionManager.js.map +1 -1
  16. package/dist/core/types.d.ts +6 -0
  17. package/dist/core/types.d.ts.map +1 -1
  18. package/dist/core/types.js.map +1 -1
  19. package/dist/memory/TopicMemory.d.ts +18 -3
  20. package/dist/memory/TopicMemory.d.ts.map +1 -1
  21. package/dist/memory/TopicMemory.js +71 -11
  22. package/dist/memory/TopicMemory.js.map +1 -1
  23. package/dist/messaging/imessage/IMessageAdapter.d.ts +35 -0
  24. package/dist/messaging/imessage/IMessageAdapter.d.ts.map +1 -1
  25. package/dist/messaging/imessage/IMessageAdapter.js +121 -1
  26. package/dist/messaging/imessage/IMessageAdapter.js.map +1 -1
  27. package/dist/messaging/imessage/types.d.ts +33 -0
  28. package/dist/messaging/imessage/types.d.ts.map +1 -1
  29. package/dist/scheduler/JobScheduler.d.ts.map +1 -1
  30. package/dist/scheduler/JobScheduler.js +8 -0
  31. package/dist/scheduler/JobScheduler.js.map +1 -1
  32. package/dist/server/routes.d.ts.map +1 -1
  33. package/dist/server/routes.js +9 -3
  34. package/dist/server/routes.js.map +1 -1
  35. package/package.json +1 -1
  36. package/scripts/attachments-sync/go.mod +8 -0
  37. package/scripts/attachments-sync/go.sum +4 -0
  38. package/scripts/attachments-sync/main.go +253 -0
  39. package/scripts/setup-imessage-hardlink.sh +73 -0
  40. package/src/data/builtin-manifest.json +93 -93
  41. package/upgrades/0.28.49.md +52 -0
  42. package/upgrades/side-effects/0.28.48.md +90 -0
  43. package/upgrades/NEXT.md +0 -53
  44. /package/upgrades/side-effects/{0.28.47-reflection-trigger-signal.md → 0.28.47.md} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "0.28.47",
3
+ "version": "0.28.49",
4
4
  "description": "Persistent autonomy infrastructure for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,8 @@
1
+ module instar-attachments-sync
2
+
3
+ go 1.26.2
4
+
5
+ require (
6
+ github.com/fsnotify/fsnotify v1.9.0 // indirect
7
+ golang.org/x/sys v0.13.0 // indirect
8
+ )
@@ -0,0 +1,4 @@
1
+ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
2
+ github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
3
+ golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
4
+ golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -0,0 +1,253 @@
1
+ // instar-attachments-sync
2
+ //
3
+ // Purpose-built binary for mirroring iMessage photo attachments to a
4
+ // readable location for the Instar agent. This binary is granted Full
5
+ // Disk Access in macOS Privacy settings — nothing else needs it.
6
+ //
7
+ // What it does (and only what it does):
8
+ // 1. On startup: hardlink all existing image/video attachments from
9
+ // ~/Library/Messages/Attachments/ to DEST_DIR.
10
+ // 2. Continuously watch for new files via FSEvents and hardlink them.
11
+ // 3. Prune dead hardlinks (source deleted, link count == 1).
12
+ //
13
+ // Naming convention: {first8ofUUID}__{original-filename}
14
+ // This mirrors the bash script it replaces, so existing hardlinks remain valid.
15
+
16
+ package main
17
+
18
+ import (
19
+ "fmt"
20
+ "log"
21
+ "os"
22
+ "path/filepath"
23
+ "strings"
24
+ "syscall"
25
+ "time"
26
+
27
+ "github.com/fsnotify/fsnotify"
28
+ )
29
+
30
+ var (
31
+ homeDir = os.Getenv("HOME")
32
+ srcDir = filepath.Join(homeDir, "Library/Messages/Attachments")
33
+ destDir string
34
+ logFile string
35
+ )
36
+
37
+ // Supported extensions to mirror
38
+ var exts = map[string]bool{
39
+ ".jpeg": true, ".jpg": true, ".png": true, ".heic": true,
40
+ ".mov": true, ".mp4": true, ".pdf": true, ".gif": true,
41
+ ".caf": true, ".m4a": true, ".3gpp": true,
42
+ }
43
+
44
+ func main() {
45
+ // Resolve paths relative to this binary's location.
46
+ // Binary lives at <agentRoot>/.instar/bin/instar-attachments-sync
47
+ // So .instar dir is filepath.Dir(filepath.Dir(exe))
48
+ exe, err := os.Executable()
49
+ if err != nil {
50
+ log.Fatalf("cannot resolve executable path: %v", err)
51
+ }
52
+ dotInstar := filepath.Dir(filepath.Dir(exe)) // <agentRoot>/.instar
53
+ destDir = filepath.Join(dotInstar, "imessage/attachments")
54
+ logFile = filepath.Join(dotInstar, "logs/attachments-watcher.log")
55
+
56
+ // Override via env for testing
57
+ if v := os.Getenv("ATTACHMENTS_DEST"); v != "" {
58
+ destDir = v
59
+ }
60
+ if v := os.Getenv("ATTACHMENTS_LOG"); v != "" {
61
+ logFile = v
62
+ }
63
+
64
+ if err := os.MkdirAll(destDir, 0755); err != nil {
65
+ log.Fatalf("cannot create dest dir: %v", err)
66
+ }
67
+ if err := os.MkdirAll(filepath.Dir(logFile), 0755); err != nil {
68
+ log.Fatalf("cannot create log dir: %v", err)
69
+ }
70
+
71
+ lf, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
72
+ if err != nil {
73
+ log.Fatalf("cannot open log: %v", err)
74
+ }
75
+ log.SetOutput(lf)
76
+ log.SetFlags(0) // we write our own timestamps
77
+
78
+ logMsg("instar-attachments-sync starting")
79
+ logMsg("src=%s dest=%s", srcDir, destDir)
80
+
81
+ // Initial sync
82
+ n, err := syncOnce()
83
+ if err != nil {
84
+ logMsg("initial sync error: %v", err)
85
+ } else {
86
+ logMsg("initial sync: linked %d new files", n)
87
+ }
88
+
89
+ // Watch for new files
90
+ if err := watch(); err != nil {
91
+ logMsg("watcher error: %v", err)
92
+ os.Exit(1)
93
+ }
94
+ }
95
+
96
+ // syncOnce walks srcDir and hardlinks any new supported files to destDir.
97
+ // Returns count of newly linked files.
98
+ func syncOnce() (int, error) {
99
+ count := 0
100
+ err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
101
+ if err != nil {
102
+ // Skip dirs/files we can't read — log once at top level
103
+ if path == srcDir {
104
+ return fmt.Errorf("cannot read source dir %s: %w", srcDir, err)
105
+ }
106
+ return nil
107
+ }
108
+ if info.IsDir() || strings.HasPrefix(info.Name(), ".") {
109
+ return nil
110
+ }
111
+ ext := strings.ToLower(filepath.Ext(info.Name()))
112
+ if !exts[ext] {
113
+ return nil
114
+ }
115
+ if linked, err := linkFile(path); err != nil {
116
+ logMsg("link error %s: %v", path, err)
117
+ } else if linked {
118
+ count++
119
+ }
120
+ return nil
121
+ })
122
+ // Prune dead hardlinks
123
+ pruneDeadLinks()
124
+ return count, err
125
+ }
126
+
127
+ // linkFile creates a hardlink in destDir for the given source file.
128
+ // Returns true if a new link was created, false if it already existed.
129
+ func linkFile(src string) (bool, error) {
130
+ base := filepath.Base(src)
131
+ // Parent dir is the UUID directory
132
+ uuid := filepath.Base(filepath.Dir(src))
133
+ prefix := uuid
134
+ if len(prefix) > 8 {
135
+ prefix = prefix[:8]
136
+ }
137
+ destName := prefix + "__" + base
138
+ dest := filepath.Join(destDir, destName)
139
+
140
+ // Check if already linked (same inode)
141
+ if isSameInode(src, dest) {
142
+ return false, nil
143
+ }
144
+
145
+ // Remove stale dest if present
146
+ if _, err := os.Lstat(dest); err == nil {
147
+ os.Remove(dest)
148
+ }
149
+
150
+ if err := os.Link(src, dest); err != nil {
151
+ return false, err
152
+ }
153
+ return true, nil
154
+ }
155
+
156
+ // isSameInode returns true if both paths exist and share an inode.
157
+ func isSameInode(a, b string) bool {
158
+ sa, err := os.Stat(a)
159
+ if err != nil {
160
+ return false
161
+ }
162
+ sb, err := os.Stat(b)
163
+ if err != nil {
164
+ return false
165
+ }
166
+ sia, ok1 := sa.Sys().(*syscall.Stat_t)
167
+ sib, ok2 := sb.Sys().(*syscall.Stat_t)
168
+ return ok1 && ok2 && sia.Ino == sib.Ino
169
+ }
170
+
171
+ // pruneDeadLinks removes hardlinks whose source has been deleted (link count == 1).
172
+ func pruneDeadLinks() {
173
+ entries, err := os.ReadDir(destDir)
174
+ if err != nil {
175
+ return
176
+ }
177
+ for _, e := range entries {
178
+ if e.IsDir() {
179
+ continue
180
+ }
181
+ path := filepath.Join(destDir, e.Name())
182
+ info, err := os.Stat(path)
183
+ if err != nil {
184
+ continue
185
+ }
186
+ if st, ok := info.Sys().(*syscall.Stat_t); ok && st.Nlink == 1 {
187
+ os.Remove(path)
188
+ }
189
+ }
190
+ }
191
+
192
+ // watch uses fsnotify to watch srcDir and re-runs syncOnce on changes.
193
+ func watch() error {
194
+ watcher, err := fsnotify.NewWatcher()
195
+ if err != nil {
196
+ return fmt.Errorf("fsnotify: %w", err)
197
+ }
198
+ defer watcher.Close()
199
+
200
+ // Watch top-level src dir; we'll add subdirs as they appear
201
+ if err := watcher.Add(srcDir); err != nil {
202
+ return fmt.Errorf("watch %s: %w", srcDir, err)
203
+ }
204
+
205
+ // Also watch existing subdirs (Messages uses 2-level nesting)
206
+ _ = filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
207
+ if err == nil && info.IsDir() {
208
+ watcher.Add(path) //nolint
209
+ }
210
+ return nil
211
+ })
212
+
213
+ logMsg("watching %s", srcDir)
214
+
215
+ debounce := time.NewTimer(0)
216
+ <-debounce.C // drain initial fire
217
+
218
+ for {
219
+ select {
220
+ case event, ok := <-watcher.Events:
221
+ if !ok {
222
+ return fmt.Errorf("watcher channel closed")
223
+ }
224
+ // Watch new subdirectories as they're created
225
+ if event.Has(fsnotify.Create) {
226
+ if info, err := os.Stat(event.Name); err == nil && info.IsDir() {
227
+ watcher.Add(event.Name) //nolint
228
+ }
229
+ }
230
+ // Debounce: wait 500ms after last event before syncing
231
+ debounce.Reset(500 * time.Millisecond)
232
+
233
+ case err, ok := <-watcher.Errors:
234
+ if !ok {
235
+ return fmt.Errorf("watcher error channel closed")
236
+ }
237
+ logMsg("watcher error: %v", err)
238
+
239
+ case <-debounce.C:
240
+ n, err := syncOnce()
241
+ if err != nil {
242
+ logMsg("sync error: %v", err)
243
+ } else if n > 0 {
244
+ logMsg("linked %d new files", n)
245
+ }
246
+ }
247
+ }
248
+ }
249
+
250
+ func logMsg(format string, args ...any) {
251
+ msg := fmt.Sprintf(format, args...)
252
+ log.Printf("%s %s", time.Now().UTC().Format("2006-01-02T15:04:05Z"), msg)
253
+ }
@@ -0,0 +1,73 @@
1
+ #!/bin/bash
2
+ # setup-imessage-hardlink.sh
3
+ #
4
+ # Creates hardlinks from ~/Library/Messages/chat.db files to an agent's
5
+ # .instar/imessage/ directory. Lets the iMessage adapter read messages
6
+ # without requiring Full Disk Access on the node binary.
7
+ #
8
+ # Must be run from a user session that HAS Full Disk Access (e.g., Terminal).
9
+ # After the hardlinks exist, no FDA is needed by the reading process.
10
+ #
11
+ # Usage:
12
+ # ./scripts/setup-imessage-hardlink.sh [agent-dir]
13
+ #
14
+ # If agent-dir is omitted, uses the current directory (must be an agent root).
15
+
16
+ set -e
17
+
18
+ AGENT_DIR="${1:-$(pwd)}"
19
+
20
+ if [ ! -d "$AGENT_DIR/.instar" ]; then
21
+ echo "Error: $AGENT_DIR is not an Instar agent directory (no .instar/ found)" >&2
22
+ echo "Usage: $0 [agent-dir]" >&2
23
+ exit 1
24
+ fi
25
+
26
+ MESSAGES_DIR="$HOME/Library/Messages"
27
+ TARGET_DIR="$AGENT_DIR/.instar/imessage"
28
+
29
+ if [ ! -f "$MESSAGES_DIR/chat.db" ]; then
30
+ echo "Error: $MESSAGES_DIR/chat.db not found — is Messages.app signed in?" >&2
31
+ exit 1
32
+ fi
33
+
34
+ # Test FDA by trying to read chat.db
35
+ if ! sqlite3 "$MESSAGES_DIR/chat.db" "SELECT 1" &>/dev/null; then
36
+ echo "Error: Cannot read $MESSAGES_DIR/chat.db" >&2
37
+ echo "Grant Full Disk Access to your terminal: System Settings → Privacy & Security → Full Disk Access" >&2
38
+ exit 1
39
+ fi
40
+
41
+ mkdir -p "$TARGET_DIR"
42
+
43
+ echo "Creating hardlinks in $TARGET_DIR..."
44
+ for f in chat.db chat.db-wal chat.db-shm; do
45
+ if [ -f "$MESSAGES_DIR/$f" ]; then
46
+ # Remove existing link/file if present
47
+ [ -e "$TARGET_DIR/$f" ] && rm "$TARGET_DIR/$f"
48
+ ln "$MESSAGES_DIR/$f" "$TARGET_DIR/$f"
49
+ echo " ✓ $f"
50
+ fi
51
+ done
52
+
53
+ # Verify same inode (hardlink worked)
54
+ ORIG_INODE=$(stat -f '%i' "$MESSAGES_DIR/chat.db")
55
+ LINK_INODE=$(stat -f '%i' "$TARGET_DIR/chat.db")
56
+ if [ "$ORIG_INODE" != "$LINK_INODE" ]; then
57
+ echo "Error: hardlink verification failed (different inodes)" >&2
58
+ exit 1
59
+ fi
60
+
61
+ echo ""
62
+ echo "Done. Configure the iMessage adapter in your config.json:"
63
+ echo ""
64
+ echo ' "messaging": [{'
65
+ echo ' "type": "imessage",'
66
+ echo ' "enabled": true,'
67
+ echo ' "config": {'
68
+ echo " \"dbPath\": \"$TARGET_DIR/chat.db\","
69
+ echo ' "authorizedContacts": ["+1..."]'
70
+ echo ' }'
71
+ echo ' }]'
72
+ echo ""
73
+ echo "After this, the iMessage adapter can read chat.db without Full Disk Access."