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.
- package/README.md +3 -0
- package/dist/cli.js +0 -0
- package/dist/commands/init.js +19 -19
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/server.js +1 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/core/Config.d.ts.map +1 -1
- package/dist/core/Config.js +3 -0
- package/dist/core/Config.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +15 -7
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +14 -2
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/types.d.ts +6 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/memory/TopicMemory.d.ts +18 -3
- package/dist/memory/TopicMemory.d.ts.map +1 -1
- package/dist/memory/TopicMemory.js +71 -11
- package/dist/memory/TopicMemory.js.map +1 -1
- package/dist/messaging/imessage/IMessageAdapter.d.ts +35 -0
- package/dist/messaging/imessage/IMessageAdapter.d.ts.map +1 -1
- package/dist/messaging/imessage/IMessageAdapter.js +121 -1
- package/dist/messaging/imessage/IMessageAdapter.js.map +1 -1
- package/dist/messaging/imessage/types.d.ts +33 -0
- package/dist/messaging/imessage/types.d.ts.map +1 -1
- package/dist/scheduler/JobScheduler.d.ts.map +1 -1
- package/dist/scheduler/JobScheduler.js +8 -0
- package/dist/scheduler/JobScheduler.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +9 -3
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/attachments-sync/go.mod +8 -0
- package/scripts/attachments-sync/go.sum +4 -0
- package/scripts/attachments-sync/main.go +253 -0
- package/scripts/setup-imessage-hardlink.sh +73 -0
- package/src/data/builtin-manifest.json +93 -93
- package/upgrades/0.28.49.md +52 -0
- package/upgrades/side-effects/0.28.48.md +90 -0
- package/upgrades/NEXT.md +0 -53
- /package/upgrades/side-effects/{0.28.47-reflection-trigger-signal.md → 0.28.47.md} +0 -0
package/package.json
CHANGED
|
@@ -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."
|