@join-pantheon/cli 0.26.4 → 0.27.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.
@@ -68,6 +68,8 @@ struct Item {
68
68
  let goal: String
69
69
  let ref: String
70
70
  let space: String
71
+ /// whose human must decide this: this member for their own decision, the teammate for theirs, "" when it is not a decision
72
+ let decider: String
71
73
 
72
74
  var isDecision: Bool { kind == "decision" }
73
75
  var heading: String {
@@ -152,7 +154,8 @@ func fetchState() -> TrayState {
152
154
  when: (i["when"] as? Double) ?? Double(i["when"] as? Int ?? 0),
153
155
  goal: i["goal"] as? String ?? "",
154
156
  ref: i["ref"] as? String ?? "",
155
- space: i["space"] as? String ?? ""
157
+ space: i["space"] as? String ?? "",
158
+ decider: i["decider"] as? String ?? ""
156
159
  ))
157
160
  }
158
161
  for d in (j["decisions"] as? [[String: Any]] ?? []) {
@@ -840,7 +843,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
840
843
  private var panelRendered: NSAttributedString? // the act laid out for a person
841
844
  private var panelRaw: String = "" // the act exactly as it was sent
842
845
  private var panelShowingRaw = false
843
- private var panelRawToggle: NSButton?
846
+ private var panelRawButton: NSButton? // "Original text" / "Laid out", shown only when the two differ
847
+ private var panelButtons: NSStackView? // the action row; hidden while replying
848
+ private var panelReplyRow: NSStackView? // the reply box; shown only in reply mode
849
+ private var panelCancel: NSButton?
850
+ private var panelReplyButton: NSButton? // the primary action, hidden while replying
851
+ private var panelStatusRow: NSStackView? // collapses when there is nothing to say
852
+ private var panelReply: NSTextView? // the person's own words
853
+ private var panelReplyKind: NSPopUpButton? // Answer / Note / Ask back
854
+ private var panelSendButton: NSButton?
855
+ private var panelRetract: NSButton? // the undo, for a minute after a send
856
+ private var panelLastSent: String = "" // act id of what was just sent
844
857
 
845
858
  private func label(_ text: String, size: CGFloat, weight: NSFont.Weight, color: NSColor, width: CGFloat) -> NSTextField {
846
859
  let l = NSTextField(labelWithString: text)
@@ -951,7 +964,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
951
964
  guard self.panelItem?.id == item.id else { return }
952
965
  self.panelRendered = rendered
953
966
  self.panelRaw = raw
954
- self.panelRawToggle?.isHidden = raw.isEmpty
967
+ self.panelRawButton?.isHidden = raw.isEmpty
955
968
  if !headline.isEmpty { self.panelHeadline?.stringValue = headline }
956
969
  if !self.panelShowingRaw && rendered.length > 0 { self.showBody(rendered, width: width) }
957
970
  }
@@ -962,7 +975,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
962
975
  @objc private func panelToggleRaw() {
963
976
  guard let item = panelItem else { return }
964
977
  panelShowingRaw.toggle()
965
- panelRawToggle?.title = panelShowingRaw ? "Show it laid out" : "Show it as sent"
978
+ panelRawButton?.title = panelShowingRaw ? "Laid out" : "Original text"
966
979
  let width: CGFloat = 620 - 52
967
980
  if panelShowingRaw { showBody(plain(panelRaw.isEmpty ? item.detail : panelRaw), width: width) }
968
981
  else if let r = panelRendered { showBody(r, width: width) }
@@ -1037,42 +1050,161 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
1037
1050
  if !item.goal.isEmpty { meta += " · goal \(item.goal)" }
1038
1051
  stack.addArrangedSubview(label(meta, size: 11, weight: .regular, color: .tertiaryLabelColor, width: inner))
1039
1052
 
1040
- let status = label("", size: 11, weight: .medium, color: .secondaryLabelColor, width: inner)
1053
+ // Whose decision this is, said before the box that answers it. Your own
1054
+ // answer to a teammate's decision lands as an act and settles nothing, so the
1055
+ // panel offers Answer only to the person it is waiting on.
1056
+ let owner: String = item.isDecision ? "Yours to decide."
1057
+ : !item.decider.isEmpty ? "\(item.decider)'s to decide. You can add a note or ask back."
1058
+ : item.kind == "owed" ? "Your agent owes this. You can add a note or ask back."
1059
+ : "Waiting on \(item.who). You can add a note or ask back."
1060
+ let ownerRow = NSStackView()
1061
+ ownerRow.orientation = .horizontal
1062
+ ownerRow.alignment = .centerY
1063
+ ownerRow.spacing = 10
1064
+ ownerRow.translatesAutoresizingMaskIntoConstraints = false
1065
+ let ownerLabel = label(owner, size: 12, weight: .medium, color: .secondaryLabelColor, width: inner - 170)
1066
+ ownerLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
1067
+ ownerRow.addArrangedSubview(ownerLabel)
1068
+ let ownerSpacer = NSView()
1069
+ ownerSpacer.translatesAutoresizingMaskIntoConstraints = false
1070
+ ownerSpacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
1071
+ ownerRow.addArrangedSubview(ownerSpacer)
1072
+ // The one action that is the person's own voice, set apart from the rest and
1073
+ // reachable with Return. Pressing it only opens the box; nothing is sent.
1074
+ let replyBtn = NSButton(title: "Reply manually", target: self, action: #selector(panelReplyManually))
1075
+ replyBtn.bezelStyle = .rounded
1076
+ replyBtn.keyEquivalent = "\r"
1077
+ replyBtn.setContentCompressionResistancePriority(.required, for: .horizontal)
1078
+ replyBtn.toolTip = "Type your own answer, note or question. Nothing is drafted for you, and nothing is sent until you press Send."
1079
+ panelReplyButton = replyBtn
1080
+ ownerRow.addArrangedSubview(replyBtn)
1081
+ ownerRow.widthAnchor.constraint(equalToConstant: inner).isActive = true
1082
+ if let idLine = stack.arrangedSubviews.last { stack.setCustomSpacing(6, after: idLine) }
1083
+ stack.addArrangedSubview(ownerRow)
1084
+ // Clear air between the person's own action and the rest: a rule, with room
1085
+ // on both sides, so the blue button is not read as part of the row below.
1086
+ stack.setCustomSpacing(22, after: ownerRow)
1087
+ let rule = NSBox()
1088
+ rule.boxType = .separator
1089
+ rule.translatesAutoresizingMaskIntoConstraints = false
1090
+ rule.widthAnchor.constraint(equalToConstant: inner).isActive = true
1091
+ stack.addArrangedSubview(rule)
1092
+ stack.setCustomSpacing(22, after: rule)
1093
+
1094
+ // The reply, in the person's own words: one popup for what it is, the box,
1095
+ // one Send. Return sends; Shift-Return makes a new line.
1096
+ let replyRow = NSStackView()
1097
+ replyRow.orientation = .horizontal
1098
+ replyRow.alignment = .top
1099
+ replyRow.spacing = 8
1100
+ replyRow.translatesAutoresizingMaskIntoConstraints = false
1101
+ let kinds = NSPopUpButton(frame: .zero, pullsDown: false)
1102
+ for k in replyKinds(item) { kinds.addItem(withTitle: k) }
1103
+ kinds.toolTip = "Answer settles it and the agent continues. Note goes on the record and settles nothing. Ask back puts a question to the agent that raised it."
1104
+ kinds.setContentHuggingPriority(.defaultHigh, for: .horizontal)
1105
+ panelReplyKind = kinds
1106
+ replyRow.addArrangedSubview(kinds)
1107
+ let replyScroll = NSScrollView()
1108
+ replyScroll.hasVerticalScroller = false
1109
+ replyScroll.borderType = .bezelBorder
1110
+ replyScroll.translatesAutoresizingMaskIntoConstraints = false
1111
+ let reply = NSTextView()
1112
+ reply.isEditable = true
1113
+ reply.isRichText = false
1114
+ reply.font = .systemFont(ofSize: 13)
1115
+ reply.textContainerInset = NSSize(width: 4, height: 6)
1116
+ reply.isVerticallyResizable = true
1117
+ reply.isHorizontallyResizable = false
1118
+ reply.autoresizingMask = [.width]
1119
+ reply.textContainer?.widthTracksTextView = true
1120
+ reply.delegate = self
1121
+ replyScroll.documentView = reply
1122
+ replyScroll.heightAnchor.constraint(equalToConstant: 58).isActive = true
1123
+ replyScroll.setContentHuggingPriority(.defaultLow, for: .horizontal)
1124
+ panelReply = reply
1125
+ replyRow.addArrangedSubview(replyScroll)
1126
+ let send = NSButton(title: "Send", target: self, action: #selector(panelSend(_:)))
1127
+ send.bezelStyle = .rounded
1128
+ send.setContentHuggingPriority(.defaultHigh, for: .horizontal)
1129
+ panelSendButton = send
1130
+ replyRow.addArrangedSubview(send)
1131
+ let cancel = NSButton(title: "Cancel", target: self, action: #selector(panelCancelReply))
1132
+ cancel.bezelStyle = .rounded
1133
+ cancel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
1134
+ cancel.toolTip = "Back to the buttons. Your words stay in the box."
1135
+ panelCancel = cancel
1136
+ replyRow.addArrangedSubview(cancel)
1137
+ replyRow.widthAnchor.constraint(equalToConstant: inner).isActive = true
1138
+ panelReplyRow = replyRow
1139
+ stack.addArrangedSubview(replyRow)
1140
+ replyRow.isHidden = true
1141
+
1142
+ // What happened, and the undo for a slip: Retract shows for a minute after a send.
1143
+ let statusRow = NSStackView()
1144
+ statusRow.orientation = .horizontal
1145
+ statusRow.spacing = 10
1146
+ statusRow.translatesAutoresizingMaskIntoConstraints = false
1147
+ let status = label("", size: 11, weight: .medium, color: .secondaryLabelColor, width: inner - 90)
1041
1148
  status.isHidden = true
1042
1149
  panelStatus = status
1043
- stack.addArrangedSubview(status)
1044
-
1045
- let row = NSStackView()
1046
- row.orientation = .horizontal
1047
- row.spacing = 10
1048
- row.translatesAutoresizingMaskIntoConstraints = false
1049
- let open = NSButton(title: "Open in \(state.surfaceName)", target: self, action: #selector(panelOpen))
1050
- open.bezelStyle = .rounded
1051
- open.keyEquivalent = "\r"
1052
- row.addArrangedSubview(open)
1053
- let copy = NSButton(title: "Copy the question", target: self, action: #selector(panelCopy))
1054
- copy.bezelStyle = .rounded
1055
- row.addArrangedSubview(copy)
1150
+ statusRow.addArrangedSubview(status)
1151
+ let retract = NSButton(title: "Retract", target: self, action: #selector(panelRetractAction))
1152
+ retract.bezelStyle = .rounded
1153
+ retract.controlSize = .small
1154
+ retract.toolTip = "Withdraws what you just sent. Your words stay in the record as retracted."
1155
+ retract.isHidden = true
1156
+ panelRetract = retract
1157
+ statusRow.addArrangedSubview(retract)
1158
+ panelStatusRow = statusRow
1159
+ stack.addArrangedSubview(statusRow)
1160
+ statusRow.isHidden = true
1161
+
1162
+ // Two rows: what to do about it, then what to do with the text. Every
1163
+ // button refuses to compress, so the labels are always whole.
1164
+ func button(_ title: String, _ sel: Selector, _ tip: String) -> NSButton {
1165
+ let b = NSButton(title: title, target: self, action: sel)
1166
+ b.bezelStyle = .rounded
1167
+ b.toolTip = tip
1168
+ b.setContentCompressionResistancePriority(.required, for: .horizontal)
1169
+ return b
1170
+ }
1171
+ let rows = NSStackView()
1172
+ rows.orientation = .vertical
1173
+ rows.alignment = .leading
1174
+ rows.spacing = 8
1175
+ rows.translatesAutoresizingMaskIntoConstraints = false
1176
+
1177
+ let row1 = NSStackView()
1178
+ row1.orientation = .horizontal
1179
+ row1.spacing = 10
1180
+ row1.addArrangedSubview(button("Explain in \(state.surfaceName)", #selector(panelOpen),
1181
+ "Opens \(state.surfaceName) in the team's folder and asks it to lay out your options. It will not decide or reply for you."))
1056
1182
  if item.isDecision {
1057
- let done = NSButton(title: "Mark done", target: self, action: #selector(panelDone))
1058
- done.bezelStyle = .rounded
1059
- done.toolTip = "Answer it as handled, so it stops asking"
1060
- row.addArrangedSubview(done)
1183
+ row1.addArrangedSubview(button("Mark done", #selector(panelDone), "Tells the agent you settled this in person, so it stops asking."))
1061
1184
  }
1062
- let thread = NSButton(title: "Show the thread", target: self, action: #selector(panelThread))
1063
- thread.bezelStyle = .rounded
1064
- row.addArrangedSubview(thread)
1065
- let rawToggle = NSButton(title: "Show it as sent", target: self, action: #selector(panelToggleRaw))
1066
- rawToggle.bezelStyle = .rounded
1067
- rawToggle.toolTip = "The act exactly as your teammate's agent wrote it"
1185
+ row1.addArrangedSubview(button("Show the thread", #selector(panelThread), "Every act in this goal, in Terminal, from the team's folder."))
1186
+ rows.addArrangedSubview(row1)
1187
+
1188
+ let row2 = NSStackView()
1189
+ row2.orientation = .horizontal
1190
+ row2.spacing = 10
1191
+ row2.addArrangedSubview(button("Copy the question", #selector(panelCopy), "Copies the question and its context as text, for an email or a message."))
1192
+ let rawToggle = button("Original text", #selector(panelToggleRaw), "Exactly as the agent wrote it, before Pantheon laid it out.")
1068
1193
  rawToggle.isHidden = true
1069
- panelRawToggle = rawToggle
1070
- row.addArrangedSubview(rawToggle)
1071
- let close = NSButton(title: "Close", target: self, action: #selector(panelClose))
1072
- close.bezelStyle = .rounded
1194
+ panelRawButton = rawToggle
1195
+ row2.addArrangedSubview(rawToggle)
1196
+ let spacer = NSView()
1197
+ spacer.translatesAutoresizingMaskIntoConstraints = false
1198
+ spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
1199
+ row2.addArrangedSubview(spacer)
1200
+ let close = button("Close", #selector(panelClose), "Nothing changes.")
1073
1201
  close.keyEquivalent = "\u{1b}"
1074
- row.addArrangedSubview(close)
1075
- stack.addArrangedSubview(row)
1202
+ row2.addArrangedSubview(close)
1203
+ row2.widthAnchor.constraint(equalToConstant: inner).isActive = true
1204
+ rows.addArrangedSubview(row2)
1205
+
1206
+ panelButtons = rows
1207
+ stack.addArrangedSubview(rows)
1076
1208
 
1077
1209
  NSLayoutConstraint.activate([
1078
1210
  stack.leadingAnchor.constraint(equalTo: effect.leadingAnchor, constant: 26),
@@ -1080,7 +1212,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
1080
1212
  stack.topAnchor.constraint(equalTo: effect.topAnchor, constant: 36),
1081
1213
  stack.bottomAnchor.constraint(equalTo: effect.bottomAnchor, constant: -22),
1082
1214
  scroll.widthAnchor.constraint(equalTo: stack.widthAnchor),
1083
- row.widthAnchor.constraint(lessThanOrEqualTo: stack.widthAnchor),
1215
+ rows.widthAnchor.constraint(lessThanOrEqualTo: stack.widthAnchor),
1084
1216
  ])
1085
1217
  let bh = scroll.heightAnchor.constraint(equalToConstant: bodyHeight(plain(item.detail), width: inner))
1086
1218
  bh.isActive = true
@@ -1104,6 +1236,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
1104
1236
  private func setStatus(_ text: String) {
1105
1237
  panelStatus?.stringValue = text
1106
1238
  panelStatus?.isHidden = text.isEmpty
1239
+ panelStatusRow?.isHidden = text.isEmpty && (panelRetract?.isHidden ?? true)
1107
1240
  }
1108
1241
 
1109
1242
  @objc private func panelClose() {
@@ -1114,14 +1247,120 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
1114
1247
  panelItem = nil
1115
1248
  panelHeadline = nil
1116
1249
  panelRendered = nil
1117
- panelRawToggle = nil
1250
+ panelRawButton = nil
1251
+ panelButtons = nil
1252
+ panelReplyRow = nil
1253
+ panelCancel = nil
1254
+ panelReplyButton = nil
1255
+ panelStatusRow = nil
1256
+ panelReply = nil
1257
+ panelReplyKind = nil
1258
+ panelSendButton = nil
1259
+ panelRetract = nil
1260
+ panelLastSent = ""
1118
1261
  panelRaw = ""
1119
1262
  panelShowingRaw = false
1120
1263
  }
1264
+ /// The question as text — what a person pasting into an email or a message
1265
+ /// wants. The agent prompt still rides with Explain, which copies it as a
1266
+ /// fallback when the agent cannot be opened.
1121
1267
  @objc private func panelCopy() {
1122
1268
  guard let item = panelItem else { return }
1123
- copyPrompt(item)
1124
- setStatus("Copied. Paste it into your agent.")
1269
+ let pb = NSPasteboard.general
1270
+ pb.clearContents()
1271
+ pb.setString(item.headline + "\n\n" + plain(panelRaw.isEmpty ? item.detail : panelRaw).string, forType: .string)
1272
+ setStatus("Copied the question.")
1273
+ }
1274
+
1275
+ /// Reply mode: the buttons step aside and only the box remains, with Cancel to
1276
+ /// bring them back. The body gives up a little height so the panel stays put.
1277
+ @objc private func panelReplyManually() {
1278
+ panelButtons?.isHidden = true
1279
+ panelReplyButton?.isHidden = true
1280
+ panelReplyButton?.keyEquivalent = ""
1281
+ panelSendButton?.keyEquivalent = "\r"
1282
+ panelReplyRow?.isHidden = false
1283
+ if let c = panelBodyHeight, c.constant > 140 { c.constant -= 20 }
1284
+ panelRetract?.isHidden = true
1285
+ setStatus("")
1286
+ if let tv = panelReply { detailPanel?.makeFirstResponder(tv) }
1287
+ }
1288
+ @objc private func panelCancelReply() { exitReplyMode() }
1289
+ private func exitReplyMode() {
1290
+ panelReplyRow?.isHidden = true
1291
+ panelSendButton?.keyEquivalent = ""
1292
+ panelButtons?.isHidden = false
1293
+ panelReplyButton?.isHidden = false
1294
+ panelReplyButton?.keyEquivalent = "\r"
1295
+ if let c = panelBodyHeight, c.constant > 0 { c.constant += 20 }
1296
+ }
1297
+
1298
+ /// What a person may send on this item. Answer only on their own decision:
1299
+ /// anyone else's answer would land as an act and settle nothing.
1300
+ private func replyKinds(_ item: Item) -> [String] {
1301
+ item.isDecision ? ["Answer", "Note", "Ask back"] : ["Note", "Ask back"]
1302
+ }
1303
+ private func kindCode(_ title: String?) -> String {
1304
+ switch title { case "Answer": return "ANSWER"; case "Ask back": return "ASK"; default: return "NOTE" }
1305
+ }
1306
+
1307
+ /// Send the words in the box as the chosen act. The CLI does the sending and
1308
+ /// prints "sent KIND id=… seq=…" or "not sent: why" on stdout; both are shown.
1309
+ @objc private func panelSend(_ sender: Any?) {
1310
+ guard let item = panelItem, let tv = panelReply else { return }
1311
+ let text = tv.string.trimmingCharacters(in: .whitespacesAndNewlines)
1312
+ if text.isEmpty { setStatus("Type your reply first."); return }
1313
+ let kind = kindCode(panelReplyKind?.titleOfSelectedItem)
1314
+ var payload: [String: Any] = ["action": "reply", "kind": kind, "body": text, "goal": item.goal, "space": item.space]
1315
+ if item.isDecision { payload["decision"] = item.id }
1316
+ if !item.ref.isEmpty { payload["act"] = item.ref }
1317
+ guard writeAction(payload) else { setStatus("Could not write the action. Use Show the thread instead."); return }
1318
+ setStatus("Sending…")
1319
+ panelSendButton?.isEnabled = false
1320
+ DispatchQueue.global().async {
1321
+ let r = run(pantheonBin, ["explain", "--queued"], timeout: 25)
1322
+ DispatchQueue.main.async {
1323
+ self.panelSendButton?.isEnabled = true
1324
+ let out = (r?.out ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
1325
+ if r?.code == 0, let idTok = out.split(separator: " ").first(where: { $0.hasPrefix("id=") }) {
1326
+ self.panelLastSent = String(idTok.dropFirst(3))
1327
+ tv.string = ""
1328
+ let what = kind == "ANSWER" ? "Answered — it is no longer waiting on you." : kind == "ASK" ? "Asked — the agent that raised this owes you a reply." : "Noted — it stays waiting."
1329
+ self.setStatus(what)
1330
+ self.exitReplyMode()
1331
+ self.panelRetract?.isHidden = false
1332
+ DispatchQueue.main.asyncAfter(deadline: .now() + 60) { [weak self] in self?.panelRetract?.isHidden = true }
1333
+ self.refresh()
1334
+ } else {
1335
+ let why = out.hasPrefix("not sent: ") ? String(out.dropFirst("not sent: ".count)) : "the relay could not be reached"
1336
+ self.setStatus("Not sent — \(why) Your words are still in the box.")
1337
+ }
1338
+ }
1339
+ }
1340
+ }
1341
+
1342
+ /// The undo. RETRACT withdraws the act; the record keeps it as retracted.
1343
+ @objc private func panelRetractAction() {
1344
+ guard !panelLastSent.isEmpty else { return }
1345
+ let id = panelLastSent
1346
+ guard writeAction(["action": "retract", "act": id]) else { setStatus("Could not write the action."); return }
1347
+ setStatus("Retracting…")
1348
+ panelRetract?.isEnabled = false
1349
+ DispatchQueue.global().async {
1350
+ let r = run(pantheonBin, ["explain", "--queued"], timeout: 25)
1351
+ DispatchQueue.main.async {
1352
+ self.panelRetract?.isEnabled = true
1353
+ if r?.code == 0 {
1354
+ self.panelLastSent = ""
1355
+ self.panelRetract?.isHidden = true
1356
+ self.setStatus("Retracted.")
1357
+ self.refresh()
1358
+ } else {
1359
+ let out = (r?.out ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
1360
+ self.setStatus("Could not retract — \(out.hasPrefix("not sent: ") ? String(out.dropFirst("not sent: ".count)) : "the relay could not be reached")")
1361
+ }
1362
+ }
1363
+ }
1125
1364
  }
1126
1365
  @objc private func panelOpen() {
1127
1366
  guard let item = panelItem else { return }
@@ -1328,3 +1567,12 @@ let app = NSApplication.shared
1328
1567
  let delegate = AppDelegate()
1329
1568
  app.delegate = delegate
1330
1569
  app.run()
1570
+
1571
+ extension AppDelegate: NSTextViewDelegate {
1572
+ func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
1573
+ guard textView === panelReply, commandSelector == #selector(NSResponder.insertNewline(_:)) else { return false }
1574
+ if NSApp.currentEvent?.modifierFlags.contains(.shift) == true { return false }
1575
+ panelSend(nil)
1576
+ return true
1577
+ }
1578
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@join-pantheon/cli",
3
- "version": "0.26.4",
3
+ "version": "0.27.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": {