@saadnvd1/agent-os 0.3.700 → 0.3.723

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 (306) hide show
  1. package/README.md +2 -0
  2. package/app/api/agent-roles/route.test.ts +67 -0
  3. package/app/api/agent-roles/route.ts +61 -0
  4. package/app/api/analyst/[id]/recommendations/[recId]/again/route.ts +15 -0
  5. package/app/api/analyst/[id]/recommendations/[recId]/dismiss/route.ts +13 -0
  6. package/app/api/analyst/[id]/recommendations/[recId]/send/route.ts +13 -0
  7. package/app/api/analyst/[id]/review/route.ts +22 -0
  8. package/app/api/analyst/[id]/route.ts +44 -0
  9. package/app/api/analyst/route.ts +10 -0
  10. package/app/api/bus/groups/route.ts +44 -0
  11. package/app/api/bus/spawn/route.ts +34 -2
  12. package/app/api/claims/[id]/route.ts +20 -0
  13. package/app/api/claims/check/route.ts +23 -0
  14. package/app/api/claims/check.test.ts +113 -0
  15. package/app/api/claims/exchange/route.ts +42 -0
  16. package/app/api/claims/outside.test.ts +90 -0
  17. package/app/api/claims/route.test.ts +193 -0
  18. package/app/api/claims/route.ts +73 -0
  19. package/app/api/hosts/[id]/route.ts +9 -1
  20. package/app/api/models/availability/route.ts +9 -0
  21. package/app/api/models/choices/route.ts +9 -0
  22. package/app/api/peer/unlink/route.ts +17 -0
  23. package/app/api/peers/[hostId]/audit/route.ts +14 -0
  24. package/app/api/peers/[hostId]/route.ts +20 -0
  25. package/app/api/peers/gate.test.ts +64 -1
  26. package/app/api/peers/pending/route.test.ts +37 -0
  27. package/app/api/peers/pending/route.ts +11 -4
  28. package/app/api/session-groups/[id]/restore/route.ts +11 -0
  29. package/app/api/session-groups/[id]/route.ts +25 -0
  30. package/app/api/session-groups/order/route.ts +15 -0
  31. package/app/api/session-groups/route.ts +21 -0
  32. package/app/api/sessions/[id]/analyst/route.ts +11 -0
  33. package/app/api/sessions/[id]/crew/route.ts +38 -0
  34. package/app/api/sessions/[id]/export/estimate/route.ts +23 -0
  35. package/app/api/sessions/[id]/export/route.test.ts +125 -0
  36. package/app/api/sessions/[id]/export/route.ts +34 -0
  37. package/app/api/sessions/[id]/fork/route.ts +3 -0
  38. package/app/api/sessions/[id]/group/route.ts +18 -0
  39. package/app/api/sessions/[id]/model/route.ts +49 -0
  40. package/app/api/sessions/[id]/route.ts +14 -4
  41. package/app/api/sessions/[id]/summarize/route.ts +2 -0
  42. package/app/api/sessions/claims.test.ts +122 -0
  43. package/app/api/sessions/group/route.ts +17 -0
  44. package/app/api/sessions/route.ts +31 -14
  45. package/app/page.tsx +10 -0
  46. package/components/Chat/Messages.tsx +104 -10
  47. package/components/Claims/ClaimFiles.tsx +38 -0
  48. package/components/Claims/ClaimRow.tsx +82 -0
  49. package/components/Claims/ClaimsSheet.tsx +176 -0
  50. package/components/Claims/OverlapPill.tsx +31 -0
  51. package/components/Claims/StartClaimsNotice.tsx +70 -0
  52. package/components/Claims/claims-ui.test.ts +161 -0
  53. package/components/Claims/format.ts +57 -0
  54. package/components/Claims/index.ts +3 -0
  55. package/components/Draft/DraftChips.tsx +6 -1
  56. package/components/Draft/DraftPanel.tsx +90 -3
  57. package/components/Draft/ModelUnavailableDialog.tsx +78 -0
  58. package/components/Draft/RoleChips.tsx +110 -0
  59. package/components/Export/ExportActions.tsx +136 -0
  60. package/components/Export/ExportSheet.tsx +217 -0
  61. package/components/Export/RangePicker.tsx +144 -0
  62. package/components/Export/Seg.tsx +38 -0
  63. package/components/Export/TurnSelectBar.tsx +45 -0
  64. package/components/Hosts/AccountMachines.tsx +32 -18
  65. package/components/Hosts/HostRow.tsx +155 -14
  66. package/components/Hosts/IncomingLinkSheet.tsx +28 -10
  67. package/components/Hosts/PeerActivitySheet.tsx +111 -0
  68. package/components/Models/AgentRoleCard.tsx +114 -0
  69. package/components/Models/AgentRolesSection.tsx +132 -0
  70. package/components/Models/ModelBadges.tsx +48 -0
  71. package/components/Models/ModelsSection.tsx +4 -2
  72. package/components/Models/RoleModelSelect.tsx +87 -0
  73. package/components/Pane/DesktopTabBar.tsx +2 -0
  74. package/components/Pane/MobileTabBar.tsx +12 -0
  75. package/components/Pane/PaneMenu.tsx +10 -0
  76. package/components/SessionList/GroupShelves.tsx +195 -0
  77. package/components/SessionList/MoveToGroupMenu.tsx +105 -0
  78. package/components/SessionList/RowSubtitle.tsx +7 -1
  79. package/components/SessionList/SelectionToolbar.tsx +52 -1
  80. package/components/SessionList/SessionRow.tsx +19 -7
  81. package/components/SessionList/SessionRowMenu.tsx +23 -0
  82. package/components/SessionList/SessionShelves.tsx +5 -0
  83. package/components/SessionList/SidebarBody.tsx +6 -0
  84. package/components/SessionList/SidebarContextMenu.test.ts +104 -0
  85. package/components/SessionList/SidebarContextMenu.tsx +59 -0
  86. package/components/SessionList/SidebarHeader.tsx +5 -0
  87. package/components/SessionList/WorkspaceSwitcher.tsx +23 -6
  88. package/components/SessionList/index.tsx +18 -14
  89. package/components/SessionList/useSidebarData.ts +10 -0
  90. package/components/Sessions/RolePill.tsx +196 -0
  91. package/components/Settings/SettingsDialog.tsx +9 -0
  92. package/components/Share/ShareDialog.tsx +13 -0
  93. package/components/ui/notice.tsx +46 -0
  94. package/components/ui/status-pill.tsx +39 -0
  95. package/data/analyst/index.ts +3 -0
  96. package/data/analyst/keys.ts +6 -0
  97. package/data/analyst/mutations.ts +116 -0
  98. package/data/analyst/queries.ts +35 -0
  99. package/data/claims/index.ts +2 -0
  100. package/data/claims/keys.ts +5 -0
  101. package/data/claims/queries.ts +47 -0
  102. package/data/exports/index.ts +2 -0
  103. package/data/exports/keys.ts +3 -0
  104. package/data/exports/queries.ts +51 -0
  105. package/data/models/index.ts +98 -0
  106. package/data/peers/keys.ts +1 -0
  107. package/data/peers/queries.ts +31 -0
  108. package/data/push/topics.ts +9 -0
  109. package/data/session-groups/index.ts +156 -0
  110. package/data/sessions/index.ts +0 -1
  111. package/data/sessions/pin.ts +2 -6
  112. package/data/sessions/queries.ts +36 -29
  113. package/data/sessions/shelf.ts +2 -6
  114. package/data/tasks/index.ts +2 -0
  115. package/hooks/useSessions.ts +0 -12
  116. package/lib/__fixtures__/peer-rpc.ts +65 -33
  117. package/lib/agents/brief.ts +1 -0
  118. package/lib/agents/spawn.ts +4 -0
  119. package/lib/analyst/__fixtures__/items.ts +57 -0
  120. package/lib/analyst/__fixtures__/mocks.ts +49 -0
  121. package/lib/analyst/brief.ts +17 -0
  122. package/lib/analyst/buildDigest.ts +204 -0
  123. package/lib/analyst/createLink.ts +98 -0
  124. package/lib/analyst/endLink.ts +31 -0
  125. package/lib/analyst/forward.ts +109 -0
  126. package/lib/analyst/index.ts +16 -0
  127. package/lib/analyst/linkView.ts +38 -0
  128. package/lib/analyst/onTurnEnd.ts +119 -0
  129. package/lib/analyst/parseReview.ts +41 -0
  130. package/lib/analyst/pushDigest.ts +143 -0
  131. package/lib/analyst/resume.ts +35 -0
  132. package/lib/analyst/route.ts +50 -0
  133. package/lib/analyst/signals.ts +121 -0
  134. package/lib/analyst/store.ts +303 -0
  135. package/lib/analyst/types.ts +140 -0
  136. package/lib/bus/condense.ts +5 -1
  137. package/lib/bus/index.ts +13 -0
  138. package/lib/bus/read-scope.ts +12 -0
  139. package/lib/bus/role-alias.ts +70 -0
  140. package/lib/bus/targets.ts +3 -0
  141. package/lib/chat/drivers/opencode-mapper.ts +55 -1
  142. package/lib/chat/drivers/pi-approvals.ts +7 -4
  143. package/lib/chat/drivers/pi-mapper.ts +38 -0
  144. package/lib/chat/drivers/pi-text-tools.ts +104 -0
  145. package/lib/chat/drivers/pi.ts +8 -3
  146. package/lib/chat/drivers/turn-items.ts +36 -11
  147. package/lib/chat/drivers/turn-outcome.ts +108 -0
  148. package/lib/chat/events.ts +8 -0
  149. package/lib/chat/registry.ts +13 -0
  150. package/lib/chat/store.ts +19 -0
  151. package/lib/chat/turns.ts +1 -0
  152. package/lib/chat/worker/extras.ts +4 -2
  153. package/lib/claims/collectLocal.ts +123 -0
  154. package/lib/claims/describe.ts +41 -0
  155. package/lib/claims/exchange.ts +91 -0
  156. package/lib/claims/goal.ts +45 -0
  157. package/lib/claims/index.ts +10 -0
  158. package/lib/claims/loop.ts +51 -0
  159. package/lib/claims/machine.ts +21 -0
  160. package/lib/claims/mine.ts +43 -0
  161. package/lib/claims/notifyOverlaps.ts +101 -0
  162. package/lib/claims/onMerged.ts +85 -0
  163. package/lib/claims/outside.ts +36 -0
  164. package/lib/claims/overlap.ts +87 -0
  165. package/lib/claims/overlapCache.ts +86 -0
  166. package/lib/claims/overlapText.ts +36 -0
  167. package/lib/claims/peers.ts +56 -0
  168. package/lib/claims/startCheck.ts +100 -0
  169. package/lib/claims/startWarning.ts +29 -0
  170. package/lib/claims/store.ts +366 -0
  171. package/lib/claims/types.ts +102 -0
  172. package/lib/claims/view.ts +80 -0
  173. package/lib/claims/yield.ts +24 -0
  174. package/lib/db/changes.ts +12 -0
  175. package/lib/db/migrations/20261020T1400_work_claims.ts +35 -0
  176. package/lib/db/migrations/20261020T1515_peer_unlinked.ts +22 -0
  177. package/lib/db/migrations/20261020T1700_clean_connect_names.ts +18 -0
  178. package/lib/db/migrations/20261020T1800_session_groups.ts +49 -0
  179. package/lib/db/migrations/20261020T1900_task_queue_after_claim.ts +13 -0
  180. package/lib/db/migrations/20261020T2000_claim_notices.ts +20 -0
  181. package/lib/db/migrations/20261020T2100_claims_via_host.ts +21 -0
  182. package/lib/db/migrations/20261020T2200_work_claims_watch.ts +10 -0
  183. package/lib/db/migrations/20261021T1400_session_agent_roles.ts +20 -0
  184. package/lib/db/migrations/20261021T1430_analyst_links.ts +52 -0
  185. package/lib/db/queries.ts +0 -40
  186. package/lib/db/session-parts/columns.ts +3 -0
  187. package/lib/db/types.ts +19 -8
  188. package/lib/doctor/checks.ts +64 -1
  189. package/lib/done/archive.ts +2 -0
  190. package/lib/drafts.ts +4 -0
  191. package/lib/export/build.ts +470 -0
  192. package/lib/export/compact.ts +156 -0
  193. package/lib/export/estimate.ts +83 -0
  194. package/lib/export/fixtures.ts +45 -0
  195. package/lib/export/html.tsx +195 -0
  196. package/lib/export/json.ts +39 -0
  197. package/lib/export/load.ts +64 -0
  198. package/lib/export/markdown.ts +127 -0
  199. package/lib/export/request.ts +36 -0
  200. package/lib/export/service.ts +142 -0
  201. package/lib/export/summary.ts +86 -0
  202. package/lib/export/turns.ts +63 -0
  203. package/lib/export/view.ts +173 -0
  204. package/lib/git-remote.ts +43 -0
  205. package/lib/hosts/index.ts +10 -1
  206. package/lib/models/agent-binding.ts +102 -0
  207. package/lib/models/agent-roles-view.ts +157 -0
  208. package/lib/models/agent-roles.ts +87 -0
  209. package/lib/models/availability-refs.ts +17 -0
  210. package/lib/models/availability.ts +91 -0
  211. package/lib/models/catalog.ts +156 -0
  212. package/lib/models/config.ts +27 -0
  213. package/lib/models/locality.ts +33 -0
  214. package/lib/models/providers.ts +11 -0
  215. package/lib/models/resolve.ts +50 -12
  216. package/lib/models/roles.ts +12 -0
  217. package/lib/needs-you.ts +1 -1
  218. package/lib/orchestration.ts +2 -0
  219. package/lib/orchestrator/act.ts +54 -4
  220. package/lib/orchestrator/brief.ts +2 -1
  221. package/lib/orchestrator/claim-wait.ts +71 -0
  222. package/lib/orchestrator/conditions.ts +28 -1
  223. package/lib/orchestrator/describe.ts +6 -1
  224. package/lib/orchestrator/facts.ts +12 -2
  225. package/lib/orchestrator/read.ts +5 -1
  226. package/lib/orchestrator/serve.ts +37 -2
  227. package/lib/orchestrator/tool-names.ts +1 -0
  228. package/lib/orchestrator/tool-schemas.ts +31 -1
  229. package/lib/orchestrator/tools.ts +3 -1
  230. package/lib/orchestrator/untrusted.ts +5 -0
  231. package/lib/orchestrator/watcher.ts +19 -1
  232. package/lib/peer/account.ts +74 -2
  233. package/lib/peer/allowlist.ts +1 -1
  234. package/lib/peer/client.ts +26 -1
  235. package/lib/peer/link-cli.ts +19 -4
  236. package/lib/peer/links.ts +117 -5
  237. package/lib/peer/machines.ts +14 -2
  238. package/lib/peer/name.ts +32 -0
  239. package/lib/peer/pairing.ts +34 -5
  240. package/lib/peer/pin.ts +10 -3
  241. package/lib/peer/reasons.ts +7 -0
  242. package/lib/peer/revoke.ts +75 -0
  243. package/lib/peer/rpc-server.ts +53 -5
  244. package/lib/peer/unlink.ts +62 -0
  245. package/lib/peer/unlinked-proof.ts +44 -0
  246. package/lib/projects.ts +6 -2
  247. package/lib/security/demo.ts +7 -0
  248. package/lib/session-artifacts/agentos-files.ts +94 -0
  249. package/lib/session-artifacts/collect.ts +17 -0
  250. package/lib/session-groups/assignSessions.ts +81 -0
  251. package/lib/session-groups/cli.ts +61 -0
  252. package/lib/session-groups/createGroup.ts +28 -0
  253. package/lib/session-groups/index.ts +21 -0
  254. package/lib/session-groups/removeGroup.ts +36 -0
  255. package/lib/session-groups/reorderGroups.ts +31 -0
  256. package/lib/session-groups/resolveGroup.ts +19 -0
  257. package/lib/session-groups/route.ts +32 -0
  258. package/lib/session-groups/store.ts +85 -0
  259. package/lib/session-groups/types.ts +25 -0
  260. package/lib/session-groups/updateGroup.ts +35 -0
  261. package/lib/session-groups/workspace.ts +21 -0
  262. package/lib/sessions/crew.ts +72 -0
  263. package/lib/sessions/launch.ts +54 -11
  264. package/lib/settings-url.ts +1 -0
  265. package/lib/share/markdown.ts +2 -2
  266. package/lib/share/redact.ts +16 -10
  267. package/lib/share/render.tsx +1 -1
  268. package/lib/share/snapshot.ts +2 -2
  269. package/lib/sidebar/rollup.ts +1 -0
  270. package/lib/sidebar/row-menu.ts +8 -1
  271. package/lib/sidebar/shelves.ts +90 -15
  272. package/lib/sidebar/visible-order.ts +5 -0
  273. package/lib/tasks/hold-checks.ts +3 -1
  274. package/lib/tasks/holds.ts +3 -0
  275. package/lib/tasks/index.ts +11 -0
  276. package/lib/tasks/queue-rows.ts +8 -1
  277. package/lib/tasks/queue-start.ts +1 -0
  278. package/lib/tasks/queue-types.ts +9 -1
  279. package/lib/tasks/queue.ts +15 -5
  280. package/lib/tasks/schedule-types.ts +1 -0
  281. package/lib/test-temp.ts +19 -3
  282. package/lib/workspaces.ts +11 -3
  283. package/lib/worktree-deps/caches.ts +3 -2
  284. package/lib/worktree-deps/clone.ts +3 -1
  285. package/lib/worktree-deps/paths.ts +26 -0
  286. package/lib/worktree-sweep/testTemp.ts +23 -106
  287. package/package.json +1 -1
  288. package/scripts/agent-os +1 -1
  289. package/scripts/autodeploy +2 -0
  290. package/scripts/autodeploy.test.ts +31 -1
  291. package/scripts/claim-hook.sh +31 -0
  292. package/scripts/claim-hook.test.ts +230 -0
  293. package/scripts/lib/cli-links.sh +36 -0
  294. package/scripts/lib/commands.sh +3 -0
  295. package/scripts/lib/machine.sh +27 -2
  296. package/scripts/lib/machine.test.ts +25 -3
  297. package/scripts/sync-cli-links +7 -0
  298. package/scripts/sync-cli-links.test.ts +69 -0
  299. package/server.ts +12 -1
  300. package/stores/claimsUi.ts +22 -0
  301. package/stores/exportUi.ts +87 -0
  302. package/app/api/groups/[...path]/route.ts +0 -136
  303. package/app/api/groups/route.ts +0 -93
  304. package/data/groups/index.ts +0 -1
  305. package/data/groups/mutations.ts +0 -95
  306. package/hooks/useGroups.ts +0 -37
