@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.
- package/README.md +35 -9
- package/dist/src/cli/commands/doctor.d.ts +6 -0
- package/dist/src/cli/commands/doctor.d.ts.map +1 -0
- package/dist/src/cli/commands/doctor.js +183 -0
- package/dist/src/cli/commands/doctor.js.map +1 -0
- package/dist/src/cli/commands/menubar.d.ts +30 -0
- package/dist/src/cli/commands/menubar.d.ts.map +1 -0
- package/dist/src/cli/commands/menubar.js +180 -0
- package/dist/src/cli/commands/menubar.js.map +1 -0
- package/dist/src/cli/commands/onboard.d.ts +38 -0
- package/dist/src/cli/commands/onboard.d.ts.map +1 -1
- package/dist/src/cli/commands/onboard.js +203 -121
- package/dist/src/cli/commands/onboard.js.map +1 -1
- package/dist/src/cli/commands/status.d.ts +9 -7
- package/dist/src/cli/commands/status.d.ts.map +1 -1
- package/dist/src/cli/commands/status.js +29 -10
- package/dist/src/cli/commands/status.js.map +1 -1
- package/dist/src/cli/commands/update.d.ts +3 -0
- package/dist/src/cli/commands/update.d.ts.map +1 -1
- package/dist/src/cli/commands/update.js +19 -0
- package/dist/src/cli/commands/update.js.map +1 -1
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +4 -0
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/cli/postinstall-menubar.d.ts +22 -0
- package/dist/src/cli/postinstall-menubar.d.ts.map +1 -0
- package/dist/src/cli/postinstall-menubar.js +39 -0
- package/dist/src/cli/postinstall-menubar.js.map +1 -0
- package/dist/src/cli/status/friend-header.d.ts +8 -1
- package/dist/src/cli/status/friend-header.d.ts.map +1 -1
- package/dist/src/cli/status/friend-header.js +93 -12
- package/dist/src/cli/status/friend-header.js.map +1 -1
- package/dist/src/cli/ui.d.ts +47 -0
- package/dist/src/cli/ui.d.ts.map +1 -0
- package/dist/src/cli/ui.js +166 -0
- package/dist/src/cli/ui.js.map +1 -0
- package/dist/src/diagnostics/doctor.d.ts +106 -0
- package/dist/src/diagnostics/doctor.d.ts.map +1 -0
- package/dist/src/diagnostics/doctor.js +251 -0
- package/dist/src/diagnostics/doctor.js.map +1 -0
- package/dist/src/diagnostics/notify.d.ts +90 -0
- package/dist/src/diagnostics/notify.d.ts.map +1 -0
- package/dist/src/diagnostics/notify.js +177 -0
- package/dist/src/diagnostics/notify.js.map +1 -0
- package/dist/src/diagnostics/repair-prompt.d.ts +49 -0
- package/dist/src/diagnostics/repair-prompt.d.ts.map +1 -0
- package/dist/src/diagnostics/repair-prompt.js +198 -0
- package/dist/src/diagnostics/repair-prompt.js.map +1 -0
- package/dist/src/jobs/handlers/dedupe-conversations.d.ts +25 -2
- package/dist/src/jobs/handlers/dedupe-conversations.d.ts.map +1 -1
- package/dist/src/jobs/handlers/dedupe-conversations.js +48 -9
- package/dist/src/jobs/handlers/dedupe-conversations.js.map +1 -1
- package/dist/src/jobs/handlers/ingest.d.ts.map +1 -1
- package/dist/src/jobs/handlers/ingest.js +8 -2
- package/dist/src/jobs/handlers/ingest.js.map +1 -1
- package/dist/src/main.js +43 -4
- package/dist/src/main.js.map +1 -1
- package/dist/src/mcp/server.d.ts.map +1 -1
- package/dist/src/mcp/server.js +43 -3
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/mcp/tools/context-pack.js +163 -25
- package/dist/src/mcp/tools/context-pack.js.map +1 -1
- package/dist/src/observability/onboarding-metric.d.ts +115 -0
- package/dist/src/observability/onboarding-metric.d.ts.map +1 -0
- package/dist/src/observability/onboarding-metric.js +344 -0
- package/dist/src/observability/onboarding-metric.js.map +1 -0
- package/dist/src/observability/version-check.d.ts +1 -0
- package/dist/src/observability/version-check.d.ts.map +1 -1
- package/dist/src/observability/version-check.js +2 -1
- package/dist/src/observability/version-check.js.map +1 -1
- package/dist/src/retrieval/context-pack.d.ts +37 -0
- package/dist/src/retrieval/context-pack.d.ts.map +1 -1
- package/dist/src/retrieval/context-pack.js +165 -1
- package/dist/src/retrieval/context-pack.js.map +1 -1
- package/dist/src/retrieval/current-truth.d.ts +326 -0
- package/dist/src/retrieval/current-truth.d.ts.map +1 -0
- package/dist/src/retrieval/current-truth.js +747 -0
- package/dist/src/retrieval/current-truth.js.map +1 -0
- package/dist/src/retrieval/git-state.d.ts +53 -0
- package/dist/src/retrieval/git-state.d.ts.map +1 -0
- package/dist/src/retrieval/git-state.js +174 -0
- package/dist/src/retrieval/git-state.js.map +1 -0
- package/dist/src/server/routes/friend-status.d.ts +63 -0
- package/dist/src/server/routes/friend-status.d.ts.map +1 -1
- package/dist/src/server/routes/friend-status.js +97 -0
- package/dist/src/server/routes/friend-status.js.map +1 -1
- package/operator/swiftbar/render-menu.py +444 -0
- package/operator/swiftbar/rift.10s.sh +147 -0
- 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.
|
|
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": {
|