@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.
- package/README.md +2 -0
- package/app/api/agent-roles/route.test.ts +67 -0
- package/app/api/agent-roles/route.ts +61 -0
- package/app/api/analyst/[id]/recommendations/[recId]/again/route.ts +15 -0
- package/app/api/analyst/[id]/recommendations/[recId]/dismiss/route.ts +13 -0
- package/app/api/analyst/[id]/recommendations/[recId]/send/route.ts +13 -0
- package/app/api/analyst/[id]/review/route.ts +22 -0
- package/app/api/analyst/[id]/route.ts +44 -0
- package/app/api/analyst/route.ts +10 -0
- package/app/api/bus/groups/route.ts +44 -0
- package/app/api/bus/spawn/route.ts +34 -2
- package/app/api/claims/[id]/route.ts +20 -0
- package/app/api/claims/check/route.ts +23 -0
- package/app/api/claims/check.test.ts +113 -0
- package/app/api/claims/exchange/route.ts +42 -0
- package/app/api/claims/outside.test.ts +90 -0
- package/app/api/claims/route.test.ts +193 -0
- package/app/api/claims/route.ts +73 -0
- package/app/api/hosts/[id]/route.ts +9 -1
- package/app/api/models/availability/route.ts +9 -0
- package/app/api/models/choices/route.ts +9 -0
- package/app/api/peer/unlink/route.ts +17 -0
- package/app/api/peers/[hostId]/audit/route.ts +14 -0
- package/app/api/peers/[hostId]/route.ts +20 -0
- package/app/api/peers/gate.test.ts +64 -1
- package/app/api/peers/pending/route.test.ts +37 -0
- package/app/api/peers/pending/route.ts +11 -4
- package/app/api/session-groups/[id]/restore/route.ts +11 -0
- package/app/api/session-groups/[id]/route.ts +25 -0
- package/app/api/session-groups/order/route.ts +15 -0
- package/app/api/session-groups/route.ts +21 -0
- package/app/api/sessions/[id]/analyst/route.ts +11 -0
- package/app/api/sessions/[id]/crew/route.ts +38 -0
- package/app/api/sessions/[id]/export/estimate/route.ts +23 -0
- package/app/api/sessions/[id]/export/route.test.ts +125 -0
- package/app/api/sessions/[id]/export/route.ts +34 -0
- package/app/api/sessions/[id]/fork/route.ts +3 -0
- package/app/api/sessions/[id]/group/route.ts +18 -0
- package/app/api/sessions/[id]/model/route.ts +49 -0
- package/app/api/sessions/[id]/route.ts +14 -4
- package/app/api/sessions/[id]/summarize/route.ts +2 -0
- package/app/api/sessions/claims.test.ts +122 -0
- package/app/api/sessions/group/route.ts +17 -0
- package/app/api/sessions/route.ts +31 -14
- package/app/page.tsx +10 -0
- package/components/Chat/Messages.tsx +104 -10
- package/components/Claims/ClaimFiles.tsx +38 -0
- package/components/Claims/ClaimRow.tsx +82 -0
- package/components/Claims/ClaimsSheet.tsx +176 -0
- package/components/Claims/OverlapPill.tsx +31 -0
- package/components/Claims/StartClaimsNotice.tsx +70 -0
- package/components/Claims/claims-ui.test.ts +161 -0
- package/components/Claims/format.ts +57 -0
- package/components/Claims/index.ts +3 -0
- package/components/Draft/DraftChips.tsx +6 -1
- package/components/Draft/DraftPanel.tsx +90 -3
- package/components/Draft/ModelUnavailableDialog.tsx +78 -0
- package/components/Draft/RoleChips.tsx +110 -0
- package/components/Export/ExportActions.tsx +136 -0
- package/components/Export/ExportSheet.tsx +217 -0
- package/components/Export/RangePicker.tsx +144 -0
- package/components/Export/Seg.tsx +38 -0
- package/components/Export/TurnSelectBar.tsx +45 -0
- package/components/Hosts/AccountMachines.tsx +32 -18
- package/components/Hosts/HostRow.tsx +155 -14
- package/components/Hosts/IncomingLinkSheet.tsx +28 -10
- package/components/Hosts/PeerActivitySheet.tsx +111 -0
- package/components/Models/AgentRoleCard.tsx +114 -0
- package/components/Models/AgentRolesSection.tsx +132 -0
- package/components/Models/ModelBadges.tsx +48 -0
- package/components/Models/ModelsSection.tsx +4 -2
- package/components/Models/RoleModelSelect.tsx +87 -0
- package/components/Pane/DesktopTabBar.tsx +2 -0
- package/components/Pane/MobileTabBar.tsx +12 -0
- package/components/Pane/PaneMenu.tsx +10 -0
- package/components/SessionList/GroupShelves.tsx +195 -0
- package/components/SessionList/MoveToGroupMenu.tsx +105 -0
- package/components/SessionList/RowSubtitle.tsx +7 -1
- package/components/SessionList/SelectionToolbar.tsx +52 -1
- package/components/SessionList/SessionRow.tsx +19 -7
- package/components/SessionList/SessionRowMenu.tsx +23 -0
- package/components/SessionList/SessionShelves.tsx +5 -0
- package/components/SessionList/SidebarBody.tsx +6 -0
- package/components/SessionList/SidebarContextMenu.test.ts +104 -0
- package/components/SessionList/SidebarContextMenu.tsx +59 -0
- package/components/SessionList/SidebarHeader.tsx +5 -0
- package/components/SessionList/WorkspaceSwitcher.tsx +23 -6
- package/components/SessionList/index.tsx +18 -14
- package/components/SessionList/useSidebarData.ts +10 -0
- package/components/Sessions/RolePill.tsx +196 -0
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Share/ShareDialog.tsx +13 -0
- package/components/ui/notice.tsx +46 -0
- package/components/ui/status-pill.tsx +39 -0
- package/data/analyst/index.ts +3 -0
- package/data/analyst/keys.ts +6 -0
- package/data/analyst/mutations.ts +116 -0
- package/data/analyst/queries.ts +35 -0
- package/data/claims/index.ts +2 -0
- package/data/claims/keys.ts +5 -0
- package/data/claims/queries.ts +47 -0
- package/data/exports/index.ts +2 -0
- package/data/exports/keys.ts +3 -0
- package/data/exports/queries.ts +51 -0
- package/data/models/index.ts +98 -0
- package/data/peers/keys.ts +1 -0
- package/data/peers/queries.ts +31 -0
- package/data/push/topics.ts +9 -0
- package/data/session-groups/index.ts +156 -0
- package/data/sessions/index.ts +0 -1
- package/data/sessions/pin.ts +2 -6
- package/data/sessions/queries.ts +36 -29
- package/data/sessions/shelf.ts +2 -6
- package/data/tasks/index.ts +2 -0
- package/hooks/useSessions.ts +0 -12
- package/lib/__fixtures__/peer-rpc.ts +65 -33
- package/lib/agents/brief.ts +1 -0
- package/lib/agents/spawn.ts +4 -0
- package/lib/analyst/__fixtures__/items.ts +57 -0
- package/lib/analyst/__fixtures__/mocks.ts +49 -0
- package/lib/analyst/brief.ts +17 -0
- package/lib/analyst/buildDigest.ts +204 -0
- package/lib/analyst/createLink.ts +98 -0
- package/lib/analyst/endLink.ts +31 -0
- package/lib/analyst/forward.ts +109 -0
- package/lib/analyst/index.ts +16 -0
- package/lib/analyst/linkView.ts +38 -0
- package/lib/analyst/onTurnEnd.ts +119 -0
- package/lib/analyst/parseReview.ts +41 -0
- package/lib/analyst/pushDigest.ts +143 -0
- package/lib/analyst/resume.ts +35 -0
- package/lib/analyst/route.ts +50 -0
- package/lib/analyst/signals.ts +121 -0
- package/lib/analyst/store.ts +303 -0
- package/lib/analyst/types.ts +140 -0
- package/lib/bus/condense.ts +5 -1
- package/lib/bus/index.ts +13 -0
- package/lib/bus/read-scope.ts +12 -0
- package/lib/bus/role-alias.ts +70 -0
- package/lib/bus/targets.ts +3 -0
- package/lib/chat/drivers/opencode-mapper.ts +55 -1
- package/lib/chat/drivers/pi-approvals.ts +7 -4
- package/lib/chat/drivers/pi-mapper.ts +38 -0
- package/lib/chat/drivers/pi-text-tools.ts +104 -0
- package/lib/chat/drivers/pi.ts +8 -3
- package/lib/chat/drivers/turn-items.ts +36 -11
- package/lib/chat/drivers/turn-outcome.ts +108 -0
- package/lib/chat/events.ts +8 -0
- package/lib/chat/registry.ts +13 -0
- package/lib/chat/store.ts +19 -0
- package/lib/chat/turns.ts +1 -0
- package/lib/chat/worker/extras.ts +4 -2
- package/lib/claims/collectLocal.ts +123 -0
- package/lib/claims/describe.ts +41 -0
- package/lib/claims/exchange.ts +91 -0
- package/lib/claims/goal.ts +45 -0
- package/lib/claims/index.ts +10 -0
- package/lib/claims/loop.ts +51 -0
- package/lib/claims/machine.ts +21 -0
- package/lib/claims/mine.ts +43 -0
- package/lib/claims/notifyOverlaps.ts +101 -0
- package/lib/claims/onMerged.ts +85 -0
- package/lib/claims/outside.ts +36 -0
- package/lib/claims/overlap.ts +87 -0
- package/lib/claims/overlapCache.ts +86 -0
- package/lib/claims/overlapText.ts +36 -0
- package/lib/claims/peers.ts +56 -0
- package/lib/claims/startCheck.ts +100 -0
- package/lib/claims/startWarning.ts +29 -0
- package/lib/claims/store.ts +366 -0
- package/lib/claims/types.ts +102 -0
- package/lib/claims/view.ts +80 -0
- package/lib/claims/yield.ts +24 -0
- package/lib/db/changes.ts +12 -0
- package/lib/db/migrations/20261020T1400_work_claims.ts +35 -0
- package/lib/db/migrations/20261020T1515_peer_unlinked.ts +22 -0
- package/lib/db/migrations/20261020T1700_clean_connect_names.ts +18 -0
- package/lib/db/migrations/20261020T1800_session_groups.ts +49 -0
- package/lib/db/migrations/20261020T1900_task_queue_after_claim.ts +13 -0
- package/lib/db/migrations/20261020T2000_claim_notices.ts +20 -0
- package/lib/db/migrations/20261020T2100_claims_via_host.ts +21 -0
- package/lib/db/migrations/20261020T2200_work_claims_watch.ts +10 -0
- package/lib/db/migrations/20261021T1400_session_agent_roles.ts +20 -0
- package/lib/db/migrations/20261021T1430_analyst_links.ts +52 -0
- package/lib/db/queries.ts +0 -40
- package/lib/db/session-parts/columns.ts +3 -0
- package/lib/db/types.ts +19 -8
- package/lib/doctor/checks.ts +64 -1
- package/lib/done/archive.ts +2 -0
- package/lib/drafts.ts +4 -0
- package/lib/export/build.ts +470 -0
- package/lib/export/compact.ts +156 -0
- package/lib/export/estimate.ts +83 -0
- package/lib/export/fixtures.ts +45 -0
- package/lib/export/html.tsx +195 -0
- package/lib/export/json.ts +39 -0
- package/lib/export/load.ts +64 -0
- package/lib/export/markdown.ts +127 -0
- package/lib/export/request.ts +36 -0
- package/lib/export/service.ts +142 -0
- package/lib/export/summary.ts +86 -0
- package/lib/export/turns.ts +63 -0
- package/lib/export/view.ts +173 -0
- package/lib/git-remote.ts +43 -0
- package/lib/hosts/index.ts +10 -1
- package/lib/models/agent-binding.ts +102 -0
- package/lib/models/agent-roles-view.ts +157 -0
- package/lib/models/agent-roles.ts +87 -0
- package/lib/models/availability-refs.ts +17 -0
- package/lib/models/availability.ts +91 -0
- package/lib/models/catalog.ts +156 -0
- package/lib/models/config.ts +27 -0
- package/lib/models/locality.ts +33 -0
- package/lib/models/providers.ts +11 -0
- package/lib/models/resolve.ts +50 -12
- package/lib/models/roles.ts +12 -0
- package/lib/needs-you.ts +1 -1
- package/lib/orchestration.ts +2 -0
- package/lib/orchestrator/act.ts +54 -4
- package/lib/orchestrator/brief.ts +2 -1
- package/lib/orchestrator/claim-wait.ts +71 -0
- package/lib/orchestrator/conditions.ts +28 -1
- package/lib/orchestrator/describe.ts +6 -1
- package/lib/orchestrator/facts.ts +12 -2
- package/lib/orchestrator/read.ts +5 -1
- package/lib/orchestrator/serve.ts +37 -2
- package/lib/orchestrator/tool-names.ts +1 -0
- package/lib/orchestrator/tool-schemas.ts +31 -1
- package/lib/orchestrator/tools.ts +3 -1
- package/lib/orchestrator/untrusted.ts +5 -0
- package/lib/orchestrator/watcher.ts +19 -1
- package/lib/peer/account.ts +74 -2
- package/lib/peer/allowlist.ts +1 -1
- package/lib/peer/client.ts +26 -1
- package/lib/peer/link-cli.ts +19 -4
- package/lib/peer/links.ts +117 -5
- package/lib/peer/machines.ts +14 -2
- package/lib/peer/name.ts +32 -0
- package/lib/peer/pairing.ts +34 -5
- package/lib/peer/pin.ts +10 -3
- package/lib/peer/reasons.ts +7 -0
- package/lib/peer/revoke.ts +75 -0
- package/lib/peer/rpc-server.ts +53 -5
- package/lib/peer/unlink.ts +62 -0
- package/lib/peer/unlinked-proof.ts +44 -0
- package/lib/projects.ts +6 -2
- package/lib/security/demo.ts +7 -0
- package/lib/session-artifacts/agentos-files.ts +94 -0
- package/lib/session-artifacts/collect.ts +17 -0
- package/lib/session-groups/assignSessions.ts +81 -0
- package/lib/session-groups/cli.ts +61 -0
- package/lib/session-groups/createGroup.ts +28 -0
- package/lib/session-groups/index.ts +21 -0
- package/lib/session-groups/removeGroup.ts +36 -0
- package/lib/session-groups/reorderGroups.ts +31 -0
- package/lib/session-groups/resolveGroup.ts +19 -0
- package/lib/session-groups/route.ts +32 -0
- package/lib/session-groups/store.ts +85 -0
- package/lib/session-groups/types.ts +25 -0
- package/lib/session-groups/updateGroup.ts +35 -0
- package/lib/session-groups/workspace.ts +21 -0
- package/lib/sessions/crew.ts +72 -0
- package/lib/sessions/launch.ts +54 -11
- package/lib/settings-url.ts +1 -0
- package/lib/share/markdown.ts +2 -2
- package/lib/share/redact.ts +16 -10
- package/lib/share/render.tsx +1 -1
- package/lib/share/snapshot.ts +2 -2
- package/lib/sidebar/rollup.ts +1 -0
- package/lib/sidebar/row-menu.ts +8 -1
- package/lib/sidebar/shelves.ts +90 -15
- package/lib/sidebar/visible-order.ts +5 -0
- package/lib/tasks/hold-checks.ts +3 -1
- package/lib/tasks/holds.ts +3 -0
- package/lib/tasks/index.ts +11 -0
- package/lib/tasks/queue-rows.ts +8 -1
- package/lib/tasks/queue-start.ts +1 -0
- package/lib/tasks/queue-types.ts +9 -1
- package/lib/tasks/queue.ts +15 -5
- package/lib/tasks/schedule-types.ts +1 -0
- package/lib/test-temp.ts +19 -3
- package/lib/workspaces.ts +11 -3
- package/lib/worktree-deps/caches.ts +3 -2
- package/lib/worktree-deps/clone.ts +3 -1
- package/lib/worktree-deps/paths.ts +26 -0
- package/lib/worktree-sweep/testTemp.ts +23 -106
- package/package.json +1 -1
- package/scripts/agent-os +1 -1
- package/scripts/autodeploy +2 -0
- package/scripts/autodeploy.test.ts +31 -1
- package/scripts/claim-hook.sh +31 -0
- package/scripts/claim-hook.test.ts +230 -0
- package/scripts/lib/cli-links.sh +36 -0
- package/scripts/lib/commands.sh +3 -0
- package/scripts/lib/machine.sh +27 -2
- package/scripts/lib/machine.test.ts +25 -3
- package/scripts/sync-cli-links +7 -0
- package/scripts/sync-cli-links.test.ts +69 -0
- package/server.ts +12 -1
- package/stores/claimsUi.ts +22 -0
- package/stores/exportUi.ts +87 -0
- package/app/api/groups/[...path]/route.ts +0 -136
- package/app/api/groups/route.ts +0 -93
- package/data/groups/index.ts +0 -1
- package/data/groups/mutations.ts +0 -95
- package/hooks/useGroups.ts +0 -37
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { NextRequest } from "next/server";
|
|
3
|
+
import { db } from "@/lib/db";
|
|
4
|
+
import { machineId } from "@/lib/claims/machine";
|
|
5
|
+
import { putSetting } from "@/lib/settings-store";
|
|
6
|
+
import { upsertLocal } from "@/lib/claims/store";
|
|
7
|
+
import { POST } from "./route";
|
|
8
|
+
import { DELETE } from "./[id]/route";
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
db.prepare(`DELETE FROM work_claims`).run();
|
|
12
|
+
putSetting("claims.share", null);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const post = (body: unknown) =>
|
|
16
|
+
POST(
|
|
17
|
+
new NextRequest("http://localhost/api/claims", {
|
|
18
|
+
method: "POST",
|
|
19
|
+
body: JSON.stringify(body),
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
const del = (id: string) =>
|
|
23
|
+
DELETE(new Request("http://localhost"), { params: Promise.resolve({ id }) });
|
|
24
|
+
|
|
25
|
+
const ok = {
|
|
26
|
+
remote: "https://github.com/acme/widgets.git",
|
|
27
|
+
branch: "feature/search",
|
|
28
|
+
goal: "Rework search",
|
|
29
|
+
files: ["src/search.ts"],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
describe("POST /api/claims", () => {
|
|
33
|
+
it("registers, then reports an overlap with a task's claim", async () => {
|
|
34
|
+
upsertLocal(
|
|
35
|
+
{ id: machineId(), name: "here" },
|
|
36
|
+
{
|
|
37
|
+
sessionId: "t1",
|
|
38
|
+
sessionName: "Task",
|
|
39
|
+
repo: "github.com/acme/widgets",
|
|
40
|
+
branch: "task/search",
|
|
41
|
+
base: "main",
|
|
42
|
+
goal: "Search work",
|
|
43
|
+
files: ["src/search.ts"],
|
|
44
|
+
filesMore: 0,
|
|
45
|
+
prUrl: null,
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
const res = await post(ok);
|
|
49
|
+
const data = await res.json();
|
|
50
|
+
expect(res.status).toBe(200);
|
|
51
|
+
expect(data.id).toMatch(/^o-/);
|
|
52
|
+
expect(data.overlaps[0].strength).toBe("strong");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("rejects a bad body, a non-remote and a custom id", async () => {
|
|
56
|
+
expect((await post({ remote: 5 })).status).toBe(400);
|
|
57
|
+
expect((await post({ ...ok, id: "s-abc" })).status).toBe(400);
|
|
58
|
+
expect((await post({ ...ok, remote: "/srv/x" })).status).toBe(422);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("is off when claims are", async () => {
|
|
62
|
+
putSetting("claims.share", "off");
|
|
63
|
+
expect((await post(ok)).status).toBe(409);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("DELETE /api/claims/:id", () => {
|
|
68
|
+
it("ends an outside claim but not a task's", async () => {
|
|
69
|
+
upsertLocal(
|
|
70
|
+
{ id: machineId(), name: "here" },
|
|
71
|
+
{
|
|
72
|
+
sessionId: "t1",
|
|
73
|
+
sessionName: "Task",
|
|
74
|
+
repo: "github.com/acme/widgets",
|
|
75
|
+
branch: "x",
|
|
76
|
+
base: "main",
|
|
77
|
+
goal: "Task",
|
|
78
|
+
files: [],
|
|
79
|
+
filesMore: 0,
|
|
80
|
+
prUrl: null,
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
expect((await del("s-t1")).status).toBe(403);
|
|
84
|
+
const { id } = await (await post(ok)).json();
|
|
85
|
+
expect((await del(id)).status).toBe(200);
|
|
86
|
+
expect((await del("o-00000000-0000-0000-0000-000000000000")).status).toBe(
|
|
87
|
+
200
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { NextRequest } from "next/server";
|
|
3
|
+
import { db } from "@/lib/db";
|
|
4
|
+
import { machineId } from "@/lib/claims/machine";
|
|
5
|
+
import { createConnectHost } from "@/lib/hosts";
|
|
6
|
+
import { putSetting } from "@/lib/settings-store";
|
|
7
|
+
import { savePin, suspendPin } from "@/lib/peer/links";
|
|
8
|
+
import { upsertLocal } from "@/lib/claims/store";
|
|
9
|
+
import { GET } from "./route";
|
|
10
|
+
import { POST } from "./exchange/route";
|
|
11
|
+
|
|
12
|
+
const post = (body: unknown) =>
|
|
13
|
+
POST(
|
|
14
|
+
new NextRequest("http://localhost/api/claims/exchange", {
|
|
15
|
+
method: "POST",
|
|
16
|
+
body: typeof body === "string" ? body : JSON.stringify(body),
|
|
17
|
+
})
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
db.prepare(`DELETE FROM work_claims`).run();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const claim = {
|
|
25
|
+
id: "r1",
|
|
26
|
+
origin: "peer-1",
|
|
27
|
+
origin_name: "box",
|
|
28
|
+
goal: "Add oauth",
|
|
29
|
+
repo: "github.com/acme/widgets",
|
|
30
|
+
branch: "feature/oauth",
|
|
31
|
+
files: ["a.ts"],
|
|
32
|
+
ttl_s: 300,
|
|
33
|
+
age_s: 5,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe("POST /api/claims/exchange", () => {
|
|
37
|
+
it("stores a peer's claims and answers with ours", async () => {
|
|
38
|
+
upsertLocal(
|
|
39
|
+
{ id: machineId(), name: "here" },
|
|
40
|
+
{
|
|
41
|
+
sessionId: "s",
|
|
42
|
+
sessionName: "Mine",
|
|
43
|
+
repo: "github.com/acme/widgets",
|
|
44
|
+
branch: "feature/mine",
|
|
45
|
+
base: "main",
|
|
46
|
+
goal: "Mine",
|
|
47
|
+
files: [],
|
|
48
|
+
filesMore: 0,
|
|
49
|
+
prUrl: null,
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
const res = await post({
|
|
53
|
+
machine: { id: "peer-1", name: "box" },
|
|
54
|
+
claims: [claim],
|
|
55
|
+
});
|
|
56
|
+
expect(res.status).toBe(200);
|
|
57
|
+
const body = await res.json();
|
|
58
|
+
expect(body.machine.id).toBe(machineId());
|
|
59
|
+
expect(body.claims.map((c: { id: string }) => c.id)).toEqual(["s-s"]);
|
|
60
|
+
const list = await (
|
|
61
|
+
await GET(new NextRequest("http://localhost/api/claims"))
|
|
62
|
+
).json();
|
|
63
|
+
expect(list.claims.map((c: { id: string }) => c.id).sort()).toEqual([
|
|
64
|
+
"r1",
|
|
65
|
+
"s-s",
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("refuses bodies that are not claims", async () => {
|
|
70
|
+
expect((await post("{nope")).status).toBe(400);
|
|
71
|
+
expect(
|
|
72
|
+
(await post({ machine: { id: "p", name: "n" }, claims: [{ id: "x" }] }))
|
|
73
|
+
.status
|
|
74
|
+
).toBe(400);
|
|
75
|
+
expect(
|
|
76
|
+
(
|
|
77
|
+
await post({
|
|
78
|
+
machine: { id: "p", name: "n" },
|
|
79
|
+
claims: [{ ...claim, ttl_s: -5 }],
|
|
80
|
+
})
|
|
81
|
+
).status
|
|
82
|
+
).toBe(400);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("POST /api/claims/exchange as a Connect peer", () => {
|
|
87
|
+
const asPeer = (hostId: string | null) =>
|
|
88
|
+
POST(
|
|
89
|
+
new NextRequest("http://localhost/api/claims/exchange", {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: {
|
|
92
|
+
"x-agentos-trust": "peer",
|
|
93
|
+
...(hostId && { "x-agentos-peer": hostId }),
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
machine: { id: "peer-1", name: "box" },
|
|
97
|
+
claims: [claim],
|
|
98
|
+
}),
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
it("refuses a host with no active pin and stores nothing", async () => {
|
|
103
|
+
expect((await asPeer("no-such-host")).status).toBe(403);
|
|
104
|
+
expect((await asPeer(null)).status).toBe(403);
|
|
105
|
+
expect(db.prepare(`SELECT 1 FROM work_claims`).all()).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("refuses a suspended pin, and an active one keeps its via_host", async () => {
|
|
109
|
+
const host = createConnectHost(`Spark-${Date.now()}`).id;
|
|
110
|
+
savePin({
|
|
111
|
+
hostId: host,
|
|
112
|
+
machineId: "b2b2b2b2",
|
|
113
|
+
hostname: "m-0123456789abcdef.runagentos.com",
|
|
114
|
+
signPub: "s",
|
|
115
|
+
boxPub: "b",
|
|
116
|
+
});
|
|
117
|
+
expect((await asPeer(host)).status).toBe(200);
|
|
118
|
+
const via = () =>
|
|
119
|
+
(
|
|
120
|
+
db.prepare(`SELECT via_host FROM work_claims WHERE id='r1'`).get() as {
|
|
121
|
+
via_host: string | null;
|
|
122
|
+
}
|
|
123
|
+
).via_host;
|
|
124
|
+
expect(via()).toBe(host);
|
|
125
|
+
// A token caller re-sending it, with more life, must not clear the pin.
|
|
126
|
+
await post({
|
|
127
|
+
machine: { id: "peer-1", name: "box" },
|
|
128
|
+
claims: [{ ...claim, ttl_s: 600 }],
|
|
129
|
+
});
|
|
130
|
+
expect(via()).toBe(host);
|
|
131
|
+
suspendPin(host, "revoked");
|
|
132
|
+
db.prepare(`DELETE FROM work_claims`).run();
|
|
133
|
+
expect((await asPeer(host)).status).toBe(403);
|
|
134
|
+
expect(db.prepare(`SELECT 1 FROM work_claims`).all()).toEqual([]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("writes nothing when sharing is off", async () => {
|
|
138
|
+
const host = createConnectHost(`Studio-${Date.now()}`).id;
|
|
139
|
+
savePin({
|
|
140
|
+
hostId: host,
|
|
141
|
+
machineId: "c3c3c3c3",
|
|
142
|
+
hostname: "m-fedcba9876543210.runagentos.com",
|
|
143
|
+
signPub: "s",
|
|
144
|
+
boxPub: "b",
|
|
145
|
+
});
|
|
146
|
+
db.prepare(`DELETE FROM claim_peers`).run();
|
|
147
|
+
putSetting("claims.share", "off");
|
|
148
|
+
try {
|
|
149
|
+
expect((await asPeer(host)).status).toBe(200);
|
|
150
|
+
} finally {
|
|
151
|
+
putSetting("claims.share", "on");
|
|
152
|
+
}
|
|
153
|
+
expect(db.prepare(`SELECT 1 FROM claim_peers`).all()).toEqual([]);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("GET /api/claims", () => {
|
|
158
|
+
it("rejects a bad query and an unknown session", async () => {
|
|
159
|
+
const bad = await GET(
|
|
160
|
+
new NextRequest(`http://localhost/api/claims?repo=${"x".repeat(400)}`)
|
|
161
|
+
);
|
|
162
|
+
expect(bad.status).toBe(400);
|
|
163
|
+
const none = await GET(
|
|
164
|
+
new NextRequest("http://localhost/api/claims?session=missing")
|
|
165
|
+
);
|
|
166
|
+
expect(none.status).toBe(404);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("narrows to a repo and lists overlaps", async () => {
|
|
170
|
+
await post({
|
|
171
|
+
machine: { id: "peer-1", name: "box" },
|
|
172
|
+
claims: [
|
|
173
|
+
claim,
|
|
174
|
+
{
|
|
175
|
+
...claim,
|
|
176
|
+
id: "r2",
|
|
177
|
+
origin: "peer-2",
|
|
178
|
+
origin_name: "pc",
|
|
179
|
+
repo: "github.com/acme/other",
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
const body = await (
|
|
184
|
+
await GET(
|
|
185
|
+
new NextRequest(
|
|
186
|
+
"http://localhost/api/claims?repo=github.com/acme/widgets"
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
).json();
|
|
190
|
+
expect(body.claims.map((c: { id: string }) => c.id)).toEqual(["r1"]);
|
|
191
|
+
expect(body.overlaps).toEqual([]);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
claimsShared,
|
|
5
|
+
claimsView,
|
|
6
|
+
liveClaims,
|
|
7
|
+
registerOutside,
|
|
8
|
+
} from "@/lib/claims";
|
|
9
|
+
import { outsideBody } from "@/lib/claims/types";
|
|
10
|
+
import { repoKey } from "@/lib/git-remote";
|
|
11
|
+
import { getDb, queries, type Session } from "@/lib/db";
|
|
12
|
+
import { getProject } from "@/lib/projects";
|
|
13
|
+
import { expandHome } from "@/lib/tasks/session";
|
|
14
|
+
|
|
15
|
+
export const dynamic = "force-dynamic";
|
|
16
|
+
|
|
17
|
+
const query = z.object({
|
|
18
|
+
session: z.string().max(100).optional(),
|
|
19
|
+
project: z.string().max(100).optional(),
|
|
20
|
+
repo: z.string().max(300).optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// GET /api/claims - what live sessions, here and on linked machines, are
|
|
24
|
+
// working on, with the pairs that overlap and when each machine was last
|
|
25
|
+
// heard from. `session` or `project` narrows it to that repository.
|
|
26
|
+
export async function GET(request: NextRequest) {
|
|
27
|
+
const q = query.safeParse(Object.fromEntries(request.nextUrl.searchParams));
|
|
28
|
+
if (!q.success)
|
|
29
|
+
return NextResponse.json({ error: "Bad query" }, { status: 400 });
|
|
30
|
+
let repo = q.data.repo?.toLowerCase();
|
|
31
|
+
if (q.data.session) {
|
|
32
|
+
const s = queries.getSession(getDb()).get(q.data.session) as
|
|
33
|
+
Session | undefined;
|
|
34
|
+
if (!s)
|
|
35
|
+
return NextResponse.json({ error: "No such session" }, { status: 404 });
|
|
36
|
+
repo =
|
|
37
|
+
(await repoKey(expandHome(s.worktree_path || s.working_directory))) ??
|
|
38
|
+
undefined;
|
|
39
|
+
if (!repo) return NextResponse.json(shape([]));
|
|
40
|
+
}
|
|
41
|
+
if (q.data.project) {
|
|
42
|
+
const p = getProject(q.data.project);
|
|
43
|
+
if (!p)
|
|
44
|
+
return NextResponse.json({ error: "No such project" }, { status: 404 });
|
|
45
|
+
repo = (await repoKey(expandHome(p.working_directory))) ?? undefined;
|
|
46
|
+
if (!repo) return NextResponse.json(shape([]));
|
|
47
|
+
}
|
|
48
|
+
return NextResponse.json(shape(liveClaims({ repo })));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// POST /api/claims - a session AgentOS didn't start registers or renews its
|
|
52
|
+
// claim (`aos claim start|touch`). The CLI read git in its own folder and
|
|
53
|
+
// sends the remote URL, branch and paths; nothing is run here.
|
|
54
|
+
export async function POST(request: NextRequest) {
|
|
55
|
+
const body = outsideBody.safeParse(await request.json().catch(() => null));
|
|
56
|
+
if (!body.success)
|
|
57
|
+
return NextResponse.json({ error: "Bad claim" }, { status: 400 });
|
|
58
|
+
if (!claimsShared())
|
|
59
|
+
return NextResponse.json({ error: "Claims are off" }, { status: 409 });
|
|
60
|
+
const result = registerOutside(body.data);
|
|
61
|
+
if (!result)
|
|
62
|
+
return NextResponse.json(
|
|
63
|
+
{ error: "No repository remote, or that id belongs to another claim" },
|
|
64
|
+
{ status: 422 }
|
|
65
|
+
);
|
|
66
|
+
const claims = liveClaims({ repo: result.claim.repo });
|
|
67
|
+
const overlaps = shape(claims).overlaps.filter(
|
|
68
|
+
(o) => o.a === result.claim.id || o.b === result.claim.id
|
|
69
|
+
);
|
|
70
|
+
return NextResponse.json({ ...shape(claims), id: result.claim.id, overlaps });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const shape = (claims: ReturnType<typeof liveClaims>) => claimsView(claims);
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { deleteHost } from "@/lib/hosts";
|
|
3
|
+
import { pinForHost } from "@/lib/peer/links";
|
|
3
4
|
|
|
4
5
|
export async function DELETE(
|
|
5
6
|
_request: NextRequest,
|
|
6
7
|
{ params }: { params: Promise<{ id: string }> }
|
|
7
8
|
) {
|
|
8
9
|
try {
|
|
9
|
-
|
|
10
|
+
const id = (await params).id;
|
|
11
|
+
// Removing it here would leave the other machine linked to this one.
|
|
12
|
+
if (pinForHost(id))
|
|
13
|
+
return NextResponse.json(
|
|
14
|
+
{ error: "It's linked through Connect: unlink it first." },
|
|
15
|
+
{ status: 409 }
|
|
16
|
+
);
|
|
17
|
+
deleteHost(id);
|
|
10
18
|
return NextResponse.json({ success: true });
|
|
11
19
|
} catch (error) {
|
|
12
20
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { modelAvailability } from "@/lib/models/availability-refs";
|
|
3
|
+
|
|
4
|
+
// GET /api/models/availability?refresh=1 - whether each local model's
|
|
5
|
+
// server answers now (ref to state). Cloud models aren't probed.
|
|
6
|
+
export async function GET(request: NextRequest) {
|
|
7
|
+
const refresh = request.nextUrl.searchParams.has("refresh");
|
|
8
|
+
return NextResponse.json(await modelAvailability(refresh));
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { modelCatalog } from "@/lib/models/catalog";
|
|
3
|
+
|
|
4
|
+
// GET /api/models/choices - every model a picker offers, from the server:
|
|
5
|
+
// the enabled providers' models and the ones Pi reaches, each with its
|
|
6
|
+
// locality (local or cloud) and the last availability check.
|
|
7
|
+
export async function GET() {
|
|
8
|
+
return NextResponse.json({ choices: modelCatalog() });
|
|
9
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { PEER_HEADER, TRUST_HEADER } from "@/lib/security/auth";
|
|
3
|
+
import { getHost } from "@/lib/hosts";
|
|
4
|
+
import { unlinkedByPeer } from "@/lib/peer/links";
|
|
5
|
+
|
|
6
|
+
export const dynamic = "force-dynamic";
|
|
7
|
+
|
|
8
|
+
// POST /api/peer/unlink - a linked machine unlinked this one. Reached only
|
|
9
|
+
// through its signed envelope (lib/peer/rpc-server.ts), which the gate turns
|
|
10
|
+
// into `peer` trust naming the host it's pinned to; nothing else may call it.
|
|
11
|
+
export function POST(request: NextRequest) {
|
|
12
|
+
const hostId = request.headers.get(PEER_HEADER);
|
|
13
|
+
if (request.headers.get(TRUST_HEADER) !== "peer" || !hostId)
|
|
14
|
+
return NextResponse.json({ error: "forbidden" }, { status: 403 });
|
|
15
|
+
const name = getHost(hostId)?.name ?? "the other machine";
|
|
16
|
+
return NextResponse.json({ unlinked: unlinkedByPeer(hostId, name) });
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { requireApprover } from "@/lib/security/approver";
|
|
3
|
+
import { peerAudit } from "@/lib/peer/audit";
|
|
4
|
+
|
|
5
|
+
// GET /api/peers/:hostId/audit - every call to and from that linked machine,
|
|
6
|
+
// newest first: Machines → Activity.
|
|
7
|
+
export async function GET(
|
|
8
|
+
request: NextRequest,
|
|
9
|
+
{ params }: { params: Promise<{ hostId: string }> }
|
|
10
|
+
) {
|
|
11
|
+
const gate = requireApprover(request);
|
|
12
|
+
if (!gate.ok) return gate.response;
|
|
13
|
+
return NextResponse.json({ rows: peerAudit((await params).hostId) });
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { requireApprover } from "@/lib/security/approver";
|
|
3
|
+
import { unlinkPeer } from "@/lib/peer/unlink";
|
|
4
|
+
|
|
5
|
+
// DELETE /api/peers/:hostId - unlink a machine linked through Connect: its
|
|
6
|
+
// pin goes here at once, and it's told to drop its side. Takes the same
|
|
7
|
+
// trust as linking.
|
|
8
|
+
export async function DELETE(
|
|
9
|
+
request: NextRequest,
|
|
10
|
+
{ params }: { params: Promise<{ hostId: string }> }
|
|
11
|
+
) {
|
|
12
|
+
const gate = requireApprover(request);
|
|
13
|
+
if (!gate.ok) return gate.response;
|
|
14
|
+
try {
|
|
15
|
+
return NextResponse.json(await unlinkPeer((await params).hostId));
|
|
16
|
+
} catch (err) {
|
|
17
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
18
|
+
return NextResponse.json({ error }, { status: 404 });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -9,6 +9,9 @@ const link = await import("./link/route");
|
|
|
9
9
|
const cancel = await import("./link/[id]/route");
|
|
10
10
|
const confirm = await import("./link/[id]/confirm/route");
|
|
11
11
|
const hello = await import("../peer/hello/route");
|
|
12
|
+
const unlink = await import("./[hostId]/route");
|
|
13
|
+
const audit = await import("./[hostId]/audit/route");
|
|
14
|
+
const hostDelete = await import("../hosts/[id]/route");
|
|
12
15
|
|
|
13
16
|
const plain = mintDevice("phone").device.id;
|
|
14
17
|
const approver = mintDevice("tablet", null, true).device.id;
|
|
@@ -24,7 +27,10 @@ const req = (
|
|
|
24
27
|
...(o.device && { "x-agentos-device": o.device }),
|
|
25
28
|
"content-type": "application/json",
|
|
26
29
|
},
|
|
27
|
-
body:
|
|
30
|
+
body:
|
|
31
|
+
o.method === "GET"
|
|
32
|
+
? undefined
|
|
33
|
+
: (o.body ?? JSON.stringify({ code: "123456" })),
|
|
28
34
|
});
|
|
29
35
|
const params = { params: Promise.resolve({ id: "nope" }) };
|
|
30
36
|
|
|
@@ -105,7 +111,64 @@ describe("linking routes", () => {
|
|
|
105
111
|
"/api/peers/account",
|
|
106
112
|
"/api/peers/link",
|
|
107
113
|
"/api/peers/link/x/confirm",
|
|
114
|
+
"/api/peers/h1",
|
|
115
|
+
"/api/peers/h1/audit",
|
|
116
|
+
"/api/peer/unlink",
|
|
108
117
|
])
|
|
109
118
|
expect(isPublicPath(p)).toBe(false);
|
|
110
119
|
});
|
|
120
|
+
|
|
121
|
+
it("unlink and Activity take the same trust as linking", async () => {
|
|
122
|
+
const hostParams = { params: Promise.resolve({ hostId: "nope" }) };
|
|
123
|
+
for (const who of [
|
|
124
|
+
{},
|
|
125
|
+
{ trust: "device", device: plain },
|
|
126
|
+
{ trust: "peer" },
|
|
127
|
+
{ trust: "open" },
|
|
128
|
+
]) {
|
|
129
|
+
const del = req("/api/peers/nope", {
|
|
130
|
+
...who,
|
|
131
|
+
method: "DELETE",
|
|
132
|
+
body: "",
|
|
133
|
+
});
|
|
134
|
+
expect((await unlink.DELETE(del, hostParams)).status).toBe(403);
|
|
135
|
+
const get = req("/api/peers/nope/audit", { ...who, method: "GET" });
|
|
136
|
+
expect((await audit.GET(get, hostParams)).status).toBe(403);
|
|
137
|
+
}
|
|
138
|
+
const asApprover = { trust: "device", device: approver };
|
|
139
|
+
expect(
|
|
140
|
+
(
|
|
141
|
+
await unlink.DELETE(
|
|
142
|
+
req("/x", { ...asApprover, method: "DELETE", body: "" }),
|
|
143
|
+
hostParams
|
|
144
|
+
)
|
|
145
|
+
).status
|
|
146
|
+
).toBe(404);
|
|
147
|
+
const rows = await audit.GET(
|
|
148
|
+
req("/x", { ...asApprover, method: "GET" }),
|
|
149
|
+
hostParams
|
|
150
|
+
);
|
|
151
|
+
expect(await rows.json()).toEqual({ rows: [] });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("won't remove a Connect-linked machine without unlinking it", async () => {
|
|
155
|
+
const { createConnectHost } = await import("@/lib/hosts");
|
|
156
|
+
const { savePin, pinForHost } = await import("@/lib/peer/links");
|
|
157
|
+
const id = createConnectHost("Linked").id;
|
|
158
|
+
savePin({
|
|
159
|
+
hostId: id,
|
|
160
|
+
machineId: "f0f0f0f0",
|
|
161
|
+
hostname: "m-0000000000000000.runagentos.com",
|
|
162
|
+
signPub: "x",
|
|
163
|
+
boxPub: "y",
|
|
164
|
+
});
|
|
165
|
+
const res = await hostDelete.DELETE(
|
|
166
|
+
req("/x", { method: "DELETE", body: "" }),
|
|
167
|
+
{
|
|
168
|
+
params: Promise.resolve({ id }),
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
expect(res.status).toBe(409);
|
|
172
|
+
expect(pinForHost(id)).not.toBeNull();
|
|
173
|
+
});
|
|
111
174
|
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { NextRequest } from "next/server";
|
|
3
|
+
import { mintDevice } from "@/lib/security/devices";
|
|
4
|
+
|
|
5
|
+
const seen: unknown[] = [];
|
|
6
|
+
vi.mock("@/lib/peer/pairing", () => ({
|
|
7
|
+
pendingRequests: (_now: number, o: unknown) => {
|
|
8
|
+
seen.push(o);
|
|
9
|
+
return [];
|
|
10
|
+
},
|
|
11
|
+
}));
|
|
12
|
+
const { GET } = await import("./route");
|
|
13
|
+
|
|
14
|
+
const get = (headers: Record<string, string>) =>
|
|
15
|
+
GET(new NextRequest("http://localhost/api/peers/pending", { headers }));
|
|
16
|
+
|
|
17
|
+
describe("GET /api/peers/pending", () => {
|
|
18
|
+
it("hands out the second code only to a caller that could accept", async () => {
|
|
19
|
+
const plain = mintDevice("phone").device.id;
|
|
20
|
+
const approver = mintDevice("tablet", null, true).device.id;
|
|
21
|
+
for (const h of [
|
|
22
|
+
{} as Record<string, string>,
|
|
23
|
+
{ "x-agentos-trust": "device", "x-agentos-device": plain },
|
|
24
|
+
{ "x-agentos-trust": "peer" },
|
|
25
|
+
])
|
|
26
|
+
expect((await get(h)).status).toBe(200);
|
|
27
|
+
await get({ "x-agentos-trust": "loopback" });
|
|
28
|
+
await get({ "x-agentos-trust": "device", "x-agentos-device": approver });
|
|
29
|
+
expect(seen).toEqual([
|
|
30
|
+
{ withCodes: false },
|
|
31
|
+
{ withCodes: false },
|
|
32
|
+
{ withCodes: false },
|
|
33
|
+
{ withCodes: true },
|
|
34
|
+
{ withCodes: true },
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { approverOf } from "@/lib/security/approver";
|
|
2
3
|
import { pendingRequests } from "@/lib/peer/pairing";
|
|
3
4
|
|
|
4
|
-
// GET /api/peers/pending - machines asking to link here, waiting for the
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// GET /api/peers/pending - machines asking to link here, waiting for the
|
|
6
|
+
// code, and accepted ones whose second code is still to be typed there. That
|
|
7
|
+
// code goes only to a caller that could have accepted.
|
|
8
|
+
export function GET(request: NextRequest) {
|
|
9
|
+
return NextResponse.json({
|
|
10
|
+
requests: pendingRequests(Date.now(), {
|
|
11
|
+
withCodes: approverOf(request.headers) !== null,
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
7
14
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { restoreGroup } from "@/lib/session-groups";
|
|
2
|
+
import { run } from "@/lib/session-groups/route";
|
|
3
|
+
|
|
4
|
+
// POST /api/session-groups/:id/restore - undo a delete.
|
|
5
|
+
export async function POST(
|
|
6
|
+
_request: Request,
|
|
7
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
8
|
+
) {
|
|
9
|
+
const { id } = await params;
|
|
10
|
+
return run(() => ({ sessionGroup: restoreGroup(id) }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { deleteGroup, updateGroup } from "@/lib/session-groups";
|
|
3
|
+
import { groupRoute, run } from "@/lib/session-groups/route";
|
|
4
|
+
|
|
5
|
+
type Ctx = { params: Promise<{ id: string }> };
|
|
6
|
+
|
|
7
|
+
const patch = z.object({
|
|
8
|
+
name: z.string().optional(),
|
|
9
|
+
collapsed: z.boolean().optional(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// PATCH /api/session-groups/:id { name?, collapsed? } - rename or fold.
|
|
13
|
+
export async function PATCH(request: Request, { params }: Ctx) {
|
|
14
|
+
const { id } = await params;
|
|
15
|
+
return groupRoute(request, patch, (b) => ({
|
|
16
|
+
sessionGroup: updateGroup(id, b),
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// DELETE /api/session-groups/:id - soft delete; its sessions fall back to the
|
|
21
|
+
// status shelves and restore brings them back.
|
|
22
|
+
export async function DELETE(_request: Request, { params }: Ctx) {
|
|
23
|
+
const { id } = await params;
|
|
24
|
+
return run(() => ({ sessionGroup: deleteGroup(id) }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { reorderGroups } from "@/lib/session-groups";
|
|
3
|
+
import { groupRoute } from "@/lib/session-groups/route";
|
|
4
|
+
|
|
5
|
+
const order = z.object({
|
|
6
|
+
workspaceId: z.string().nullable(),
|
|
7
|
+
ids: z.array(z.string()),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
// POST /api/session-groups/order { workspaceId, ids } - a workspace's order.
|
|
11
|
+
export async function POST(request: Request) {
|
|
12
|
+
return groupRoute(request, order, (b) => ({
|
|
13
|
+
sessionGroups: reorderGroups(b.workspaceId, b.ids),
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createGroup, listGroups } from "@/lib/session-groups";
|
|
3
|
+
import { groupRoute, run } from "@/lib/session-groups/route";
|
|
4
|
+
|
|
5
|
+
const create = z.object({
|
|
6
|
+
name: z.string({ error: "A group needs a name" }),
|
|
7
|
+
workspaceId: z.string().nullable(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
// GET /api/session-groups - every live group.
|
|
11
|
+
export async function GET() {
|
|
12
|
+
return run(() => ({ sessionGroups: listGroups() }));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// POST /api/session-groups { name, workspaceId } - create one at the bottom
|
|
16
|
+
// of its workspace's order.
|
|
17
|
+
export async function POST(request: Request) {
|
|
18
|
+
return groupRoute(request, create, (b) => ({
|
|
19
|
+
sessionGroup: createGroup(b.workspaceId, b.name),
|
|
20
|
+
}));
|
|
21
|
+
}
|