@a-t-h-i/bot-lobby 0.6.7 → 0.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +182 -16
- package/package.json +5 -1
- package/prompts/master.md +15 -0
- package/prompts/panel.md +4 -0
- package/prompts/planner.md +5 -0
- package/prompts/pr-review.md +62 -0
- package/prompts/splitter.md +58 -0
- package/src/ask/dialog.ts +19 -5
- package/src/ask/state.ts +8 -2
- package/src/ask/tool.ts +3 -1
- package/src/ask/view.ts +6 -3
- package/src/classifier/instance.ts +6 -0
- package/src/classifier/knowledge.ts +158 -0
- package/src/classifier/review.ts +146 -0
- package/src/excalidraw/check.ts +31 -0
- package/src/excalidraw/client.ts +266 -0
- package/src/excalidraw/room.ts +118 -0
- package/src/excalidraw/scene.ts +746 -0
- package/src/excalidraw/sessions.ts +302 -0
- package/src/excalidraw/tools.ts +223 -0
- package/src/execution/agent-runner.ts +2 -0
- package/src/execution/pi-runner.ts +9 -3
- package/src/execution/workspace.ts +185 -0
- package/src/index.ts +3 -0
- package/src/knowledge/edit.ts +109 -0
- package/src/knowledge/notes.ts +143 -0
- package/src/knowledge/selector.ts +1 -1
- package/src/knowledge/store.ts +16 -2
- package/src/lobby/ask.ts +42 -21
- package/src/lobby/issues.ts +1 -1
- package/src/lobby/knowledge.ts +179 -0
- package/src/lobby/layout.ts +95 -18
- package/src/lobby/planner.ts +205 -8
- package/src/lobby/pr-review.ts +360 -0
- package/src/lobby/pulls.ts +250 -0
- package/src/lobby/quickfix.ts +2 -0
- package/src/lobby/runtime.ts +121 -13
- package/src/lobby/split.ts +230 -0
- package/src/lobby/tabs/excalidraw.ts +95 -0
- package/src/lobby/tabs/git.ts +162 -0
- package/src/lobby/tabs/knowledge.ts +135 -0
- package/src/lobby/tabs/plan.ts +3 -1
- package/src/lobby/tabs/tasks.ts +11 -2
- package/src/lobby/view.ts +611 -32
- package/src/master/master.ts +39 -13
- package/src/pi/commands.ts +12 -7
- package/src/pi/model-support.ts +9 -0
- package/src/pi/plan-checklist.ts +42 -0
- package/src/pi/route.ts +2 -1
- package/src/pi/settings-ui.ts +41 -1
- package/src/pi/start-flags.ts +14 -0
- package/src/pi/start-task.ts +56 -5
- package/src/pi/tools.ts +4 -2
- package/src/schemas/configuration.ts +32 -3
- package/src/schemas/task.ts +15 -0
- package/src/state/backlog.ts +27 -4
- package/src/state/metrics.ts +2 -2
- package/src/state/persistence.ts +5 -5
- package/src/text.ts +38 -0
- package/src/workflow/workflow.ts +11 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Excalidraw's shared-session ("live collaboration") rooms, the parts that do
|
|
3
|
+
* not touch the network: reading and making room links, and the AES-GCM
|
|
4
|
+
* envelope every message in a room travels in. Excalidraw's collaboration
|
|
5
|
+
* server only relays opaque bytes; the key sits in the link's `#room=id,key`
|
|
6
|
+
* fragment, which a browser never sends to any server, so whoever holds the
|
|
7
|
+
* link can read and draw in the room and nobody else can.
|
|
8
|
+
*/
|
|
9
|
+
import { randomBytes, webcrypto } from "node:crypto";
|
|
10
|
+
|
|
11
|
+
/** Excalidraw's own collaboration server; `BOT_LOBBY_EXCALIDRAW_SERVER` points at a self-hosted one. */
|
|
12
|
+
export const DEFAULT_SERVER = "https://oss-collab.excalidraw.com";
|
|
13
|
+
|
|
14
|
+
/** The site a made-up room link opens on. */
|
|
15
|
+
export const DEFAULT_ORIGIN = "https://excalidraw.com";
|
|
16
|
+
|
|
17
|
+
const ROOM_ID = /^[a-zA-Z0-9_-]+$/;
|
|
18
|
+
/** A 128-bit key in base64url is exactly 22 characters. */
|
|
19
|
+
const ROOM_KEY = /^[a-zA-Z0-9_-]{22}$/;
|
|
20
|
+
const FRAGMENT = /#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/;
|
|
21
|
+
const IV_BYTES = 12;
|
|
22
|
+
|
|
23
|
+
export interface RoomLink {
|
|
24
|
+
roomId: string;
|
|
25
|
+
roomKey: string;
|
|
26
|
+
/** The link as Excalidraw writes it: `https://excalidraw.com/#room=<id>,<key>`. */
|
|
27
|
+
url: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A room link as people paste it: the full URL (any Excalidraw host, so a
|
|
32
|
+
* self-hosted one works), `#room=id,key`, or the bare `id,key`. Undefined when
|
|
33
|
+
* it is none of those or the key is not the 22 characters Excalidraw makes.
|
|
34
|
+
*/
|
|
35
|
+
export function parseRoomLink(input: string): RoomLink | undefined {
|
|
36
|
+
const text = input.trim();
|
|
37
|
+
if (!text) return undefined;
|
|
38
|
+
let roomId: string | undefined;
|
|
39
|
+
let roomKey: string | undefined;
|
|
40
|
+
let origin = DEFAULT_ORIGIN;
|
|
41
|
+
const fragment = text.match(FRAGMENT);
|
|
42
|
+
if (fragment) {
|
|
43
|
+
[, roomId, roomKey] = fragment;
|
|
44
|
+
const before = text.slice(0, fragment.index);
|
|
45
|
+
if (before) {
|
|
46
|
+
try {
|
|
47
|
+
const parsed = new URL(before);
|
|
48
|
+
if (parsed.protocol === "https:" || parsed.protocol === "http:") origin = `${parsed.origin}${parsed.pathname === "/" ? "" : parsed.pathname}`;
|
|
49
|
+
else return undefined;
|
|
50
|
+
} catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
const bare = text.match(/^([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/);
|
|
56
|
+
if (!bare) return undefined;
|
|
57
|
+
[, roomId, roomKey] = bare;
|
|
58
|
+
}
|
|
59
|
+
if (!roomId || !roomKey || !ROOM_ID.test(roomId) || !ROOM_KEY.test(roomKey)) return undefined;
|
|
60
|
+
return { roomId, roomKey, url: `${origin.replace(/\/$/, "")}/#room=${roomId},${roomKey}` };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A fresh room: opening its link in Excalidraw starts the session, and the room is the first thing in it. */
|
|
64
|
+
export function newRoomLink(origin = DEFAULT_ORIGIN): RoomLink {
|
|
65
|
+
const roomId = randomBytes(10).toString("hex");
|
|
66
|
+
const roomKey = randomBytes(16).toString("base64url");
|
|
67
|
+
return { roomId, roomKey, url: `${origin.replace(/\/$/, "")}/#room=${roomId},${roomKey}` };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The link with its key hidden, for logs and lists. */
|
|
71
|
+
export function maskedLink(link: RoomLink): string {
|
|
72
|
+
return `room ${link.roomId}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const keys = new Map<string, Promise<webcrypto.CryptoKey>>();
|
|
76
|
+
|
|
77
|
+
function cryptoKey(roomKey: string, usage: "encrypt" | "decrypt"): Promise<webcrypto.CryptoKey> {
|
|
78
|
+
const id = `${usage}:${roomKey}`;
|
|
79
|
+
let key = keys.get(id);
|
|
80
|
+
if (!key) {
|
|
81
|
+
key = webcrypto.subtle.importKey("jwk", { alg: "A128GCM", ext: true, k: roomKey, key_ops: ["encrypt", "decrypt"], kty: "oct" }, { name: "AES-GCM", length: 128 }, false, [usage]);
|
|
82
|
+
keys.set(id, key);
|
|
83
|
+
if (keys.size > 32) keys.delete(keys.keys().next().value!);
|
|
84
|
+
}
|
|
85
|
+
return key;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface Sealed {
|
|
89
|
+
data: Buffer;
|
|
90
|
+
iv: Buffer;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A message as Excalidraw sends it: the JSON of `payload`, encrypted with a fresh IV. */
|
|
94
|
+
export async function seal(roomKey: string, payload: unknown): Promise<Sealed> {
|
|
95
|
+
const iv = randomBytes(IV_BYTES);
|
|
96
|
+
const encrypted = await webcrypto.subtle.encrypt({ name: "AES-GCM", iv }, await cryptoKey(roomKey, "encrypt"), new TextEncoder().encode(JSON.stringify(payload)));
|
|
97
|
+
return { data: Buffer.from(encrypted), iv };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function bytes(value: unknown): Uint8Array | undefined {
|
|
101
|
+
if (value instanceof Uint8Array) return value;
|
|
102
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
103
|
+
if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** What `seal` made, opened; undefined when it is not ours to read (a wrong key, a damaged message). */
|
|
108
|
+
export async function unseal(roomKey: string, data: unknown, iv: unknown): Promise<unknown> {
|
|
109
|
+
const body = bytes(data);
|
|
110
|
+
const nonce = bytes(iv);
|
|
111
|
+
if (!body || !nonce) return undefined;
|
|
112
|
+
try {
|
|
113
|
+
const opened = await webcrypto.subtle.decrypt({ name: "AES-GCM", iv: nonce }, await cryptoKey(roomKey, "decrypt"), body);
|
|
114
|
+
return JSON.parse(new TextDecoder().decode(opened));
|
|
115
|
+
} catch {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|