@join-pantheon/cli 0.24.0 → 0.24.4
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/dist/cli.js +76 -0
- package/dist/continued.js +35 -0
- package/dist/decision-ui.js +14 -1
- package/dist/humanize.js +223 -0
- package/dist/notifier.js +19 -5
- package/dist/relay.js +11 -0
- package/dist/tray.js +29 -4
- package/dist/upgrade.js +5 -1
- package/dist/worker.js +34 -10
- package/helper/macos/main.swift +31 -1
- package/helper/macos/menubar.swift +577 -17
- package/package.json +1 -1
package/dist/worker.js
CHANGED
|
@@ -10,6 +10,7 @@ import { applyAnswer, loadPolicy, mayAsk, needsSelfUpgrade, recordAsk, savePolic
|
|
|
10
10
|
import { agentHookStates, repairHook } from "./notify.js";
|
|
11
11
|
import { fence, assess } from "./untrusted.js";
|
|
12
12
|
import { allowed, recordRun } from "./spend.js";
|
|
13
|
+
import { loadContinued, markContinued } from "./continued.js";
|
|
13
14
|
import { loadProfile, reservedDecisionsNote } from "./profile.js";
|
|
14
15
|
import { clip } from "./brief.js";
|
|
15
16
|
import { composeWorker } from "./notice.js";
|
|
@@ -178,6 +179,7 @@ There is no human in this session. Do not ask for approval; you will not get an
|
|
|
178
179
|
Act, reply if a reply is owed, then finish.`;
|
|
179
180
|
}
|
|
180
181
|
let fleetLock = null;
|
|
182
|
+
const CONTINUATION_HORIZON_MS = 24 * 60 * 60_000;
|
|
181
183
|
export class Worker {
|
|
182
184
|
cfg;
|
|
183
185
|
opts;
|
|
@@ -218,6 +220,19 @@ export class Worker {
|
|
|
218
220
|
this.cfg = cfg;
|
|
219
221
|
this.opts = opts;
|
|
220
222
|
}
|
|
223
|
+
spaceKey() { return this.cfg.spaceId ?? this.cfg.space; }
|
|
224
|
+
contLoaded = false;
|
|
225
|
+
loadContinued() {
|
|
226
|
+
if (this.contLoaded)
|
|
227
|
+
return;
|
|
228
|
+
this.contLoaded = true;
|
|
229
|
+
for (const id of loadContinued(this.spaceKey(), this.opts.home))
|
|
230
|
+
this.contDone.add(id);
|
|
231
|
+
}
|
|
232
|
+
markDone(id) {
|
|
233
|
+
this.contDone.add(id);
|
|
234
|
+
markContinued(this.spaceKey(), id, this.opts.home);
|
|
235
|
+
}
|
|
221
236
|
async bringMachineAlong() {
|
|
222
237
|
if (this.opts.selfUpgrade?.run)
|
|
223
238
|
return;
|
|
@@ -459,7 +474,7 @@ export class Worker {
|
|
|
459
474
|
this.requeueContinuation(continuation, 10_000);
|
|
460
475
|
}
|
|
461
476
|
else {
|
|
462
|
-
this.
|
|
477
|
+
this.markDone(continuation.id);
|
|
463
478
|
if (!this.skipLogged.has(`cont:${continuation.id}`)) {
|
|
464
479
|
this.skipLogged.add(`cont:${continuation.id}`);
|
|
465
480
|
this.opts.log(`- not continuing from ${continuation.id}: ${msg.split("\n")[0]}`);
|
|
@@ -510,7 +525,7 @@ export class Worker {
|
|
|
510
525
|
return this.runJob(client, o, persona, policy, (branch) => buildBrief(o, trigger, branch, persona), true);
|
|
511
526
|
}
|
|
512
527
|
async handleContinuation(client, act, persona, policy) {
|
|
513
|
-
this.
|
|
528
|
+
this.markDone(act.id);
|
|
514
529
|
const o = {
|
|
515
530
|
id: `cont:${act.id}`, kind: "CONTINUE", owner: this.cfg.member,
|
|
516
531
|
ref: act.id, goal: act.goal, created_at: act.ts,
|
|
@@ -590,19 +605,23 @@ export class Worker {
|
|
|
590
605
|
at: Date.now(), goal: o.goal, obligation: o.id, ok,
|
|
591
606
|
triggeredBy, summary,
|
|
592
607
|
}, this.opts.home);
|
|
593
|
-
|
|
608
|
+
const sent = client.mineSince(startedAt, o.goal);
|
|
609
|
+
const quiet = ok && !requireReply && sent.length === 0;
|
|
610
|
+
if (this.opts.notify && !quiet && (!ok || loadProfile(this.cfg).notifications?.worker_done)) {
|
|
594
611
|
try {
|
|
595
|
-
const reply =
|
|
612
|
+
const reply = sent[sent.length - 1] ?? null;
|
|
596
613
|
this.opts.notify(composeWorker({ ok, triggeredBy, obligationBody: o.body, goal: o.goal ? client.goals.get(o.goal) : undefined, reply, obligationRef: o.ref }, (() => { const tools = installedTools(); return { tools, preferred: preferredTool(loadProfile(this.cfg), tools, this.opts.home) }; })()));
|
|
597
614
|
}
|
|
598
615
|
catch { }
|
|
599
616
|
}
|
|
600
|
-
this.opts.log(
|
|
601
|
-
:
|
|
602
|
-
:
|
|
617
|
+
this.opts.log(quiet ? `< read ${o.id} on ${branch} — the thread was already settled, nothing sent`
|
|
618
|
+
: ok ? `< handled ${o.id} on ${branch}`
|
|
619
|
+
: !exited ? `! run failed for ${o.id}: ${detail.split("\n")[0]} (see ${runLog})`
|
|
620
|
+
: `! run exited cleanly but sent no act for ${o.id} — see ${runLog}`);
|
|
603
621
|
return { obligation: o, branch, worktree, ok, detail };
|
|
604
622
|
}
|
|
605
623
|
enqueueContinuations(client, arrived) {
|
|
624
|
+
this.loadContinued();
|
|
606
625
|
for (const r of arrived) {
|
|
607
626
|
if (this.contDone.has(r.id) || this.contQueue.some((c) => c.act.id === r.id))
|
|
608
627
|
continue;
|
|
@@ -613,6 +632,11 @@ export class Worker {
|
|
|
613
632
|
...(g ? { goalMembers: g.members, goalState: g.state } : {}),
|
|
614
633
|
}))
|
|
615
634
|
continue;
|
|
635
|
+
if (Date.now() - r.ts > CONTINUATION_HORIZON_MS) {
|
|
636
|
+
this.markDone(r.id);
|
|
637
|
+
this.opts.log(`- not continuing from ${r.act} ${r.id}: it landed ${Math.round((Date.now() - r.ts) / 3_600_000)}h ago — that thread is for a live session to read`);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
616
640
|
this.contQueue.push({ act: r, notBefore: Date.now() });
|
|
617
641
|
if (this.contQueue.length > 20)
|
|
618
642
|
this.contQueue.shift();
|
|
@@ -638,7 +662,7 @@ export class Worker {
|
|
|
638
662
|
ref: r.id, goal: r.goal, created_at: r.ts, body: r.body.slice(0, 120),
|
|
639
663
|
});
|
|
640
664
|
if (sel.skip) {
|
|
641
|
-
this.
|
|
665
|
+
this.markDone(r.id);
|
|
642
666
|
if (!this.skipLogged.has(`cont:${r.id}`)) {
|
|
643
667
|
this.skipLogged.add(`cont:${r.id}`);
|
|
644
668
|
this.opts.log(`- not continuing from ${r.act} ${r.id}: ${sel.skip}`);
|
|
@@ -689,10 +713,10 @@ export class Worker {
|
|
|
689
713
|
result = { ok: false, alreadyCurrent: false, from: VERSION, to: relayPkg ?? "?" };
|
|
690
714
|
this.opts.log(`! self-upgrade failed: ${e.message}`);
|
|
691
715
|
}
|
|
692
|
-
if (result.ok &&
|
|
716
|
+
if (result.ok && result.to === result.from) {
|
|
693
717
|
delete policy.attempts.self_upgrade;
|
|
694
718
|
savePolicy(policy, home);
|
|
695
|
-
this.opts.log(`* self-upgrade:
|
|
719
|
+
this.opts.log(`* self-upgrade: nothing newer than ${result.to} to install yet (the relay runs ${relayPkg}); will check again`);
|
|
696
720
|
return;
|
|
697
721
|
}
|
|
698
722
|
if (result.ok && !result.alreadyCurrent) {
|
package/helper/macos/main.swift
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* (the choice becomes the default), through a .command file
|
|
10
10
|
* Terminal runs — no automation permission needed
|
|
11
11
|
* Show details prints the act and the decision in Terminal
|
|
12
|
+
* Done (decisions) answers it as handled, silently — the reminders stop
|
|
12
13
|
* Dismiss / × closes
|
|
13
14
|
*
|
|
14
15
|
* It owns no state. Everything it needs arrives as one JSON document:
|
|
@@ -25,7 +26,7 @@ import AppKit
|
|
|
25
26
|
let args = CommandLine.arguments
|
|
26
27
|
|
|
27
28
|
if args.count == 2, args[1] == "version" {
|
|
28
|
-
print("
|
|
29
|
+
print("3")
|
|
29
30
|
exit(0)
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -45,6 +46,8 @@ struct Notice: Decodable {
|
|
|
45
46
|
let details: Details?
|
|
46
47
|
/** where to write the chosen action and which .command files to open — supplied by the CLI */
|
|
47
48
|
let bin_dir: String
|
|
49
|
+
/** the pantheon CLI to run silently for one-click actions — supplied by the CLI, since this helper has no PATH worth trusting */
|
|
50
|
+
let pantheon: String?
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
var notice: Notice
|
|
@@ -138,6 +141,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
138
141
|
act(["action": "details", "act": notice.details?.act as Any, "decision": notice.details?.decision as Any].compactMapValues { $0 is NSNull ? nil : $0 }, command: "pantheon-details.command")
|
|
139
142
|
}
|
|
140
143
|
|
|
144
|
+
/// Done: write the action, run the CLI without a Terminal, close. The CLI answers the decision as handled.
|
|
145
|
+
@objc func markDone(_ sender: Any?) {
|
|
146
|
+
guard !notice.bin_dir.isEmpty, let decision = notice.details?.decision else { NSApp.terminate(nil); return }
|
|
147
|
+
let dir = URL(fileURLWithPath: notice.bin_dir)
|
|
148
|
+
let payload: [String: Any] = ["action": "done", "decision": decision, "act": notice.details?.act as Any].compactMapValues { $0 is NSNull ? nil : $0 }
|
|
149
|
+
if let data = try? JSONSerialization.data(withJSONObject: payload) {
|
|
150
|
+
try? data.write(to: dir.appendingPathComponent(".hud-action.json"), options: .atomic)
|
|
151
|
+
}
|
|
152
|
+
let bin = (notice.pantheon?.isEmpty == false) ? notice.pantheon! : "pantheon"
|
|
153
|
+
let quoted = "'" + bin.replacingOccurrences(of: "'", with: "'\\''") + "'"
|
|
154
|
+
let p = Process()
|
|
155
|
+
p.executableURL = URL(fileURLWithPath: "/bin/sh")
|
|
156
|
+
p.arguments = ["-lc", "\(quoted) explain --queued >/dev/null 2>&1"] // login shell: PATH with node, as the user's own terminal has
|
|
157
|
+
try? p.run()
|
|
158
|
+
NSApp.terminate(nil)
|
|
159
|
+
}
|
|
160
|
+
|
|
141
161
|
@objc func legacyOpen(_ sender: Any?) {
|
|
142
162
|
if !legacyPrompt.isEmpty { NSPasteboard.general.clearContents(); NSPasteboard.general.setString(legacyPrompt, forType: .string) }
|
|
143
163
|
if let b = legacyBundle, let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: b) {
|
|
@@ -260,6 +280,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
260
280
|
constraints += [d.leadingAnchor.constraint(equalTo: pull.trailingAnchor, constant: 14), d.centerYAnchor.constraint(equalTo: pull.centerYAnchor)]
|
|
261
281
|
trailing = d
|
|
262
282
|
}
|
|
283
|
+
if notice.details?.decision != nil {
|
|
284
|
+
// A decision can be settled from the banner itself: one click, no terminal, no more reminders.
|
|
285
|
+
let done = NSButton(title: "Done", target: self, action: #selector(markDone(_:)))
|
|
286
|
+
done.isBordered = false; done.font = .systemFont(ofSize: 12, weight: .medium); done.contentTintColor = .controlAccentColor
|
|
287
|
+
done.setAccessibilityLabel("Mark this decision handled")
|
|
288
|
+
done.translatesAutoresizingMaskIntoConstraints = false
|
|
289
|
+
root.addSubview(done)
|
|
290
|
+
constraints += [done.leadingAnchor.constraint(equalTo: trailing.trailingAnchor, constant: 14), done.centerYAnchor.constraint(equalTo: pull.centerYAnchor)]
|
|
291
|
+
trailing = done
|
|
292
|
+
}
|
|
263
293
|
let dis = NSButton(title: "Dismiss", target: self, action: #selector(dismiss(_:)))
|
|
264
294
|
dis.isBordered = false; dis.font = .systemFont(ofSize: 12); dis.contentTintColor = .secondaryLabelColor
|
|
265
295
|
dis.translatesAutoresizingMaskIntoConstraints = false
|