@getrift/rift 0.1.0-beta.12 → 0.1.0-beta.14

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 (89) hide show
  1. package/README.md +35 -9
  2. package/dist/src/cli/commands/doctor.d.ts +6 -0
  3. package/dist/src/cli/commands/doctor.d.ts.map +1 -0
  4. package/dist/src/cli/commands/doctor.js +183 -0
  5. package/dist/src/cli/commands/doctor.js.map +1 -0
  6. package/dist/src/cli/commands/menubar.d.ts +30 -0
  7. package/dist/src/cli/commands/menubar.d.ts.map +1 -0
  8. package/dist/src/cli/commands/menubar.js +180 -0
  9. package/dist/src/cli/commands/menubar.js.map +1 -0
  10. package/dist/src/cli/commands/onboard.d.ts +38 -0
  11. package/dist/src/cli/commands/onboard.d.ts.map +1 -1
  12. package/dist/src/cli/commands/onboard.js +203 -121
  13. package/dist/src/cli/commands/onboard.js.map +1 -1
  14. package/dist/src/cli/commands/status.d.ts +9 -7
  15. package/dist/src/cli/commands/status.d.ts.map +1 -1
  16. package/dist/src/cli/commands/status.js +29 -10
  17. package/dist/src/cli/commands/status.js.map +1 -1
  18. package/dist/src/cli/commands/update.d.ts +3 -0
  19. package/dist/src/cli/commands/update.d.ts.map +1 -1
  20. package/dist/src/cli/commands/update.js +19 -0
  21. package/dist/src/cli/commands/update.js.map +1 -1
  22. package/dist/src/cli/index.d.ts.map +1 -1
  23. package/dist/src/cli/index.js +4 -0
  24. package/dist/src/cli/index.js.map +1 -1
  25. package/dist/src/cli/postinstall-menubar.d.ts +22 -0
  26. package/dist/src/cli/postinstall-menubar.d.ts.map +1 -0
  27. package/dist/src/cli/postinstall-menubar.js +39 -0
  28. package/dist/src/cli/postinstall-menubar.js.map +1 -0
  29. package/dist/src/cli/status/friend-header.d.ts +8 -1
  30. package/dist/src/cli/status/friend-header.d.ts.map +1 -1
  31. package/dist/src/cli/status/friend-header.js +93 -12
  32. package/dist/src/cli/status/friend-header.js.map +1 -1
  33. package/dist/src/cli/ui.d.ts +47 -0
  34. package/dist/src/cli/ui.d.ts.map +1 -0
  35. package/dist/src/cli/ui.js +166 -0
  36. package/dist/src/cli/ui.js.map +1 -0
  37. package/dist/src/diagnostics/doctor.d.ts +106 -0
  38. package/dist/src/diagnostics/doctor.d.ts.map +1 -0
  39. package/dist/src/diagnostics/doctor.js +251 -0
  40. package/dist/src/diagnostics/doctor.js.map +1 -0
  41. package/dist/src/diagnostics/notify.d.ts +90 -0
  42. package/dist/src/diagnostics/notify.d.ts.map +1 -0
  43. package/dist/src/diagnostics/notify.js +177 -0
  44. package/dist/src/diagnostics/notify.js.map +1 -0
  45. package/dist/src/diagnostics/repair-prompt.d.ts +49 -0
  46. package/dist/src/diagnostics/repair-prompt.d.ts.map +1 -0
  47. package/dist/src/diagnostics/repair-prompt.js +198 -0
  48. package/dist/src/diagnostics/repair-prompt.js.map +1 -0
  49. package/dist/src/jobs/handlers/dedupe-conversations.d.ts +25 -2
  50. package/dist/src/jobs/handlers/dedupe-conversations.d.ts.map +1 -1
  51. package/dist/src/jobs/handlers/dedupe-conversations.js +48 -9
  52. package/dist/src/jobs/handlers/dedupe-conversations.js.map +1 -1
  53. package/dist/src/jobs/handlers/ingest.d.ts.map +1 -1
  54. package/dist/src/jobs/handlers/ingest.js +8 -2
  55. package/dist/src/jobs/handlers/ingest.js.map +1 -1
  56. package/dist/src/main.js +43 -4
  57. package/dist/src/main.js.map +1 -1
  58. package/dist/src/mcp/server.d.ts.map +1 -1
  59. package/dist/src/mcp/server.js +43 -3
  60. package/dist/src/mcp/server.js.map +1 -1
  61. package/dist/src/mcp/tools/context-pack.js +163 -25
  62. package/dist/src/mcp/tools/context-pack.js.map +1 -1
  63. package/dist/src/observability/onboarding-metric.d.ts +115 -0
  64. package/dist/src/observability/onboarding-metric.d.ts.map +1 -0
  65. package/dist/src/observability/onboarding-metric.js +344 -0
  66. package/dist/src/observability/onboarding-metric.js.map +1 -0
  67. package/dist/src/observability/version-check.d.ts +1 -0
  68. package/dist/src/observability/version-check.d.ts.map +1 -1
  69. package/dist/src/observability/version-check.js +2 -1
  70. package/dist/src/observability/version-check.js.map +1 -1
  71. package/dist/src/retrieval/context-pack.d.ts +37 -0
  72. package/dist/src/retrieval/context-pack.d.ts.map +1 -1
  73. package/dist/src/retrieval/context-pack.js +165 -1
  74. package/dist/src/retrieval/context-pack.js.map +1 -1
  75. package/dist/src/retrieval/current-truth.d.ts +326 -0
  76. package/dist/src/retrieval/current-truth.d.ts.map +1 -0
  77. package/dist/src/retrieval/current-truth.js +747 -0
  78. package/dist/src/retrieval/current-truth.js.map +1 -0
  79. package/dist/src/retrieval/git-state.d.ts +53 -0
  80. package/dist/src/retrieval/git-state.d.ts.map +1 -0
  81. package/dist/src/retrieval/git-state.js +174 -0
  82. package/dist/src/retrieval/git-state.js.map +1 -0
  83. package/dist/src/server/routes/friend-status.d.ts +63 -0
  84. package/dist/src/server/routes/friend-status.d.ts.map +1 -1
  85. package/dist/src/server/routes/friend-status.js +97 -0
  86. package/dist/src/server/routes/friend-status.js.map +1 -1
  87. package/operator/swiftbar/render-menu.py +444 -0
  88. package/operator/swiftbar/rift.10s.sh +147 -0
  89. package/package.json +4 -1