@@ -0,0 +1,86 @@
1
+ import { addHits, noHits } from "../share/redact";
2
+ import type { Counts, ExportDoc, ExportTurn } from "./build";
3
+ import { inRange, resolveRange } from "./turns";
4
+ import {
5
+ maskedList,
6
+ type ExportMode,
7
+ type ExportRange,
8
+ type ExportRedact,
9
+ type Hits,
10
+ } from "./view";
11
+
12
+ export interface Selected {
13
+ doc: ExportDoc;
14
+ turns: ExportTurn[];
15
+ sel: { from: number; to: number; of: number };
16
+ hits: Hits;
17
+ omissions: Counts;
18
+ truncations: Counts;
19
+ }
20
+
21
+ export interface ExportInfo {
22
+ mode: ExportMode;
23
+ redact: ExportRedact;
24
+ generatedAt: string;
25
+ }
26
+
27
+ const sum = (parts: Counts[]) => {
28
+ const out: Counts = {};
29
+ for (const p of parts)
30
+ for (const [k, v] of Object.entries(p)) out[k] = (out[k] ?? 0) + v;
31
+ return out;
32
+ };
33
+
34
+ /** The turns a range means, with the counts for just those. */
35
+ export function selectTurns(doc: ExportDoc, range?: ExportRange): Selected {
36
+ const sel = resolveRange(doc.turns, range);
37
+ const turns = doc.turns.filter((t) => inRange(t.n, sel));
38
+ return {
39
+ doc,
40
+ turns,
41
+ sel,
42
+ hits: turns.reduce((h, t) => addHits(h, t.hits), doc.metaHits),
43
+ omissions: sum(turns.map((t) => t.omitted)),
44
+ truncations: sum(turns.map((t) => t.truncated)),
45
+ };
46
+ }
47
+
48
+ const plural = (n: number, what: string) => `${n} ${what}${n === 1 ? "" : "s"}`;
49
+
50
+ export const countList = (c: Counts) =>
51
+ Object.entries(c)
52
+ .filter(([, n]) => n > 0)
53
+ .map(([what, n]) => plural(n, what))
54
+ .join(", ");
55
+
56
+ const NOTHING = noHits();
57
+ export { NOTHING };
58
+
59
+ /** The lines every format prints under "Export notes". */
60
+ export function exportNotes(s: Selected, info: ExportInfo) {
61
+ const { from, to, of } = s.sel;
62
+ const n = s.turns.filter((t) => t.n > 0).length;
63
+ const first = s.turns[0]?.startedAt.slice(0, 16).replace("T", " ");
64
+ const last = s.turns[s.turns.length - 1]?.endedAt
65
+ .slice(0, 16)
66
+ .replace("T", " ");
67
+ const whole = from <= 1 && to >= of;
68
+ const range = whole
69
+ ? `Whole session, ${plural(of, "turn")}${first ? `, ${first} to ${last}` : ""}`
70
+ : `Turns ${from}–${to} of ${of} (${plural(n, "turn")}), ${first} to ${last}`;
71
+ const masked = maskedList(s.hits);
72
+ return {
73
+ range,
74
+ detail:
75
+ info.mode === "compact"
76
+ ? "Compact: thinking dropped; tool output cut to its first 10 and last 20 lines; file changes reduced to counts"
77
+ : "Full: nothing dropped except what the notes below list",
78
+ redaction:
79
+ (info.redact === "all"
80
+ ? "Credentials, home paths, addresses, machine names and emails are masked"
81
+ : "Credentials are masked; paths, machine names and emails are included") +
82
+ (masked ? `. Masked: ${masked}` : ""),
83
+ omitted: countList(s.omissions) || "nothing",
84
+ truncated: countList(s.truncations) || "nothing",
85
+ };
86
+ }
@@ -0,0 +1,63 @@
1
+ import type { ChatItem } from "../chat/events";
2
+ import { BadShare } from "../share/service";
3
+ import type { ExportRange } from "./view";
4
+
5
+ export interface Turn {
6
+ /** 1-based; 0 is whatever came before the first message. */
7
+ n: number;
8
+ items: ChatItem[];
9
+ }
10
+
11
+ /**
12
+ * Items (already `visible()`, in stored order) as turns. A turn is a user
13
+ * message and everything until the next one; a message sent while the agent
14
+ * was working stays inside the turn it landed in.
15
+ */
16
+ export function groupTurns(items: ChatItem[]): Turn[] {
17
+ const turns: Turn[] = [];
18
+ let n = 0;
19
+ for (const item of items) {
20
+ const starts = item.kind === "user" && !item.midTurn;
21
+ if (starts) {
22
+ n++;
23
+ turns.push({ n, items: [] });
24
+ } else if (turns.length === 0) {
25
+ turns.push({ n: 0, items: [] });
26
+ }
27
+ turns[turns.length - 1].items.push(item);
28
+ }
29
+ return turns;
30
+ }
31
+
32
+ type Numbered = { n: number; items: { kind: string }[] };
33
+
34
+ export const turnCount = (turns: Numbered[]) =>
35
+ turns.filter((t) => t.n > 0).length;
36
+
37
+ /** The inclusive turn numbers a range means, within 1..of. Throws BadShare. */
38
+ export function resolveRange(
39
+ turns: Numbered[],
40
+ range: ExportRange | undefined
41
+ ): { from: number; to: number; of: number } {
42
+ const of = turnCount(turns);
43
+ if (!range || of === 0) return { from: 1, to: Math.max(of, 1), of };
44
+ if ("last" in range) {
45
+ const k = Math.max(1, Math.min(of, Math.floor(range.last)));
46
+ return { from: of - k + 1, to: of, of };
47
+ }
48
+ if ("sinceCompaction" in range) {
49
+ let at = 0;
50
+ for (const t of turns)
51
+ if (t.items.some((i) => i.kind === "compacted")) at = Math.max(t.n, 1);
52
+ return { from: at || 1, to: of, of };
53
+ }
54
+ const { from, to } = range;
55
+ if (from < 1 || from > of)
56
+ throw new BadShare(`range starts at turn ${from}, there are ${of}`, 400);
57
+ if (to < from) throw new BadShare("range ends before it starts", 400);
58
+ return { from, to: Math.min(to, of), of };
59
+ }
60
+
61
+ /** Turn 0 (the start) rides along with a range that begins at turn 1. */
62
+ export const inRange = (n: number, sel: { from: number; to: number }) =>
63
+ n === 0 ? sel.from <= 1 : n >= sel.from && n <= sel.to;
@@ -0,0 +1,173 @@
1
+ // Wire types and helpers for session export. Imports nothing outside itself
2
+ // (bar the share helpers it reuses) so the sheet and the native app can use it.
3
+
4
+ import { fileName, formatBytes } from "../share/view";
5
+
6
+ export { formatBytes };
7
+
8
+ export type ExportFormat = "md" | "json" | "html";
9
+ export type ExportMode = "full" | "compact";
10
+ /** "all": mask paths, hosts and emails too. "secrets": credentials only. */
11
+ export type ExportRedact = "all" | "secrets";
12
+
13
+ export type ExportRange =
14
+ { from: number; to: number } | { last: number } | { sinceCompaction: true };
15
+
16
+ export interface ExportRequest {
17
+ format: ExportFormat;
18
+ mode: ExportMode;
19
+ range?: ExportRange;
20
+ redact: ExportRedact;
21
+ }
22
+
23
+ export const DEFAULT_REQUEST: ExportRequest = {
24
+ format: "md",
25
+ mode: "compact",
26
+ redact: "all",
27
+ };
28
+
29
+ export const EXPORT_FORMATS: {
30
+ id: ExportFormat;
31
+ label: string;
32
+ hint: string;
33
+ ext: string;
34
+ mime: string;
35
+ }[] = [
36
+ {
37
+ id: "md",
38
+ label: "Markdown",
39
+ hint: "Best for pasting into a chat",
40
+ ext: "md",
41
+ mime: "text/markdown; charset=utf-8",
42
+ },
43
+ {
44
+ id: "json",
45
+ label: "JSON",
46
+ hint: "For scripts and tools",
47
+ ext: "json",
48
+ mime: "application/json",
49
+ },
50
+ {
51
+ id: "html",
52
+ label: "HTML",
53
+ hint: "To read or upload as a page",
54
+ ext: "html",
55
+ mime: "text/html; charset=utf-8",
56
+ },
57
+ ];
58
+
59
+ export const formatInfo = (f: ExportFormat) =>
60
+ EXPORT_FORMATS.find((x) => x.id === f)!;
61
+
62
+ export const MAX_EXPORT_BYTES = 25 * 1024 * 1024;
63
+ export const WARN_TOKENS = 100_000;
64
+ export const DANGER_TOKENS = 1_000_000;
65
+ export const TOO_BIG =
66
+ "That export is over 25 MB. Narrow the range or use Compact.";
67
+
68
+ export type Hits = Record<"secret" | "path" | "ip" | "host" | "email", number>;
69
+
70
+ export interface ExportTurnInfo {
71
+ /** 0 is the part before the first message. */
72
+ n: number;
73
+ at: string;
74
+ /** The message that began the turn. */
75
+ id?: string;
76
+ excerpt: string;
77
+ items: number;
78
+ tokens: number;
79
+ }
80
+
81
+ export interface ExportWarning {
82
+ level: "info" | "warn" | "danger";
83
+ code:
84
+ | "sensitive"
85
+ | "paths-included"
86
+ | "full-output"
87
+ | "secrets-found"
88
+ | "large"
89
+ | "huge"
90
+ | "too-big";
91
+ text: string;
92
+ }
93
+
94
+ export interface Size {
95
+ bytes: number;
96
+ tokens: number;
97
+ }
98
+
99
+ export interface ExportEstimate {
100
+ title: string;
101
+ turns: ExportTurnInfo[];
102
+ selected: {
103
+ from: number;
104
+ to: number;
105
+ of: number;
106
+ turns: number;
107
+ startedAt: string | null;
108
+ endedAt: string | null;
109
+ };
110
+ /** The chosen format, in each mode. */
111
+ estimate: { full: Size; compact: Size };
112
+ hits: Hits;
113
+ omissions: Record<string, number>;
114
+ truncations: Record<string, number>;
115
+ warnings: ExportWarning[];
116
+ tooBig: boolean;
117
+ }
118
+
119
+ /** "all", "turn-12", "turns-12-18" for a file name. */
120
+ export function rangeSlug(sel: { from: number; to: number; of: number }) {
121
+ if (sel.from <= 1 && sel.to >= sel.of) return "all";
122
+ return sel.from === sel.to
123
+ ? `turn-${sel.from}`
124
+ : `turns-${sel.from}-${sel.to}`;
125
+ }
126
+
127
+ export function exportFileName(
128
+ title: string,
129
+ slug: string,
130
+ format: ExportFormat
131
+ ): string {
132
+ const ext = formatInfo(format).ext;
133
+ const base = fileName(title, "md").replace(/\.md$/, "");
134
+ return `${base}-${slug}.${ext}`;
135
+ }
136
+
137
+ /** "about 24k tokens" */
138
+ export function tokenLabel(tokens: number): string {
139
+ if (tokens < 1000) return `about ${tokens} tokens`;
140
+ if (tokens < 1_000_000) return `about ${Math.round(tokens / 1000)}k tokens`;
141
+ return `about ${(tokens / 1_000_000).toFixed(1)}M tokens`;
142
+ }
143
+
144
+ const KINDS: Record<keyof Hits, [string, string]> = {
145
+ secret: ["credential", "credentials"],
146
+ path: ["home path", "home paths"],
147
+ ip: ["address", "addresses"],
148
+ host: ["machine name", "machine names"],
149
+ email: ["email", "emails"],
150
+ };
151
+
152
+ /** "3 credentials, 88 home paths", or "" when nothing was masked. */
153
+ export function maskedList(hits: Hits): string {
154
+ return (Object.keys(KINDS) as (keyof Hits)[])
155
+ .filter((k) => hits[k] > 0)
156
+ .map((k) => `${hits[k]} ${KINDS[k][hits[k] === 1 ? 0 : 1]}`)
157
+ .join(", ");
158
+ }
159
+
160
+ export function redactionStatus(hits: Hits): string {
161
+ const list = maskedList(hits);
162
+ return list ? `Masked ${list}` : "Nothing needed masking";
163
+ }
164
+
165
+ export const COMPACT_RULES = [
166
+ "Thinking blocks are dropped (counted).",
167
+ "Tool and command output keeps the first 10 and last 20 lines, 2,000 characters at most.",
168
+ "Tool input is clipped to 300 characters; a shell command to 1,000.",
169
+ "File changes become the path and +added −removed.",
170
+ "Only the last to-do list of each turn is kept.",
171
+ "Pages the agent showed keep their title only.",
172
+ "A pasted block over 8,000 characters keeps its start and end.",
173
+ ];
package/lib/git-remote.ts CHANGED
@@ -12,6 +12,36 @@ export function slugOfRemote(url: string | null | undefined): string | null {
12
12
  return m ? `${m[1]}/${m[2]}`.toLowerCase() : null;
13
13
  }
