drafted 1.11.7 → 1.11.9

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 (2) hide show
  1. package/install-mcp.sh +174 -0
  2. package/package.json +1 -1
package/install-mcp.sh CHANGED
@@ -625,6 +625,8 @@ import AppKit
625
625
 
626
626
  @main
627
627
  struct DraftedUpdaterApp: App {
628
+ @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
629
+
628
630
  init() {
629
631
  Telemetry.report(event: "drafted_update_helper_started", updateHelperStatus: "running")
630
632
  Telemetry.report(event: "drafted_heartbeat", updateHelperStatus: "running")
@@ -632,6 +634,7 @@ struct DraftedUpdaterApp: App {
632
634
 
633
635
  var body: some Scene {
634
636
  MenuBarExtra {
637
+ Button("Check for Updates…") { Actions.checkForUpdate(userInitiated: true) }
635
638
  Button("Update Drafted") { Actions.updateDrafted() }
636
639
  Button("View Logs") { Actions.viewLogs() }
637
640
  Button("Open Drafted") { Actions.openDrafted() }
@@ -651,6 +654,24 @@ struct DraftedUpdaterApp: App {
651
654
  }
652
655
  }
653
656
 
657
+ // Drives the genuine auto-update: a background check shortly after login and
658
+ // every 6 hours while the menu-bar helper is resident. When a newer published
659
+ // version exists (and the user hasn't skipped it), it prompts Update / Skip.
660
+ final class AppDelegate: NSObject, NSApplicationDelegate {
661
+ private var timer: Timer?
662
+
663
+ func applicationDidFinishLaunching(_ notification: Notification) {
664
+ DispatchQueue.main.asyncAfter(deadline: .now() + 25) {
665
+ Actions.checkForUpdate(userInitiated: false)
666
+ }
667
+ let t = Timer(timeInterval: 6 * 3600, repeats: true) { _ in
668
+ Actions.checkForUpdate(userInitiated: false)
669
+ }
670
+ RunLoop.main.add(t, forMode: .common)
671
+ timer = t
672
+ }
673
+ }
674
+
654
675
 
655
676
  final class UpdateStatusWindow: NSWindowController {
656
677
  private let stack = NSStackView()
@@ -734,6 +755,8 @@ final class UpdateStatusWindow: NSWindowController {
734
755
  }
735
756
 
736
757
  struct Actions {
758
+ static let home = FileManager.default.homeDirectoryForCurrentUser
759
+
737
760
  static func logoImage() -> NSImage? {
738
761
  guard
739
762
  let url = Bundle.main.url(forResource: "logo", withExtension: "svg"),
@@ -747,6 +770,157 @@ struct Actions {
747
770
  private static var isUpdating = false
748
771
  private static var updateWindow: UpdateStatusWindow?
749
772
 
773
+ // MARK: - Auto-update check
774
+
775
+ // Compare the installed CLI version to npm's published `latest`. Prompt only
776
+ // when newer and not skipped (background), or always report (user-initiated).
777
+ static func checkForUpdate(userInitiated: Bool) {
778
+ let installed = installedVersion()
779
+ latestVersion { latest in
780
+ DispatchQueue.main.async {
781
+ guard let latest = latest else {
782
+ if userInitiated {
783
+ showInfo(title: "Couldn’t check for updates", body: "Please check your connection and try again.")
784
+ }
785
+ return
786
+ }
787
+ guard let installed = installed, isNewer(latest, than: installed) else {
788
+ if userInitiated {
789
+ showInfo(title: "Drafted is up to date", body: "You’re on \(installed ?? "the latest") version.")
790
+ }
791
+ return
792
+ }
793
+ if !userInitiated && skippedVersion() == latest { return }
794
+ presentUpdateAlert(latest: latest, installed: installed)
795
+ }
796
+ }
797
+ }
798
+
799
+ static func presentUpdateAlert(latest: String, installed: String) {
800
+ Telemetry.report(event: "drafted_update_available", updateHelperStatus: "running")
801
+ let alert = NSAlert()
802
+ alert.messageText = "Drafted \(latest) is available"
803
+ alert.informativeText = "You have \(installed). Update now? Your editor needs to be restarted afterward to load the new MCP tools."
804
+ alert.addButton(withTitle: "Update")
805
+ alert.addButton(withTitle: "Skip This Version")
806
+ alert.addButton(withTitle: "Later")
807
+ if let logo = logoImage() { alert.icon = logo }
808
+ NSApp.activate(ignoringOtherApps: true)
809
+ switch alert.runModal() {
810
+ case .alertFirstButtonReturn:
811
+ updateDrafted()
812
+ case .alertSecondButtonReturn:
813
+ setSkippedVersion(latest)
814
+ Telemetry.report(event: "drafted_update_skipped", updateHelperStatus: "running")
815
+ default:
816
+ break
817
+ }
818
+ }
819
+
820
+ static func showInfo(title: String, body: String) {
821
+ let alert = NSAlert()
822
+ alert.messageText = title
823
+ alert.informativeText = body
824
+ alert.addButton(withTitle: "OK")
825
+ if let logo = logoImage() { alert.icon = logo }
826
+ NSApp.activate(ignoringOtherApps: true)
827
+ alert.runModal()
828
+ }
829
+
830
+ // Installed version, resolved WITHOUT depending on the login-shell PATH —
831
+ // the installer's npm prefix (~/.drafted/npm-global) is never written to any
832
+ // login profile, so `bash -lc "drafted"` finds nothing. Read the installed
833
+ // package.json at the fixed prefix first (the source npm itself writes), then
834
+ // fall back to the absolute binary, then a login shell (custom prefixes on PATH).
835
+ static func installedVersion() -> String? {
836
+ let pkg = home.appendingPathComponent(".drafted/npm-global/lib/node_modules/drafted/package.json")
837
+ if
838
+ let data = try? Data(contentsOf: pkg),
839
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
840
+ let version = json["version"] as? String,
841
+ isSemver(version)
842
+ { return version }
843
+
844
+ let bin = home.appendingPathComponent(".drafted/npm-global/bin/drafted").path
845
+ for command in ["\"\(bin)\" --version 2>/dev/null", "drafted --version 2>/dev/null"] {
846
+ let process = Process()
847
+ process.executableURL = URL(fileURLWithPath: "/bin/bash")
848
+ process.arguments = ["-lc", command]
849
+ let pipe = Pipe()
850
+ process.standardOutput = pipe
851
+ process.standardError = FileHandle.nullDevice
852
+ do { try process.run(); process.waitUntilExit() } catch { continue }
853
+ let data = pipe.fileHandleForReading.readDataToEndOfFile()
854
+ if let out = String(data: data, encoding: .utf8), let version = firstSemver(in: out) {
855
+ return version
856
+ }
857
+ }
858
+ return nil
859
+ }
860
+
861
+ static func latestVersion(_ completion: @escaping (String?) -> Void) {
862
+ guard let url = URL(string: "https://registry.npmjs.org/drafted/latest") else { completion(nil); return }
863
+ var request = URLRequest(url: url)
864
+ request.timeoutInterval = 15
865
+ URLSession.shared.dataTask(with: request) { data, _, _ in
866
+ guard
867
+ let data = data,
868
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
869
+ let version = json["version"] as? String
870
+ else { completion(nil); return }
871
+ completion(version)
872
+ }.resume()
873
+ }
874
+
875
+ static func skippedVersion() -> String? {
876
+ let path = home.appendingPathComponent(".drafted/install.json")
877
+ guard
878
+ let data = try? Data(contentsOf: path),
879
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
880
+ else { return nil }
881
+ return json["skippedVersion"] as? String
882
+ }
883
+
884
+ static func setSkippedVersion(_ version: String) {
885
+ let path = home.appendingPathComponent(".drafted/install.json")
886
+ var json = ((try? Data(contentsOf: path)).flatMap { try? JSONSerialization.jsonObject(with: $0) } as? [String: Any]) ?? [:]
887
+ json["skippedVersion"] = version
888
+ if let out = try? JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted, .sortedKeys]) {
889
+ try? out.write(to: path)
890
+ }
891
+ }
892
+
893
+ // First dotted-numeric token in the text, e.g. "1.11.7" from "drafted 1.11.7".
894
+ static func firstSemver(in text: String) -> String? {
895
+ var current = ""
896
+ for ch in text {
897
+ if ch.isNumber || ch == "." {
898
+ current.append(ch)
899
+ } else {
900
+ if isSemver(current) { return current }
901
+ current = ""
902
+ }
903
+ }
904
+ return isSemver(current) ? current : nil
905
+ }
906
+
907
+ static func isSemver(_ s: String) -> Bool {
908
+ let parts = s.split(separator: ".")
909
+ return parts.count >= 2 && parts.allSatisfy { !$0.isEmpty && $0.allSatisfy { $0.isNumber } }
910
+ }
911
+
912
+ // Numeric, dot-separated comparison (our versions have no prerelease tags).
913
+ static func isNewer(_ a: String, than b: String) -> Bool {
914
+ let pa = a.split(separator: ".").map { Int($0) ?? 0 }
915
+ let pb = b.split(separator: ".").map { Int($0) ?? 0 }
916
+ for i in 0..<max(pa.count, pb.count) {
917
+ let x = i < pa.count ? pa[i] : 0
918
+ let y = i < pb.count ? pb[i] : 0
919
+ if x != y { return x > y }
920
+ }
921
+ return false
922
+ }
923
+
750
924
  static func updateDrafted() {
751
925
  if isUpdating {
752
926
  updateWindow?.showUpdating()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.11.7",
3
+ "version": "1.11.9",
4
4
  "description": "Drafted — visual thinking surface for humans and AI agents. Renders HTML, markdown, images, and code as frames on a zoomable canvas, with MCP tools for AI agents and real-time sync for humans.",
5
5
  "type": "module",
6
6
  "files": [