@@ -0,0 +1,444 @@
1
+ #!/usr/bin/env python3
2
+ """Render the friend-facing Rift SwiftBar menu.
3
+
4
+ Pure formatting: reads structured state from the environment and prints a
5
+ SwiftBar menu to stdout. No network, no clock except `now`, no CLI calls
6
+ of its own. The shell wrapper (`rift.10s.sh`) gathers the inputs and the
7
+ repair prompt always comes from `rift doctor` — this script never invents
8
+ diagnosis or hardcodes a prompt.
9
+
10
+ The menu answers one question by default: "Is Rift working?" Operator
11
+ detail (MCP counters, launchd labels, Node versions, commits) is
12
+ deliberately absent.
13
+
14
+ Inputs (all via env, all optional — missing inputs degrade gracefully):
15
+ RIFT_DOCTOR_JSON `rift doctor --json` output (the verdict + one fix).
16
+ RIFT_STATUS_JSON `rift status --json` output (capture times, clients).
17
+ RIFT_MCP_JSON `/stats/mcp-usage` body (memory-today delight metric).
18
+ RIFT_HEALTH "up" | "down" — coarse /health probe, used only when
19
+ the doctor report is unavailable (CLI not resolvable).
20
+ RIFT_NODE_BIN absolute node binary, for menu actions.
21
+ RIFT_JS absolute path to the rift JS entrypoint.
22
+ RIFT_BIN absolute rift launcher, fallback when RIFT_JS is unset.
23
+ RIFT_LOG_DIR logs folder, for "Open logs".
24
+ RIFT_DATA_DIR data folder, for "Open data folder".
25
+ RIFT_ABOUT_URL About Rift URL (default https://getrift.dev/about).
26
+ RIFT_NOW_MS injected clock for tests (epoch ms); defaults to now.
27
+ """
28
+ import json
29
+ import os
30
+ import sys
31
+
32
+ CLIENT_LABELS = {
33
+ "claude-desktop": "Claude Desktop",
34
+ "claude-code": "Claude Code",
35
+ "cursor": "Cursor",
36
+ "codex": "Codex",
37
+ }
38
+
39
+ ABOUT_DEFAULT = "https://getrift.dev/about"
40
+
41
+
42
+ def env(key, default=""):
43
+ return os.environ.get(key, default)
44
+
45
+
46
+ def parse_json(raw):
47
+ raw = (raw or "").strip()
48
+ if not raw:
49
+ return None
50
+ try:
51
+ return json.loads(raw)
52
+ except Exception:
53
+ return None
54
+
55
+
56
+ def now_ms():
57
+ raw = env("RIFT_NOW_MS")
58
+ if raw:
59
+ try:
60
+ return int(raw)
61
+ except ValueError:
62
+ pass
63
+ import time
64
+
65
+ return int(time.time() * 1000)
66
+
67
+
68
+ def parse_iso_ms(iso):
69
+ """Parse an ISO-8601 timestamp to epoch ms, or None."""
70
+ if not iso:
71
+ return None
72
+ s = iso.strip()
73
+ if s.endswith("Z"):
74
+ s = s[:-1] + "+00:00"
75
+ try:
76
+ from datetime import datetime
77
+
78
+ return int(datetime.fromisoformat(s).timestamp() * 1000)
79
+ except Exception:
80
+ return None
81
+
82
+
83
+ def fmt_duration(seconds):
84
+ seconds = int(max(0, seconds))
85
+ if seconds < 60:
86
+ return f"{seconds}s"
87
+ minutes = seconds // 60
88
+ if minutes < 60:
89
+ return f"{minutes}m"
90
+ hours = minutes // 60
91
+ rem_min = minutes % 60
92
+ if hours < 24:
93
+ return f"{hours}h {rem_min}m" if rem_min else f"{hours}h"
94
+ days = hours // 24
95
+ rem_hours = hours % 24
96
+ return f"{days}d {rem_hours}h" if rem_hours else f"{days}d"
97
+
98
+
99
+ def age_ago(iso, now):
100
+ ms = parse_iso_ms(iso)
101
+ if ms is None:
102
+ return None
103
+ return fmt_duration((now - ms) / 1000) + " ago"
104
+
105
+
106
+ def time_until(iso, now):
107
+ ms = parse_iso_ms(iso)
108
+ if ms is None:
109
+ return None
110
+ delta = (ms - now) / 1000
111
+ if delta <= 0:
112
+ return "due now"
113
+ return "in " + fmt_duration(delta)
114
+
115
+
116
+ def fmt_tokens(n):
117
+ if n >= 1_000_000:
118
+ return "~{:.1f}M context tokens".format(n / 1_000_000).replace(".0M", "M")
119
+ if n >= 1_000:
120
+ return "~{}k context tokens".format(round(n / 1_000))
121
+ return f"~{n} context tokens"
122
+
123
+
124
+ def memory_today(mcp):
125
+ """Friendly 'memory today' row, or None when not available."""
126
+ if not isinstance(mcp, dict):
127
+ return None
128
+ today = mcp.get("today")
129
+ if not isinstance(today, dict):
130
+ return None
131
+ parts = []
132
+ hits = today.get("context_hits")
133
+ if isinstance(hits, int):
134
+ parts.append(f"{hits} useful recall" + ("" if hits == 1 else "s"))
135
+ tokens = today.get("context_tokens_delivered_estimate")
136
+ if isinstance(tokens, int):
137
+ parts.append(fmt_tokens(tokens))
138
+ return " · ".join(parts) if parts else None
139
+
140
+
141
+ # --- Menu-action line builders -------------------------------------------
142
+ #
143
+ # SwiftBar runs an item's `shell=` command with a stripped PATH, so every
144
+ # action uses absolute binaries. rift subcommands run through node + the
145
+ # resolved JS entrypoint (the npm `rift` symlink's `#!/usr/bin/env node`
146
+ # shebang can't resolve `node` under a stripped PATH); when only RIFT_BIN
147
+ # resolved we fall back to it directly.
148
+
149
+
150
+ def sb_param(value):
151
+ """Quote a SwiftBar parameter value so paths with spaces or SwiftBar
152
+ separators survive parsing.
153
+
154
+ SwiftBar splits an item's parameters on whitespace and uses `|` to
155
+ separate the title from its params, so any value carrying a space, tab,
156
+ `|`, or a quote must be wrapped to stay one token. We only quote when
157
+ needed, leaving simple values (and flag args like `--target=claude`)
158
+ untouched.
159
+ """
160
+ s = str(value)
161
+ if s == "" or any(ch in s for ch in " \t|\"'"):
162
+ return '"' + s.replace('"', '\\"') + '"'
163
+ return s
164
+
165
+
166
+ def rift_runnable():
167
+ node = env("RIFT_NODE_BIN")
168
+ js = env("RIFT_JS")
169
+ if node and js:
170
+ return [node, js]
171
+ rift = env("RIFT_BIN")
172
+ if rift:
173
+ return [rift]
174
+ return None
175
+
176
+
177
+ def rift_action(label, args, terminal, refresh=False):
178
+ """A menu line that runs a rift subcommand, or None if rift is unresolved."""
179
+ base = rift_runnable()
180
+ if base is None:
181
+ return None
182
+ parts = base + args
183
+ line = f"{label} | shell={sb_param(parts[0])}"
184
+ for i, p in enumerate(parts[1:], start=1):
185
+ line += f" param{i}={sb_param(p)}"
186
+ line += f" terminal={'true' if terminal else 'false'}"
187
+ if refresh:
188
+ line += " refresh=true"
189
+ return line
190
+
191
+
192
+ def open_action(label, path):
193
+ if not path:
194
+ return None
195
+ return f"{label} | shell=/usr/bin/open param1={sb_param(path)} terminal=false"
196
+
197
+
198
+ def about_line():
199
+ return f"About Rift | href={sb_param(env('RIFT_ABOUT_URL', ABOUT_DEFAULT))}"
200
+
201
+
202
+ # --- Verdict --------------------------------------------------------------
203
+
204
+
205
+ def verdict_of(doctor, health):
206
+ """healthy | warning | broken | unknown."""
207
+ if isinstance(doctor, dict) and "healthy" in doctor:
208
+ if not doctor.get("healthy"):
209
+ return "broken"
210
+ issues = doctor.get("issues") or []
211
+ if any(i.get("severity") == "warning" for i in issues):
212
+ return "warning"
213
+ return "healthy"
214
+ # No diagnosis available — fall back to the coarse health probe.
215
+ if health == "up":
216
+ return "healthy"
217
+ if health == "down":
218
+ return "broken"
219
+ return "unknown"
220
+
221
+
222
+ def has_doctor_report(doctor):
223
+ """True only when RIFT_DOCTOR_JSON parsed into a real doctor report.
224
+
225
+ Doctor-powered menu actions (`Copy fix prompt …`, `Troubleshooting`) are
226
+ gated on this, never on the rift binary merely resolving: if `rift doctor
227
+ --json` emitted nothing or failed, re-running doctor from the menu would
228
+ fail the same way, so we hide those actions and show friend-safe text.
229
+ """
230
+ return isinstance(doctor, dict) and "healthy" in doctor
231
+
232
+
233
+ TITLES = {
234
+ "healthy": "Rift | sfimage=circle.fill sfcolor=systemGreen",
235
+ "warning": "Rift | sfimage=exclamationmark.circle.fill sfcolor=systemYellow",
236
+ "broken": "Rift | sfimage=exclamationmark.triangle.fill sfcolor=systemRed",
237
+ "unknown": "Rift | sfimage=questionmark.circle.fill sfcolor=systemGray",
238
+ }
239
+
240
+
241
+ # --- Section renderers ----------------------------------------------------
242
+
243
+
244
+ def render_capture_rows(friend, now):
245
+ rows = []
246
+ capture = (friend or {}).get("capture") if isinstance(friend, dict) else None
247
+ if not isinstance(capture, dict):
248
+ return rows
249
+ last = age_ago(capture.get("last_run_at"), now)
250
+ rows.append(f"Last capture: {last}" if last else "Last capture: none yet")
251
+ nxt = time_until(capture.get("next_run_at"), now)
252
+ if nxt:
253
+ rows.append(f"Next capture: {nxt}")
254
+ return rows
255
+
256
+
257
+ def render_client_rows(status, doctor):
258
+ """Connected-client rows + connect actions for missing ones."""
259
+ rows = []
260
+ clients = status.get("mcp_clients") if isinstance(status, dict) else None
261
+ if not isinstance(clients, list):
262
+ return rows
263
+ connected = [c for c in clients if c.get("installed")]
264
+ missing = [c for c in clients if not c.get("installed")]
265
+ for c in connected:
266
+ label = CLIENT_LABELS.get(c.get("client"), c.get("client"))
267
+ rows.append(f"{label} connected | sfimage=checkmark.circle sfcolor=systemGreen")
268
+ if not connected:
269
+ rows.append("No AI tools connected yet")
270
+ for c in missing:
271
+ cid = c.get("client")
272
+ label = CLIENT_LABELS.get(cid, cid)
273
+ action = rift_action(
274
+ f"Connect {label}", ["mcp", "install", f"--client={cid}"], terminal=True
275
+ )
276
+ rows.append(action if action else f"{label} not connected")
277
+ return rows
278
+
279
+
280
+ def has_voyage_issue(doctor):
281
+ if not isinstance(doctor, dict):
282
+ return False
283
+ return any(
284
+ i.get("kind") in ("voyage_key_missing", "voyage_embed_errors", "index_write_errors")
285
+ for i in (doctor.get("issues") or [])
286
+ )
287
+
288
+
289
+ def render_advisories(doctor):
290
+ """Warning-level issues that aren't already shown as client rows."""
291
+ rows = []
292
+ if not isinstance(doctor, dict):
293
+ return rows
294
+ for issue in doctor.get("issues") or []:
295
+ if issue.get("severity") != "warning":
296
+ continue
297
+ if issue.get("kind") == "mcp_not_installed":
298
+ continue # already surfaced as a "Connect …" client row
299
+ rows.append(f"{issue.get('title', 'Heads up')}")
300
+ action = issue.get("nextAction")
301
+ if action:
302
+ rows.append(f"--{action}")
303
+ if issue.get("kind") == "update_available":
304
+ menu_action = rift_action("Update Rift", ["update"], terminal=True, refresh=True)
305
+ if menu_action:
306
+ rows.append(menu_action)
307
+ return rows
308
+
309
+
310
+ def render_footer_actions(broken, doctor_ok):
311
+ """Common Rift-owned actions. Order differs by state per the plan.
312
+
313
+ `doctor_ok` gates the doctor-powered actions (`Copy fix prompt …` and
314
+ `Troubleshooting`): they appear only when the CLI actually produced a
315
+ report, never merely because the rift binary resolved.
316
+ """
317
+ lines = ["---"]
318
+ if broken:
319
+ if doctor_ok:
320
+ for label, target in (("Claude", "claude"), ("Codex", "codex")):
321
+ action = rift_action(
322
+ f"Copy fix prompt for {label}",
323
+ ["doctor", "--copy-prompt", f"--target={target}"],
324
+ terminal=False,
325
+ )
326
+ if action:
327
+ lines.append(action)
328
+ logs = open_action("Open logs", env("RIFT_LOG_DIR"))
329
+ if logs:
330
+ lines.append(logs)
331
+ else:
332
+ cap = rift_action("Capture now", ["capture"], terminal=False, refresh=True)
333
+ if cap:
334
+ lines.append(cap)
335
+ st = rift_action("Open status", ["status"], terminal=True)
336
+ if st:
337
+ lines.append(st)
338
+ data = open_action("Open data folder", env("RIFT_DATA_DIR"))
339
+ if data:
340
+ lines.append(data)
341
+ lines.append("---")
342
+ lines.append(about_line())
343
+ if doctor_ok:
344
+ trouble = rift_action("Troubleshooting", ["doctor"], terminal=True)
345
+ if trouble:
346
+ lines.append(trouble)
347
+ lines.append("---")
348
+ lines.append("Refresh | refresh=true")
349
+ return lines
350
+
351
+
352
+ # --- Top-level render -----------------------------------------------------
353
+
354
+
355
+ def render_attention(doctor, health, lines):
356
+ """Render the 'needs attention' / indeterminate states.
357
+
358
+ Friend-safe by contract: the concrete repair always comes from the
359
+ doctor report (and the redacted prompt from `rift doctor --copy-prompt`,
360
+ wired into the footer). Doctor-powered actions are gated on a real report
361
+ existing — not on the rift binary resolving — so a present-but-broken CLI
362
+ never offers a fix it can't produce. We never emit launchd labels,
363
+ kickstart commands, or other operator internals.
364
+ """
365
+ doctor_ok = has_doctor_report(doctor)
366
+ broken_issues = []
367
+ if isinstance(doctor, dict):
368
+ broken_issues = [
369
+ i for i in (doctor.get("issues") or []) if i.get("severity") == "broken"
370
+ ]
371
+
372
+ if broken_issues:
373
+ # The doctor diagnosed something concrete — name it and surface the
374
+ # single repair action it chose.
375
+ lines.append("Rift needs attention")
376
+ lines.append("---")
377
+ for issue in broken_issues:
378
+ lines.append(f"✗ {issue.get('title', 'Something is wrong')}")
379
+ primary = doctor.get("primary") or broken_issues[0]
380
+ if primary.get("nextAction"):
381
+ lines.append("---")
382
+ lines.append(f"Fix: {primary['nextAction']}")
383
+ elif health == "down":
384
+ # Background engine isn't answering. Stay friend-safe: no launchctl,
385
+ # no daemon label. With no doctor report we also can't build a repair
386
+ # prompt, so say so plainly rather than offer dead doctor actions.
387
+ lines.append("Rift needs attention")
388
+ lines.append("---")
389
+ lines.append("Rift's background engine is not responding")
390
+ if not doctor_ok:
391
+ lines.append("Rift couldn't run a self-check — reopen Rift, then check below")
392
+ else:
393
+ # verdict unknown: couldn't even confirm health.
394
+ lines.append("Rift status unavailable")
395
+ lines.append("---")
396
+ lines.append("Rift couldn't reach its own tools to check")
397
+
398
+ lines += render_footer_actions(broken=True, doctor_ok=doctor_ok)
399
+ return lines
400
+
401
+
402
+ def render(doctor, status, mcp, health, now):
403
+ friend = status.get("friend") if isinstance(status, dict) else None
404
+ verdict = verdict_of(doctor, health)
405
+ lines = [TITLES[verdict], "---"]
406
+
407
+ if verdict in ("broken", "unknown"):
408
+ return render_attention(doctor, health, lines)
409
+
410
+ # healthy or warning
411
+ lines.append("Rift is working")
412
+ lines += render_capture_rows(friend, now)
413
+ mem = memory_today(mcp)
414
+ if mem:
415
+ lines.append(f"Memory today: {mem}")
416
+
417
+ client_rows = render_client_rows(status, doctor)
418
+ if client_rows:
419
+ lines.append("---")
420
+ lines += client_rows
421
+ if not has_voyage_issue(doctor):
422
+ lines.append("Search index healthy")
423
+
424
+ advisories = render_advisories(doctor)
425
+ if advisories:
426
+ lines.append("---")
427
+ lines.append("Heads up:")
428
+ lines += advisories
429
+
430
+ lines += render_footer_actions(broken=False, doctor_ok=has_doctor_report(doctor))
431
+ return lines
432
+
433
+
434
+ def main():
435
+ doctor = parse_json(env("RIFT_DOCTOR_JSON"))
436
+ status = parse_json(env("RIFT_STATUS_JSON"))
437
+ mcp = parse_json(env("RIFT_MCP_JSON"))
438
+ health = env("RIFT_HEALTH")
439
+ for line in render(doctor, status, mcp, health, now_ms()):
440
+ print(line)
441
+
442
+
443
+ if __name__ == "__main__":
444
+ main()
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # SwiftBar plugin — Rift friend-facing heartbeat.
4
+ #
5
+ # Answers one question at a glance: "Is Rift working?" Operator telemetry
6
+ # (MCP counters, launchd labels, Node versions, commits) is intentionally
7
+ # absent — run `rift status` / `rift doctor` for that. The verdict and the
8
+ # one repair action come from `rift doctor`; the repair PROMPT is generated
9
+ # by `rift doctor --copy-prompt` (never hardcoded here), so redaction and
10
+ # diagnosis stay centralized in the CLI.
11
+ #
12
+ # Install:
13
+ # rift menubar install
14
+ #
15
+ # Overrides:
16
+ # RIFT_BASE_URL default http://127.0.0.1:3577
17
+ # RIFT_LOG_DIR default ~/Library/Application Support/Rift/logs
18
+ # RIFT_DATA_DIR default ~/Library/Application Support/Rift/data
19
+ # RIFT_ABOUT_URL default https://getrift.dev/about
20
+ #
21
+ # Reads live daemon state on every tick. Never caches.
22
+
23
+ set -u
24
+
25
+ BASE_URL="${RIFT_BASE_URL:-http://127.0.0.1:3577}"
26
+ ABOUT_URL="${RIFT_ABOUT_URL:-https://getrift.dev/about}"
27
+
28
+ # Resolve script path even when invoked via symlink, so the plugin can
29
+ # sit in SwiftBar's plugins dir and still open the repo's logs/data dirs
30
+ # and find render-menu.py next to itself.
31
+ SOURCE="${BASH_SOURCE[0]}"
32
+ while [[ -L "$SOURCE" ]]; do
33
+ DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
34
+ SOURCE="$(readlink "$SOURCE")"
35
+ [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
36
+ done
37
+ SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
38
+ RIFT_HOME_DEFAULT="${HOME}/Library/Application Support/Rift"
39
+ LOG_DIR="${RIFT_LOG_DIR:-${RIFT_HOME_DEFAULT}/logs}"
40
+ DATA_DIR="${RIFT_DATA_DIR:-${RIFT_HOME_DEFAULT}/data}"
41
+ RENDERER="$SCRIPT_DIR/render-menu.py"
42
+
43
+ # Resolve the rift CLI. SwiftBar invokes shell commands with a stripped
44
+ # PATH, so menu actions need absolute paths end-to-end. The npm-installed
45
+ # `rift` is a symlink to a JS file with a `#!/usr/bin/env node` shebang —
46
+ # executing it directly under a stripped PATH fails with `env: node: No
47
+ # such file or directory`. So we resolve:
48
+ # - RIFT_BIN: the on-PATH `rift` launcher (diagnostics + fallback).
49
+ # - RIFT_JS: the real JS entrypoint behind RIFT_BIN's symlink chain,
50
+ # to pass as node's first arg.
51
+ # - NODE_BIN: an absolute node binary, so actions don't depend on PATH.
52
+ RIFT_BIN="${RIFT_BIN:-}"
53
+ if [[ -z "$RIFT_BIN" ]]; then
54
+ for candidate in \
55
+ "/opt/homebrew/bin/rift" \
56
+ "/usr/local/bin/rift" \
57
+ "$HOME/.npm-global/bin/rift" \
58
+ "$HOME/.local/bin/rift" \
59
+ "$REPO_ROOT/node_modules/.bin/rift"; do
60
+ if [[ -x "$candidate" ]]; then
61
+ RIFT_BIN="$candidate"
62
+ break
63
+ fi
64
+ done
65
+ fi
66
+
67
+ RIFT_JS=""
68
+ if [[ -n "$RIFT_BIN" ]]; then
69
+ target="$RIFT_BIN"
70
+ while [[ -L "$target" ]]; do
71
+ link_dir="$(cd -P "$(dirname "$target")" && pwd)"
72
+ link_target="$(readlink "$target")"
73
+ if [[ "$link_target" = /* ]]; then
74
+ target="$link_target"
75
+ else
76
+ target="$link_dir/$link_target"
77
+ fi
78
+ done
79
+ if [[ -f "$target" ]]; then
80
+ RIFT_JS="$target"
81
+ fi
82
+ fi
83
+
84
+ NODE_BIN="${NODE_BIN:-}"
85
+ if [[ -z "$NODE_BIN" ]]; then
86
+ for candidate in \
87
+ "/opt/homebrew/bin/node" \
88
+ "/usr/local/bin/node" \
89
+ "/usr/bin/node"; do
90
+ if [[ -x "$candidate" ]]; then
91
+ NODE_BIN="$candidate"
92
+ break
93
+ fi
94
+ done
95
+ fi
96
+
97
+ # Run a rift subcommand, preferring node + JS entrypoint (PATH-independent),
98
+ # falling back to the launcher. Prints stdout; swallows stderr. Returns the
99
+ # command's exit code, or 127 when rift can't be resolved at all.
100
+ run_rift() {
101
+ if [[ -n "$RIFT_JS" && -n "$NODE_BIN" ]]; then
102
+ "$NODE_BIN" "$RIFT_JS" "$@" 2>/dev/null
103
+ return $?
104
+ fi
105
+ if [[ -n "$RIFT_BIN" ]]; then
106
+ "$RIFT_BIN" "$@" 2>/dev/null
107
+ return $?
108
+ fi
109
+ return 127
110
+ }
111
+
112
+ # --- Gather state ---
113
+ # doctor --json is the canonical verdict + one fix (exits non-zero when
114
+ # broken, but still prints a valid report on stdout — capture regardless).
115
+ RIFT_DOCTOR_JSON="$(run_rift doctor --json || true)"
116
+ RIFT_STATUS_JSON="$(run_rift status --json --no-capability-map || true)"
117
+ # Memory-today is a delight metric from an unauthenticated endpoint.
118
+ RIFT_MCP_JSON="$(curl -fsS -m 2 "$BASE_URL/stats/mcp-usage" 2>/dev/null || true)"
119
+ # Coarse health probe — only consulted by the renderer when the doctor
120
+ # report is unavailable (rift CLI not resolvable on this machine).
121
+ if curl -fsS -m 2 "$BASE_URL/health" >/dev/null 2>&1; then
122
+ RIFT_HEALTH="up"
123
+ else
124
+ RIFT_HEALTH="down"
125
+ fi
126
+
127
+ export RIFT_DOCTOR_JSON RIFT_STATUS_JSON RIFT_MCP_JSON RIFT_HEALTH
128
+ export RIFT_NODE_BIN="$NODE_BIN" RIFT_JS RIFT_BIN
129
+ export RIFT_LOG_DIR="$LOG_DIR" RIFT_DATA_DIR="$DATA_DIR" RIFT_ABOUT_URL="$ABOUT_URL"
130
+
131
+ # --- Render ---
132
+ if [[ -f "$RENDERER" ]]; then
133
+ /usr/bin/python3 "$RENDERER"
134
+ else
135
+ # Renderer missing (broken install). Degrade to a minimal, honest menu
136
+ # rather than a blank one.
137
+ if [[ "$RIFT_HEALTH" == "up" ]]; then
138
+ echo "rift | sfimage=circle.fill sfcolor=systemGreen"
139
+ else
140
+ echo "rift | sfimage=exclamationmark.triangle.fill sfcolor=systemRed"
141
+ fi
142
+ echo "---"
143
+ echo "Menu renderer missing — reinstall Rift menu bar"
144
+ echo "Open logs | shell=/usr/bin/open param1=\"$LOG_DIR\" terminal=false"
145
+ echo "About Rift | href=$ABOUT_URL"
146
+ echo "Refresh | refresh=true"
147
+ fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getrift/rift",
3
- "version": "0.1.0-beta.12",
3
+ "version": "0.1.0-beta.14",
4
4
  "description": "Local-first personal memory + reasoning infrastructure with an MCP interface.",
5
5
  "homepage": "https://getrift.dev",
6
6
  "license": "UNLICENSED",
@@ -14,6 +14,7 @@
14
14
  },
15
15
  "files": [
16
16
  "dist/src",
17
+ "operator/swiftbar",
17
18
  "README.md"
18
19
  ],
19
20
  "scripts": {
@@ -26,6 +27,7 @@
26
27
  "check:rename": "bash scripts/check-rename.sh",
27
28
  "check:tarball": "bash scripts/verify-tarball.sh",
28
29
  "plist:generate": "bash scripts/generate-plist.sh",
30
+ "postinstall": "node -e \"const e=process.env;if(e.RIFT_SKIP_POSTINSTALL_MENUBAR==='1'||(e.RIFT_POSTINSTALL_MENUBAR!=='1'&&e.npm_config_global!=='true'&&e.npm_config_location!=='global'))process.exit(0);import('./dist/src/cli/postinstall-menubar.js').catch(() => {})\"",
29
31
  "prepublishOnly": "pnpm check && pnpm check:rename && pnpm build && pnpm check:tarball"
30
32
  },
31
33
  "dependencies": {
@@ -38,6 +40,7 @@
38
40
  "fastify": "^5.0.0",
39
41
  "mammoth": "^1.12.0",
40
42
  "pdfjs-dist": "^5.6.205",
43
+ "picocolors": "^1.1.1",
41
44
  "zod": "^3.24.0"
42
45
  },
43
46
  "devDependencies": {