@join-pantheon/cli 0.22.0 → 0.23.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.
- package/README.md +5 -0
- package/dist/cli.js +54 -12
- package/dist/client.js +34 -0
- package/dist/config.js +15 -3
- package/dist/doctor.js +13 -0
- package/dist/email.js +47 -23
- package/dist/host-env.js +57 -0
- package/dist/protocol.js +10 -1
- package/dist/quickstart.js +14 -4
- package/dist/relay.js +180 -74
- package/dist/roster-file.js +19 -0
- package/dist/service.js +25 -2
- package/dist/setup-guide.js +4 -2
- package/dist/tray.js +8 -2
- package/helper/macos/menubar.swift +110 -43
- package/package.json +2 -2
|
@@ -26,6 +26,8 @@ let pollSeconds = argv.count >= 3 ? (Double(argv[2]) ?? 15) : 15
|
|
|
26
26
|
struct TrayState {
|
|
27
27
|
var configured = false
|
|
28
28
|
var reachable = false // did tray-status itself run and parse
|
|
29
|
+
var decisionsOk = false
|
|
30
|
+
var unavailableSpaces: [String] = []
|
|
29
31
|
var relayOk = false
|
|
30
32
|
var space = ""
|
|
31
33
|
var member = ""
|
|
@@ -40,7 +42,7 @@ struct TrayState {
|
|
|
40
42
|
var surfaceName = "agent"
|
|
41
43
|
var surfaceBundleID = ""
|
|
42
44
|
var decisionsPrompt = ""
|
|
43
|
-
var decisions: [(id: String, body: String, prompt: String)] = []
|
|
45
|
+
var decisions: [(id: String, body: String, detail: String, prompt: String)] = []
|
|
44
46
|
var version = ""
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -70,6 +72,8 @@ func fetchState() -> TrayState {
|
|
|
70
72
|
s.reachable = true
|
|
71
73
|
s.configured = j["configured"] as? Bool ?? false
|
|
72
74
|
s.version = j["version"] as? String ?? ""
|
|
75
|
+
s.decisionsOk = j["decisions_ok"] as? Bool ?? false
|
|
76
|
+
s.unavailableSpaces = j["unavailable_spaces"] as? [String] ?? []
|
|
73
77
|
s.relayOk = j["relay_ok"] as? Bool ?? false
|
|
74
78
|
s.space = j["space"] as? String ?? ""
|
|
75
79
|
s.member = j["member"] as? String ?? ""
|
|
@@ -88,6 +92,7 @@ func fetchState() -> TrayState {
|
|
|
88
92
|
s.decisions.append((
|
|
89
93
|
id: d["id"] as? String ?? "",
|
|
90
94
|
body: d["body"] as? String ?? "",
|
|
95
|
+
detail: d["detail"] as? String ?? d["body"] as? String ?? "",
|
|
91
96
|
prompt: d["prompt"] as? String ?? ""
|
|
92
97
|
))
|
|
93
98
|
}
|
|
@@ -98,6 +103,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
98
103
|
private var item: NSStatusItem!
|
|
99
104
|
private var state = TrayState()
|
|
100
105
|
private var timer: Timer?
|
|
106
|
+
private var refreshing = false
|
|
107
|
+
private var menuOpen = false
|
|
108
|
+
private var hasLoaded = false
|
|
109
|
+
private var workerAction: String?
|
|
110
|
+
private var actionError: String?
|
|
101
111
|
|
|
102
112
|
func applicationDidFinishLaunching(_ note: Notification) {
|
|
103
113
|
NSApp.setActivationPolicy(.accessory)
|
|
@@ -105,6 +115,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
105
115
|
let menu = NSMenu()
|
|
106
116
|
menu.delegate = self
|
|
107
117
|
item.menu = menu
|
|
118
|
+
paint()
|
|
108
119
|
refresh()
|
|
109
120
|
timer = Timer.scheduledTimer(withTimeInterval: pollSeconds, repeats: true) { [weak self] _ in
|
|
110
121
|
self?.refresh()
|
|
@@ -112,12 +123,25 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
112
123
|
}
|
|
113
124
|
|
|
114
125
|
@objc func refresh() {
|
|
126
|
+
guard !refreshing else { return }
|
|
127
|
+
refreshing = true
|
|
115
128
|
DispatchQueue.global().async {
|
|
116
129
|
let next = fetchState()
|
|
117
|
-
DispatchQueue.main.async {
|
|
130
|
+
DispatchQueue.main.async {
|
|
131
|
+
self.state = next
|
|
132
|
+
self.hasLoaded = true
|
|
133
|
+
self.refreshing = false
|
|
134
|
+
self.paint()
|
|
135
|
+
if !self.menuOpen, let menu = self.item.menu { self.menuNeedsUpdate(menu) }
|
|
136
|
+
}
|
|
118
137
|
}
|
|
119
138
|
}
|
|
120
139
|
|
|
140
|
+
// Keep targets stable while a person is navigating the menu. The badge can
|
|
141
|
+
// refresh immediately; refreshed rows appear on the next opening.
|
|
142
|
+
func menuWillOpen(_ menu: NSMenu) { menuOpen = true; refresh() }
|
|
143
|
+
func menuDidClose(_ menu: NSMenu) { menuOpen = false }
|
|
144
|
+
|
|
121
145
|
enum MarkMode { case full, idle, off }
|
|
122
146
|
|
|
123
147
|
/// The Three Stones mark — the product's own logo, the same three arcs the
|
|
@@ -164,68 +188,87 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
164
188
|
private func paint() {
|
|
165
189
|
let mode: MarkMode = !state.configured || !state.relayOk ? .off : state.workerRunning ? .full : .idle
|
|
166
190
|
item.button?.image = markImage(mode)
|
|
167
|
-
|
|
191
|
+
let unknown = hasLoaded && (!state.reachable || (state.configured && !state.decisionsOk))
|
|
192
|
+
item.button?.title = actionError != nil ? " !" : state.decisions.isEmpty ? (unknown ? " !" : "") : " \(state.decisions.count)\(unknown ? "+" : "")"
|
|
168
193
|
item.button?.imagePosition = .imageLeading
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
194
|
+
let summary = !hasLoaded ? "Checking status…" : !state.reachable ? "Unable to check status" : !state.configured ? "Not joined to a team yet" : !state.decisionsOk ? "Unable to check every decision" : state.decisions.isEmpty ? "No decisions need your input" : "\(state.decisions.count) decisions need your input"
|
|
195
|
+
item.button?.toolTip = "Pantheon — " + (actionError ?? summary)
|
|
196
|
+
item.button?.setAccessibilityLabel("Pantheon — " + (actionError ?? summary))
|
|
172
197
|
}
|
|
173
198
|
|
|
174
199
|
func menuNeedsUpdate(_ menu: NSMenu) {
|
|
175
200
|
menu.removeAllItems()
|
|
176
201
|
let info = { (text: String) in
|
|
177
|
-
let
|
|
202
|
+
let title = text.count > 110 ? String(text.prefix(109)) + "…" : text
|
|
203
|
+
let mi = NSMenuItem(title: title, action: nil, keyEquivalent: "")
|
|
204
|
+
mi.isEnabled = false
|
|
205
|
+
mi.toolTip = text
|
|
206
|
+
mi.attributedTitle = NSAttributedString(string: title, attributes: [.foregroundColor: NSColor.labelColor])
|
|
178
207
|
menu.addItem(mi)
|
|
179
208
|
}
|
|
180
209
|
|
|
181
|
-
if !
|
|
182
|
-
info("
|
|
183
|
-
|
|
210
|
+
if !hasLoaded {
|
|
211
|
+
info("Checking Pantheon…")
|
|
212
|
+
} else if !state.reachable {
|
|
213
|
+
info("Unable to check Pantheon status")
|
|
214
|
+
info("Try Refresh now, or Check connection below")
|
|
184
215
|
} else if !state.configured {
|
|
185
|
-
info("
|
|
186
|
-
info("Run
|
|
216
|
+
info("Connect with your team")
|
|
217
|
+
info("Run pantheon start, or join with an invite link")
|
|
187
218
|
} else {
|
|
188
|
-
info("\(state.member)
|
|
219
|
+
info("\(state.member) · \(state.space)")
|
|
189
220
|
info(state.relayOk
|
|
190
|
-
? "
|
|
191
|
-
: "
|
|
192
|
-
info("\(state.workerRunning ? "●" : "○") Unattended work \(state.workerRunning ? "on" : "off") " +
|
|
193
|
-
"\(state.notifierRunning ? "●" : "○") Alerts \(state.hookOn ? "●" : "○") Wake hook")
|
|
194
|
-
if !state.workerState.isEmpty {
|
|
195
|
-
info("Worker: " + String(state.workerState.prefix(80)))
|
|
196
|
-
}
|
|
197
|
-
|
|
221
|
+
? "Connected · \(state.online) of \(state.members) members online"
|
|
222
|
+
: "Connection unavailable · \(state.unavailableSpaces.joined(separator: ", "))")
|
|
198
223
|
menu.addItem(.separator())
|
|
224
|
+
if !state.decisionsOk {
|
|
225
|
+
info("Some decisions could not be checked")
|
|
226
|
+
info("Refresh to try again. The count may be incomplete.")
|
|
227
|
+
}
|
|
199
228
|
if state.decisions.isEmpty {
|
|
200
|
-
info("
|
|
229
|
+
if state.decisionsOk { info("No decisions need your input") }
|
|
201
230
|
} else {
|
|
202
231
|
let noun = state.decisions.count == 1 ? "decision needs" : "decisions need"
|
|
203
232
|
info("\(state.decisions.count) \(noun) your input")
|
|
204
|
-
info("Choose one — your agent opens with the question already asked")
|
|
205
233
|
for d in state.decisions.prefix(5) {
|
|
206
|
-
let
|
|
207
|
-
|
|
208
|
-
|
|
234
|
+
let decision = makeItem(d.body.count > 72 ? String(d.body.prefix(71)) + "…" : d.body, #selector(openDecision(_:)))
|
|
235
|
+
decision.toolTip = d.detail
|
|
236
|
+
decision.representedObject = [d.id, d.prompt]
|
|
237
|
+
menu.addItem(decision)
|
|
209
238
|
}
|
|
210
|
-
|
|
211
|
-
menu.addItem(makeItem("Explain all in \(state.surfaceName)", #selector(openAllDecisions)))
|
|
212
|
-
}
|
|
213
|
-
menu.addItem(makeItem("View all decision details", #selector(openApprovals)))
|
|
239
|
+
menu.addItem(makeItem("\(state.decisions.count > 5 ? "Review all \(state.decisions.count) decisions" : "Review decisions") in \(state.surfaceName)", #selector(openAllDecisions)))
|
|
214
240
|
}
|
|
215
|
-
|
|
216
241
|
menu.addItem(.separator())
|
|
217
|
-
|
|
242
|
+
info(state.workerRunning ? "Unattended work is on" : state.workerInstalled ? "Unattended work is paused" : "Unattended work is not set up")
|
|
243
|
+
if !state.workerState.isEmpty && state.workerRunning { info(state.workerState) }
|
|
244
|
+
if let action = workerAction {
|
|
245
|
+
info(action)
|
|
246
|
+
} else if state.workerRunning {
|
|
218
247
|
menu.addItem(makeItem("Pause unattended work", #selector(pauseWorker)))
|
|
219
248
|
} else if state.workerInstalled {
|
|
220
249
|
menu.addItem(makeItem("Resume unattended work", #selector(resumeWorker)))
|
|
221
250
|
} else {
|
|
222
|
-
info("
|
|
251
|
+
info("Set up with pantheon service install worker")
|
|
223
252
|
}
|
|
224
253
|
}
|
|
254
|
+
if let error = actionError {
|
|
255
|
+
menu.addItem(.separator())
|
|
256
|
+
info(error)
|
|
257
|
+
}
|
|
225
258
|
|
|
259
|
+
let details = NSMenuItem(title: "Diagnostics", action: nil, keyEquivalent: "")
|
|
260
|
+
let submenu = NSMenu()
|
|
261
|
+
for text in ["Version \(state.version)", "Alerts: \(state.notifierRunning ? "running" : "off")", "Wake hook: \(state.hookOn ? "installed" : "off")"] {
|
|
262
|
+
let row = NSMenuItem(title: text, action: nil, keyEquivalent: "")
|
|
263
|
+
row.isEnabled = false
|
|
264
|
+
submenu.addItem(row)
|
|
265
|
+
}
|
|
266
|
+
submenu.addItem(makeItem("View decision details in Terminal", #selector(openApprovals)))
|
|
267
|
+
details.submenu = submenu
|
|
268
|
+
menu.addItem(details)
|
|
226
269
|
menu.addItem(.separator())
|
|
227
270
|
menu.addItem(makeItem("Open status in Terminal", #selector(openStatus)))
|
|
228
|
-
menu.addItem(makeItem("
|
|
271
|
+
menu.addItem(makeItem("Check connection in Terminal", #selector(openDoctor)))
|
|
229
272
|
menu.addItem(makeItem("Refresh now", #selector(refreshNow)))
|
|
230
273
|
menu.addItem(.separator())
|
|
231
274
|
menu.addItem(makeItem("Quit pantheon menu bar", #selector(quit)))
|
|
@@ -237,15 +280,26 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
237
280
|
return mi
|
|
238
281
|
}
|
|
239
282
|
|
|
283
|
+
private func changeWorker(_ title: String, path: String, args: [String]) {
|
|
284
|
+
guard workerAction == nil else { return }
|
|
285
|
+
workerAction = title
|
|
286
|
+
actionError = nil
|
|
287
|
+
paint()
|
|
288
|
+
if let menu = item.menu { menuNeedsUpdate(menu) }
|
|
289
|
+
DispatchQueue.global().async {
|
|
290
|
+
let result = run(path, args)
|
|
291
|
+
DispatchQueue.main.async {
|
|
292
|
+
self.workerAction = nil
|
|
293
|
+
if result?.code != 0 { self.reportFailure("Could not change unattended work. Check connection in Terminal.") }
|
|
294
|
+
self.refresh()
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
240
298
|
@objc private func pauseWorker() {
|
|
241
|
-
|
|
299
|
+
changeWorker("Pausing unattended work…", path: pantheonBin, args: ["stop", "worker"])
|
|
242
300
|
}
|
|
243
301
|
@objc private func resumeWorker() {
|
|
244
|
-
|
|
245
|
-
DispatchQueue.global().async {
|
|
246
|
-
_ = run("/bin/launchctl", ["load", "-w", plist])
|
|
247
|
-
self.refresh()
|
|
248
|
-
}
|
|
302
|
+
changeWorker("Resuming unattended work…", path: "/bin/launchctl", args: ["load", "-w", state.workerPlist])
|
|
249
303
|
}
|
|
250
304
|
// .command files are written by `pantheon menubar` next to the tray binary;
|
|
251
305
|
// Terminal owns running them, so an unsigned accessory app never needs
|
|
@@ -273,13 +327,26 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
273
327
|
}
|
|
274
328
|
let dir = (Bundle.main.executablePath as NSString?)?.deletingLastPathComponent
|
|
275
329
|
?? URL(fileURLWithPath: CommandLine.arguments[0]).deletingLastPathComponent().path
|
|
276
|
-
|
|
277
|
-
|
|
330
|
+
do {
|
|
331
|
+
try id.write(toFile: dir + "/.explain-id", atomically: true, encoding: .utf8)
|
|
332
|
+
openCommand("pantheon-explain.command")
|
|
333
|
+
} catch {
|
|
334
|
+
reportFailure("Could not open your agent. The question is copied; paste it into your agent.")
|
|
335
|
+
}
|
|
278
336
|
}
|
|
279
337
|
private func openCommand(_ name: String) {
|
|
280
338
|
let dir = (Bundle.main.executablePath as NSString?)?.deletingLastPathComponent
|
|
281
339
|
?? URL(fileURLWithPath: CommandLine.arguments[0]).deletingLastPathComponent().path
|
|
282
|
-
NSWorkspace.shared.open(URL(fileURLWithPath: dir + "/" + name))
|
|
340
|
+
if !NSWorkspace.shared.open(URL(fileURLWithPath: dir + "/" + name)) {
|
|
341
|
+
reportFailure("Could not open Terminal. Run pantheon doctor to check your setup.")
|
|
342
|
+
} else {
|
|
343
|
+
actionError = nil
|
|
344
|
+
paint()
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
private func reportFailure(_ message: String) {
|
|
348
|
+
actionError = message
|
|
349
|
+
paint()
|
|
283
350
|
}
|
|
284
351
|
@objc private func refreshNow() { refresh() }
|
|
285
352
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@join-pantheon/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Let your coding agent collaborate directly with your teammates' agents — ask, agree an interface, and deliver, without a human relaying messages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"build": "rm -rf dist && tsc -p .",
|
|
14
14
|
"dev": "tsc -p . --watch",
|
|
15
15
|
"relay": "node dist/cli.js relay",
|
|
16
|
-
"test": "PANTHEON_HOME=\"$(mktemp -d)\" node --test --test-force-exit dist/test/*.test.js",
|
|
16
|
+
"test": "PANTHEON_HOME=\"$(mktemp -d)\" HOME=\"$(mktemp -d)\" node --test --test-force-exit dist/test/*.test.js",
|
|
17
17
|
"prepare": "npm run build",
|
|
18
18
|
"prepublishOnly": "npm run build && npm test",
|
|
19
19
|
"start": "node dist/cli.js relay"
|