14
14
 
15
+ // A repository's key across clones and URL forms: ssh and https of the same
16
+ // remote give the same key, with no scheme, credentials, port or ".git".
17
+ // "host/path", lower case; null for anything that isn't a remote URL.
18
+ export function repoKeyOfRemote(url: string | null | undefined): string | null {
19
+ const raw = (url ?? "").trim();
20
+ if (!raw) return null;
21
+ let host = "";
22
+ let path = "";
23
+ const scp = /^(?:[^@/:\s]+@)?([^/:\s]+):(?!\/\/)(.+)$/.exec(raw);
24
+ if (scp && !/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) {
25
+ host = scp[1];
26
+ path = scp[2];
27
+ } else {
28
+ try {
29
+ const u = new URL(raw);
30
+ if (!/^(https?|ssh|git):$/.test(u.protocol)) return null;
31
+ host = u.hostname;
32
+ path = u.pathname;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ path = path
38
+ .replace(/^\/+|\/+$/g, "")
39
+ .replace(/\.git$/i, "")
40
+ .toLowerCase();
41
+ host = host.toLowerCase();
42
+ return host && path.includes("/") ? `${host}/${path}` : null;
43
+ }
44
+
15
45
  // A remote's URL doesn't change under a running server often enough to ask
16
46
  // git on every lookup.
17
47
  const SLUG_TTL_MS = 5 * 60 * 1000;
@@ -45,3 +75,16 @@ export function repoSlugSync(dir: string): string | null {
45
75
  slugs.set(dir, { at: Date.now(), slug });
46
76
  return slug;
47
77
  }
78
+
79
+ const keys = new Map<string, { at: number; key: string | null }>();
80
+
81
+ export async function repoKey(dir: string): Promise<string | null> {
82
+ const hit = keys.get(dir);
83
+ if (hit && Date.now() - hit.at < SLUG_TTL_MS) return hit.key;
84
+ const url = await git(dir, ["config", "--get", "remote.origin.url"], {
85
+ timeout: 10000,
86
+ }).catch(() => "");
87
+ const key = repoKeyOfRemote(url);
88
+ keys.set(dir, { at: Date.now(), key });
89
+ return key;
90
+ }
@@ -29,7 +29,16 @@ export function listRemoteHosts(): Host[] {
29
29
  `SELECT h.*, CASE WHEN h.kind = 'connect'
30
30
  THEN EXISTS (SELECT 1 FROM peer_links p WHERE p.host_id = h.id)
31
31
  ELSE EXISTS (SELECT 1 FROM host_links l WHERE l.host_id = h.id)
32
- END AS linked
32
+ END AS linked,
33
+ CASE WHEN h.kind = 'connect' THEN COALESCE(
34
+ (SELECT p.state FROM peer_links p WHERE p.host_id = h.id),
35
+ (SELECT 'unlinked' FROM peer_unlinked u WHERE u.host_id = h.id LIMIT 1))
36
+ END AS peer_state,
37
+ CASE WHEN h.kind = 'connect' THEN COALESCE(
38
+ (SELECT p.suspended_reason FROM peer_links p WHERE p.host_id = h.id),
39
+ (SELECT u.reason FROM peer_unlinked u WHERE u.host_id = h.id
40
+ ORDER BY u.at DESC LIMIT 1))
41
+ END AS peer_note
33
42
  FROM hosts h ORDER BY sort_order, created_at`
34
43
  )
35
44
  .all() as Host[];
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Starting a session in an agent role: which model its binding gives it,
3
+ * whether that model's server answers, and what to run it on. A local model
4
+ * that's down never turns into a cloud one by itself: only a fallback the
5
+ * user set for the role, and only when the start asks for it.
6
+ */
7
+
8
+ import { AGENT_ROLE_DEFS, type AgentRole } from "./agent-roles";
9
+ import { modelAvailability, choiceFor } from "./catalog";
10
+ import { savedModelSettings } from "./config";
11
+ import { PI } from "./providers";
12
+ import { resolveRole, type RoleSource, type RoutedModel } from "./resolve";
13
+
14
+ export interface RoleBinding {
15
+ role: AgentRole;
16
+ routed: RoutedModel;
17
+ // The user's fallback for the role, if set.
18
+ fallback?: string;
19
+ }
20
+
21
+ export function bindingFor(
22
+ role: AgentRole,
23
+ opts: {
24
+ workspaceId?: string | null;
25
+ projectId?: string | null;
26
+ // The model picked for this one session.
27
+ override?: string | null;
28
+ } = {}
29
+ ): RoleBinding {
30
+ const modelRole = AGENT_ROLE_DEFS[role].modelRole;
31
+ const routed = resolveRole(modelRole, { ...opts, allowPi: true });
32
+ return {
33
+ role,
34
+ routed,
35
+ fallback: savedModelSettings().fallbacks?.[modelRole],
36
+ };
37
+ }
38
+
39
+ export class ModelUnavailableError extends Error {
40
+ readonly code = "model_unavailable";
41
+ constructor(
42
+ readonly role: AgentRole,
43
+ readonly ref: string,
44
+ readonly detail: string,
45
+ readonly fallback?: string
46
+ ) {
47
+ super(
48
+ `${AGENT_ROLE_DEFS[role].label}'s model ${ref} isn't answering${detail ? ` (${detail})` : ""}`
49
+ );
50
+ }
51
+ }
52
+
53
+ export interface RoleStart {
54
+ role: AgentRole;
55
+ routed: RoutedModel;
56
+ // "pi" runs the Pi agent on the model; "claude" runs Claude Code.
57
+ agentType: "claude" | "pi";
58
+ // What the agent CLI is given as its model.
59
+ model: string;
60
+ source: RoleSource | "fallback";
61
+ }
62
+
63
+ const startFrom = (
64
+ role: AgentRole,
65
+ routed: RoutedModel,
66
+ source: RoleStart["source"]
67
+ ): RoleStart =>
68
+ routed.provider.id === PI
69
+ ? {
70
+ role,
71
+ routed,
72
+ agentType: "pi",
73
+ model: routed.ref.slice(PI.length + 1),
74
+ source,
75
+ }
76
+ : { role, routed, agentType: "claude", model: routed.model, source };
77
+
78
+ // The model a session started in `role` runs on, after checking its server.
79
+ export async function prepareRoleStart(
80
+ role: AgentRole,
81
+ opts: Parameters<typeof bindingFor>[1] & { useFallback?: boolean }
82
+ ): Promise<RoleStart> {
83
+ const binding = bindingFor(role, opts);
84
+ const state = await modelAvailability(binding.routed.ref);
85
+ if (state.state !== "down")
86
+ return startFrom(role, binding.routed, binding.routed.source);
87
+ if (opts.useFallback && binding.fallback) {
88
+ const alt = bindingFor(role, { ...opts, override: binding.fallback });
89
+ const altState = await modelAvailability(alt.routed.ref);
90
+ if (altState.state !== "down")
91
+ return startFrom(role, alt.routed, "fallback");
92
+ }
93
+ throw new ModelUnavailableError(
94
+ role,
95
+ binding.routed.ref,
96
+ state.error ?? "",
97
+ binding.fallback &&
98
+ choiceFor(binding.fallback)?.availability.state !== "down"
99
+ ? binding.fallback
100
+ : undefined
101
+ );
102
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * What the Agent roles screen and the native app show: each role, the model
3
+ * saved at a scope, the model in force there and where it came from, and
4
+ * what the server knows of that model (local or cloud, answering or not).
5
+ * Saving checks what a role may run on.
6
+ */
7
+
8
+ import {
9
+ AGENT_ROLES,
10
+ AGENT_ROLE_DEFS,
11
+ INTERNAL_MODEL_ROLES,
12
+ type AgentRole,
13
+ } from "./agent-roles";
14
+ import { choiceFor, type ModelChoice } from "./catalog";
15
+ import {
16
+ effectiveModels,
17
+ projectModels,
18
+ savedModelSettings,
19
+ setModelSettings,
20
+ setProjectModels,
21
+ setWorkspaceModels,
22
+ workspaceModels,
23
+ type RoleMap,
24
+ } from "./config";
25
+ import { PI } from "./providers";
26
+ import { resolveRole, type RoleSource } from "./resolve";
27
+ import {
28
+ DEFAULT_ROLE_MODELS,
29
+ GATE_ROLES,
30
+ MODEL_ROLES,
31
+ ROLE_LABELS,
32
+ parseModelRef,
33
+ type ModelRole,
34
+ } from "./roles";
35
+
36
+ export type RoleScope = "settings" | "workspace" | "project";
37
+ export interface ScopeRef {
38
+ scope: RoleScope;
39
+ id?: string | null;
40
+ }
41
+
42
+ export interface RoleView {
43
+ modelRole: ModelRole;
44
+ label: string;
45
+ hint: string;
46
+ // Saved at this scope, or null when it inherits.
47
+ value: string | null;
48
+ binding: { ref: string; source: RoleSource } | null;
49
+ fallback: string | null;
50
+ choice: ModelChoice | null;
51
+ // Why the binding can't be used (a provider turned off).
52
+ error?: string;
53
+ default: string;
54
+ }
55
+
56
+ export interface AgentRoleView extends RoleView {
57
+ id: AgentRole;
58
+ purpose: string;
59
+ brief: string;
60
+ edits: boolean;
61
+ }
62
+
63
+ function savedAt({ scope, id }: ScopeRef): RoleMap {
64
+ if (scope === "workspace" && id) return workspaceModels(id).roles ?? {};
65
+ if (scope === "project" && id) return projectModels(id).roles ?? {};
66
+ return savedModelSettings().roles ?? {};
67
+ }
68
+
69
+ function viewOf(modelRole: ModelRole, at: ScopeRef, ctx: ContextIds): RoleView {
70
+ const effective = effectiveModels();
71
+ let binding: RoleView["binding"] = null;
72
+ let error: string | undefined;
73
+ try {
74
+ const r = resolveRole(modelRole, { ...ctx, allowPi: true }, effective);
75
+ binding = { ref: r.ref, source: r.source };
76
+ } catch (e) {
77
+ error = e instanceof Error ? e.message : String(e);
78
+ }
79
+ return {
80
+ modelRole,
81
+ label: ROLE_LABELS[modelRole].label,
82
+ hint: ROLE_LABELS[modelRole].hint,
83
+ value: savedAt(at)[modelRole] ?? null,
84
+ binding,
85
+ fallback: savedModelSettings().fallbacks?.[modelRole] ?? null,
86
+ choice: binding ? (choiceFor(binding.ref, effective) ?? null) : null,
87
+ ...(error ? { error } : {}),
88
+ default: DEFAULT_ROLE_MODELS[modelRole],
89
+ };
90
+ }
91
+
92
+ interface ContextIds {
93
+ workspaceId?: string | null;
94
+ projectId?: string | null;
95
+ }
96
+
97
+ const contextOf = ({ scope, id }: ScopeRef): ContextIds =>
98
+ scope === "workspace"
99
+ ? { workspaceId: id }
100
+ : scope === "project"
101
+ ? { projectId: id }
102
+ : {};
103
+
104
+ export function agentRolesView(at: ScopeRef, ctx?: ContextIds) {
105
+ const ids = ctx ?? contextOf(at);
106
+ return {
107
+ roles: AGENT_ROLES.map((id): AgentRoleView => ({
108
+ ...viewOf(AGENT_ROLE_DEFS[id].modelRole, at, ids),
109
+ id,
110
+ label: AGENT_ROLE_DEFS[id].label,
111
+ hint: AGENT_ROLE_DEFS[id].purpose,
112
+ purpose: AGENT_ROLE_DEFS[id].purpose,
113
+ brief: AGENT_ROLE_DEFS[id].brief,
114
+ edits: AGENT_ROLE_DEFS[id].edits,
115
+ })),
116
+ internal: INTERNAL_MODEL_ROLES(MODEL_ROLES).map((r) => viewOf(r, at, ids)),
117
+ };
118
+ }
119
+
120
+ // The models a role can be bound to: a Pi model runs a role session only,
121
+ // never a task, a gate or the orchestrator.
122
+ const PI_ROLES: readonly ModelRole[] = ["implement", "analyze", "advise"];
123
+
124
+ function checkRoles(roles: RoleMap): void {
125
+ for (const [role, ref] of Object.entries(roles) as [ModelRole, string][]) {
126
+ if (parseModelRef(ref).provider !== PI) continue;
127
+ if (!PI_ROLES.includes(role))
128
+ throw new Error(
129
+ `${ROLE_LABELS[role].label} can't run on a Pi model (${GATE_ROLES.includes(role) ? "a merge gate needs Claude Code" : "only the Developer, Analyst and Reviewer can"})`
130
+ );
131
+ }
132
+ }
133
+
134
+ export function saveAgentRoles(
135
+ at: ScopeRef,
136
+ roles: RoleMap,
137
+ fallbacks?: RoleMap
138
+ ): void {
139
+ checkRoles(roles);
140
+ if (fallbacks) {
141
+ for (const ref of Object.values(fallbacks))
142
+ if (ref && parseModelRef(ref).provider === PI)
143
+ throw new Error("A fallback must be a cloud or Claude Code model");
144
+ }
145
+ if (at.scope === "settings") {
146
+ const saved = savedModelSettings();
147
+ setModelSettings({
148
+ ...saved,
149
+ roles,
150
+ ...(fallbacks ? { fallbacks } : {}),
151
+ });
152
+ return;
153
+ }
154
+ if (!at.id) throw new Error(`${at.scope} needs an id`);
155
+ if (at.scope === "workspace") setWorkspaceModels(at.id, { roles });
156
+ else setProjectModels(at.id, { roles });
157
+ }