pi-roundtable-web 0.24.2 → 0.25.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.
- package/CHANGELOG.md +33 -0
- package/README.md +51 -9
- package/dist/chunk-action-menu-qe839g4b.js +1 -0
- package/dist/chunk-agent-0pm9p3nn.js +1 -0
- package/dist/chunk-arranged-rooms-enpfswmg.js +1 -0
- package/dist/chunk-character-creator-6yf29xwx.js +1 -0
- package/dist/chunk-codeeditor-gdbdg7rp.js +2 -0
- package/dist/chunk-confirm-xrw7d732.js +1 -0
- package/dist/chunk-connectors-anhghy3k.js +1 -0
- package/dist/chunk-conversations-ry228aes.js +1 -0
- package/dist/chunk-drag-q4fx9b22.js +1 -0
- package/dist/chunk-entry-he1embgp.js +1 -0
- package/dist/chunk-folder-list-5cp8stna.js +1 -0
- package/dist/chunk-folds-hff104tk.js +1 -0
- package/dist/chunk-group-scene-h228jdat.js +1 -0
- package/dist/chunk-index-0p5h2kdz.js +2 -0
- package/dist/chunk-index-0pt08yd9.js +1 -0
- package/dist/chunk-index-12bw9yj8.js +2 -0
- package/dist/chunk-index-18gxy79n.js +47 -0
- package/dist/{chunk-index-mpm90318.js → chunk-index-27qca4mr.js} +2 -2
- package/dist/chunk-index-2ad17eh4.js +2 -0
- package/dist/chunk-index-31wh5twj.js +52 -0
- package/dist/chunk-index-4dy94pbz.js +2 -0
- package/dist/chunk-index-4ecjtdy8.js +13 -0
- package/dist/chunk-index-4v1b7ks9.js +2 -0
- package/dist/chunk-index-5d8yjj4m.js +2 -0
- package/dist/chunk-index-89ajmxh7.js +2 -0
- package/dist/chunk-index-9b1kt7qw.css +1 -0
- package/dist/chunk-index-9jqcskb4.js +14 -0
- package/dist/chunk-index-b9ny4623.js +2 -0
- package/dist/chunk-index-bvfqjh77.js +2 -0
- package/dist/chunk-index-d5abvq7v.js +2 -0
- package/dist/chunk-index-e1dr6g10.js +2 -0
- package/dist/chunk-index-gpkzhq1g.js +2 -0
- package/dist/chunk-index-hk3xyy20.js +2 -0
- package/dist/chunk-index-jm23vn03.js +4 -0
- package/dist/chunk-index-k7gh0jvz.js +2 -0
- package/dist/{chunk-index-6t68gbex.js → chunk-index-krtk6nn2.js} +2 -2
- package/dist/chunk-index-ky42ev0b.js +2 -0
- package/dist/chunk-index-pa2yyqh0.js +2 -0
- package/dist/chunk-index-pcvt668q.js +2 -0
- package/dist/chunk-index-pqw27eye.js +1 -0
- package/dist/chunk-index-s681nny9.js +2 -0
- package/dist/chunk-index-wmbmmrd4.js +302 -0
- package/dist/chunk-index-ymbjrsp6.js +2 -0
- package/dist/chunk-index-zfd00hwb.js +2 -0
- package/dist/chunk-lang-css-t85r8rz0.js +1 -0
- package/dist/{chunk-lang-dockerfile-ey0nvhhm.js → chunk-lang-dockerfile-d5mzxs60.js} +1 -1
- package/dist/{chunk-lang-go-403qsw9f.js → chunk-lang-go-830fmhba.js} +3 -3
- package/dist/chunk-lang-html-n6jparge.js +1 -0
- package/dist/chunk-lang-javascript-xj00cp97.js +1 -0
- package/dist/chunk-lang-json-5eaarz7j.js +1 -0
- package/dist/chunk-lang-markdown-zxx8eke8.js +6 -0
- package/dist/{chunk-lang-python-9jk5a49y.js → chunk-lang-python-fjw84w10.js} +2 -2
- package/dist/chunk-lang-rust-5rbp2zdk.js +1 -0
- package/dist/{chunk-lang-shell-cgq6q0kj.js → chunk-lang-shell-vzagpym3.js} +1 -1
- package/dist/chunk-lang-sql-0c2w9c2y.js +2 -0
- package/dist/{chunk-lang-toml-xh45nm68.js → chunk-lang-toml-yr9fv6eb.js} +1 -1
- package/dist/chunk-lang-typescript-8f6yywt3.js +1 -0
- package/dist/{chunk-lang-xml-1mh879j9.js → chunk-lang-xml-eze08p42.js} +1 -1
- package/dist/{chunk-lang-yaml-jac98srt.js → chunk-lang-yaml-c4b6ft3y.js} +1 -1
- package/dist/chunk-layout-actions-bv0chj41.js +1 -0
- package/dist/chunk-menu-items-jrwxycr1.js +2 -0
- package/dist/chunk-notes-9b7pgqfe.js +1 -0
- package/dist/chunk-overview-8yw71esz.js +1 -0
- package/dist/chunk-palette-frms7axe.js +1 -0
- package/dist/chunk-placement-items-fvfy9m0y.js +1 -0
- package/dist/chunk-prompt-history-yrqehads.js +1 -0
- package/dist/chunk-prompts-g6609tqx.js +1 -0
- package/dist/chunk-room-confirms-0a2y0925.js +1 -0
- package/dist/chunk-room-menu-g11q97br.js +1 -0
- package/dist/chunk-rooms-menu-button-h9qge48n.js +1 -0
- package/dist/chunk-schedules-ezmacrbj.js +1 -0
- package/dist/chunk-seat-menu-jv4s7hmj.js +1 -0
- package/dist/chunk-sheet-hosts-b0azyfhb.js +1 -0
- package/dist/chunk-skill-editor-h6k6whwg.js +8 -0
- package/dist/chunk-skills-gqcfhq56.js +1 -0
- package/dist/chunk-switcher-list-mcpamqnj.js +1 -0
- package/dist/chunk-table-s8cde1xz.js +1 -0
- package/dist/chunk-table-seats-wqkjn8bc.js +1 -0
- package/dist/chunk-table2d-4sje1fr0.js +1 -0
- package/dist/chunk-table3d-h64skazp.js +4220 -0
- package/dist/chunk-transcript-c59c6mpm.js +1 -0
- package/dist/chunk-workspace-0cyspkk4.js +1 -0
- package/dist/chunk-zh-tw-nc93jm10.js +1 -0
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/package.json +3 -3
- package/src/api-types.ts +214 -2
- package/src/app/api-http.ts +66 -0
- package/src/app/app.ts +18 -1
- package/src/app/card-log.ts +6 -0
- package/src/app/layout-routes.ts +239 -0
- package/src/app/layout.ts +133 -0
- package/src/app/prompt-routes.ts +167 -0
- package/src/app/room-log.ts +7 -0
- package/src/app/rooms.ts +23 -0
- package/src/app/routes.ts +155 -131
- package/src/app/skill-doc-routes.ts +122 -0
- package/src/app/team-routes.ts +457 -0
- package/src/app/threads.ts +14 -0
- package/src/i18n/zh-tw.ts +177 -10
- package/src/layout-order.ts +210 -0
- package/src/push/mutes.ts +17 -0
- package/src/web-plugin.ts +25 -0
- package/dist/chunk-action-menu-7v5fce2v.js +0 -1
- package/dist/chunk-agent-2rw4ydw2.js +0 -1
- package/dist/chunk-agent-sheet-2q24dkkf.js +0 -3
- package/dist/chunk-character-creator-8q6ympxj.js +0 -1
- package/dist/chunk-codeeditor-fgmq298t.js +0 -2
- package/dist/chunk-connectors-mr53r7fh.js +0 -1
- package/dist/chunk-conversations-4vy2z6w9.js +0 -1
- package/dist/chunk-entry-jg1eem82.js +0 -1
- package/dist/chunk-group-scene-na95b1te.js +0 -1
- package/dist/chunk-index-11rjg8a8.js +0 -1
- package/dist/chunk-index-1y50d6sj.js +0 -2
- package/dist/chunk-index-5y2r0eaj.js +0 -47
- package/dist/chunk-index-7fphxxzd.js +0 -14
- package/dist/chunk-index-80f1zkp6.css +0 -1
- package/dist/chunk-index-87h45frg.js +0 -2
- package/dist/chunk-index-bnwmssyn.js +0 -2
- package/dist/chunk-index-ckgbszwt.js +0 -51
- package/dist/chunk-index-fmjjekhd.js +0 -2
- package/dist/chunk-index-t1m65yn6.js +0 -13
- package/dist/chunk-index-t711t17m.js +0 -2
- package/dist/chunk-index-txv7qvyy.js +0 -2
- package/dist/chunk-index-wr3dg24d.js +0 -2
- package/dist/chunk-index-yx683d5n.js +0 -2
- package/dist/chunk-lang-css-fkdg3x8x.js +0 -1
- package/dist/chunk-lang-html-v8h9g977.js +0 -1
- package/dist/chunk-lang-javascript-ykb2vhaa.js +0 -1
- package/dist/chunk-lang-json-2rze3rpy.js +0 -1
- package/dist/chunk-lang-markdown-6w2htnfc.js +0 -6
- package/dist/chunk-lang-rust-24fggcqx.js +0 -1
- package/dist/chunk-lang-sql-gjywb9ke.js +0 -2
- package/dist/chunk-lang-typescript-qh6rebyb.js +0 -1
- package/dist/chunk-menu-items-zjpyj51b.js +0 -2
- package/dist/chunk-notes-sjba7kw3.js +0 -1
- package/dist/chunk-overview-26r77kb6.js +0 -1
- package/dist/chunk-room-confirms-851184ep.js +0 -1
- package/dist/chunk-room-menu-8y9c026y.js +0 -1
- package/dist/chunk-skills-56s4hdbn.js +0 -1
- package/dist/chunk-table-qykpvp32.js +0 -1
- package/dist/chunk-table2d-k60ressj.js +0 -1
- package/dist/chunk-table3d-bv2ajk39.js +0 -4220
- package/dist/chunk-transcript-vekz1mh6.js +0 -1
- package/dist/chunk-workspace-hevefd0d.js +0 -1
- package/dist/chunk-zh-tw-5r2wjre0.js +0 -1
package/src/app/routes.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
AgentChange,
|
|
3
2
|
AgentDirectory,
|
|
4
3
|
AgentTeam,
|
|
5
4
|
AvatarStudio,
|
|
@@ -9,23 +8,22 @@ import type {
|
|
|
9
8
|
Logger,
|
|
10
9
|
ModeChange,
|
|
11
10
|
ScheduleStore,
|
|
11
|
+
SkillRegistry,
|
|
12
12
|
Speaker,
|
|
13
13
|
} from "pi-roundtable";
|
|
14
|
-
import {
|
|
15
|
-
import { CharacterError
|
|
14
|
+
import { PluginError } from "pi-roundtable";
|
|
15
|
+
import { CharacterError } from "pi-roundtable/characters";
|
|
16
16
|
import { AgentError } from "pi-roundtable/kit";
|
|
17
17
|
import type {
|
|
18
|
-
AgentPatch,
|
|
19
|
-
AgentProfileView,
|
|
20
18
|
ApiError,
|
|
21
19
|
CharacterRefusal,
|
|
22
|
-
CharacterRequest,
|
|
23
20
|
MessagesView,
|
|
24
21
|
ModelsView,
|
|
25
22
|
RoomsView,
|
|
26
23
|
SchedulesView,
|
|
27
24
|
ScheduleView,
|
|
28
25
|
SessionView,
|
|
26
|
+
SkillView,
|
|
29
27
|
WorkspaceConflict,
|
|
30
28
|
WorkspaceNew,
|
|
31
29
|
WorkspaceRefusalBody,
|
|
@@ -34,14 +32,21 @@ import type {
|
|
|
34
32
|
WorkspaceSave,
|
|
35
33
|
} from "../api-types.ts";
|
|
36
34
|
import type { Authenticated } from "../auth/authenticator.ts";
|
|
35
|
+
import { type ConsolePresentation, message } from "../features.ts";
|
|
37
36
|
import { json } from "../http.ts";
|
|
38
37
|
import type { PushMutes } from "../push/mutes.ts";
|
|
39
|
-
import {
|
|
38
|
+
import { bad, bodyOf, HttpError, notFound } from "./api-http.ts";
|
|
40
39
|
import type { CardLog } from "./card-log.ts";
|
|
41
40
|
import { type FileDir, fileResponse, isFileId } from "./files.ts";
|
|
41
|
+
import type { LayoutStore } from "./layout.ts";
|
|
42
|
+
import { LayoutRoutes } from "./layout-routes.ts";
|
|
43
|
+
import { PromptApi, type PromptApiOptions } from "./prompt-routes.ts";
|
|
42
44
|
import { PAGE_LIMIT, type RoomLog } from "./room-log.ts";
|
|
43
45
|
import type { Rooms } from "./rooms.ts";
|
|
46
|
+
import { SkillDocApi } from "./skill-doc-routes.ts";
|
|
44
47
|
import type { AppSurface } from "./surface.ts";
|
|
48
|
+
import { TeamApi } from "./team-routes.ts";
|
|
49
|
+
import type { WebThreads } from "./threads.ts";
|
|
45
50
|
import { UploadRefusal, type Uploads } from "./uploads.ts";
|
|
46
51
|
import {
|
|
47
52
|
EDIT_MAX_BYTES,
|
|
@@ -50,31 +55,59 @@ import {
|
|
|
50
55
|
WorkspaceRefusal,
|
|
51
56
|
} from "./workspace-files.ts";
|
|
52
57
|
|
|
53
|
-
/** The largest JSON body a write reads: 64 KiB. */
|
|
54
|
-
const MAX_BODY_BYTES = 64 * 1024;
|
|
55
|
-
/** The largest body of a character write: 4 KiB. */
|
|
56
|
-
const MAX_CHARACTER_BYTES = 4 * 1024;
|
|
57
|
-
|
|
58
58
|
export interface AppApiOptions {
|
|
59
|
+
/** The host's wording: a refusal reaches a client of the API in this locale when the catalog has it. */
|
|
60
|
+
presentation?: ConsolePresentation;
|
|
59
61
|
surface: AppSurface;
|
|
60
62
|
rooms: Rooms;
|
|
61
|
-
log: RoomLog
|
|
62
|
-
cards: CardLog
|
|
63
|
+
log: Pick<RoomLog, "page" | "lastOf" | "removeRoom">;
|
|
64
|
+
cards: Pick<CardLog, "during" | "removeRoom">;
|
|
63
65
|
uploads: Uploads;
|
|
64
66
|
/** Where stored files are read from. */
|
|
65
67
|
files: FileDir;
|
|
66
68
|
modes: Pick<ConversationModes, "set" | "peek">;
|
|
67
69
|
/** The rooms whose push the owners muted. */
|
|
68
|
-
mutes: Pick<PushMutes, "set">;
|
|
70
|
+
mutes: Pick<PushMutes, "set" | "setMany">;
|
|
71
|
+
/** The dispatch threads, which go with the room they hang under. */
|
|
72
|
+
threads: Pick<WebThreads, "under" | "removeUnder">;
|
|
73
|
+
/** The owners' shared arrangement of the room list: pins and folders. */
|
|
74
|
+
layout: LayoutStore;
|
|
69
75
|
conversations: Pick<ConversationPort, "startFresh">;
|
|
70
76
|
identity: Pick<IdentityService, "tierOf">;
|
|
71
77
|
/** Present when the schedules pane is served. */
|
|
72
78
|
schedules?: Pick<ScheduleStore, "all" | "remove">;
|
|
73
|
-
directory: Pick<AgentDirectory, "agent">;
|
|
79
|
+
directory: Pick<AgentDirectory, "agent" | "agents" | "group" | "groups">;
|
|
74
80
|
team: Pick<
|
|
75
81
|
AgentTeam,
|
|
76
|
-
|
|
82
|
+
| "update"
|
|
83
|
+
| "promptHistory"
|
|
84
|
+
| "dress"
|
|
85
|
+
| "usableModels"
|
|
86
|
+
| "modelOf"
|
|
87
|
+
| "defaultModel"
|
|
88
|
+
| "createAgent"
|
|
89
|
+
| "archive"
|
|
90
|
+
| "restore"
|
|
91
|
+
| "remove"
|
|
92
|
+
| "createGroup"
|
|
93
|
+
| "updateGroup"
|
|
77
94
|
>;
|
|
95
|
+
/** Present when the host has the skills addon: the catalog an agent's skills are chosen from, and the change of what it carries. */
|
|
96
|
+
skills?: {
|
|
97
|
+
catalog(): SkillView[];
|
|
98
|
+
attach(
|
|
99
|
+
agent: string,
|
|
100
|
+
add: readonly string[],
|
|
101
|
+
remove: readonly string[],
|
|
102
|
+
): Promise<string[]>;
|
|
103
|
+
/** The registry's documents: a skill's SKILL.md read, saved and made, and its versions. */
|
|
104
|
+
documents: Pick<
|
|
105
|
+
SkillRegistry,
|
|
106
|
+
"document" | "saveDocument" | "createDocument" | "documentHistory"
|
|
107
|
+
>;
|
|
108
|
+
};
|
|
109
|
+
/** Present when the host has prompt documents (the prompts plugin): the main prompts the owner edits. */
|
|
110
|
+
prompts?: PromptApiOptions["prompts"];
|
|
78
111
|
avatars: Pick<AvatarStudio, "serve">;
|
|
79
112
|
/** Present when the workspace pane is served. */
|
|
80
113
|
workspace?: WorkspaceFiles;
|
|
@@ -89,19 +122,6 @@ export interface AppApiOptions {
|
|
|
89
122
|
logger: Logger;
|
|
90
123
|
}
|
|
91
124
|
|
|
92
|
-
class HttpError extends Error {
|
|
93
|
-
constructor(
|
|
94
|
-
readonly status: number,
|
|
95
|
-
message: string,
|
|
96
|
-
) {
|
|
97
|
-
super(message);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const bad = (message: string) => new HttpError(400, message);
|
|
102
|
-
const notFound = () =>
|
|
103
|
-
new HttpError(404, "There is no such room, agent or file.");
|
|
104
|
-
|
|
105
125
|
/** A refusal of the workspace pane as the HTTP error the owner reads. */
|
|
106
126
|
function refusalOf(refusal: WorkspaceRefusal): HttpError {
|
|
107
127
|
const { status } = refusal;
|
|
@@ -183,33 +203,6 @@ export function partsOf(path: string): string[] | undefined {
|
|
|
183
203
|
}
|
|
184
204
|
}
|
|
185
205
|
|
|
186
|
-
async function bodyOf(
|
|
187
|
-
request: Request,
|
|
188
|
-
limit = MAX_BODY_BYTES,
|
|
189
|
-
): Promise<Record<string, unknown>> {
|
|
190
|
-
const raw = await readLimited(request, limit);
|
|
191
|
-
if (!raw) throw new HttpError(413, "The body is too large.");
|
|
192
|
-
let value: unknown;
|
|
193
|
-
try {
|
|
194
|
-
value = JSON.parse(raw.toString("utf8"));
|
|
195
|
-
} catch {
|
|
196
|
-
throw bad("The body is not JSON.");
|
|
197
|
-
}
|
|
198
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
199
|
-
throw bad("The body is not an object.");
|
|
200
|
-
return value as Record<string, unknown>;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const optionalText = (
|
|
204
|
-
body: Record<string, unknown>,
|
|
205
|
-
key: string,
|
|
206
|
-
): string | undefined => {
|
|
207
|
-
const value = body[key];
|
|
208
|
-
if (value === undefined) return undefined;
|
|
209
|
-
if (typeof value !== "string") throw bad(`${key} must be text.`);
|
|
210
|
-
return value;
|
|
211
|
-
};
|
|
212
|
-
|
|
213
206
|
/**
|
|
214
207
|
* The owner app's REST under `<mount>/api/` and `<mount>/avatars/`. Every route is behind the
|
|
215
208
|
* authenticator, which also checks the `Origin` of a write; a write also reads the person's tier
|
|
@@ -218,9 +211,44 @@ const optionalText = (
|
|
|
218
211
|
*/
|
|
219
212
|
export class AppApi {
|
|
220
213
|
readonly #options: AppApiOptions;
|
|
214
|
+
readonly #team: TeamApi;
|
|
215
|
+
readonly #layout: LayoutRoutes;
|
|
216
|
+
readonly #prompts: PromptApi | undefined;
|
|
217
|
+
readonly #skillDocs: SkillDocApi | undefined;
|
|
221
218
|
|
|
222
219
|
constructor(options: AppApiOptions) {
|
|
223
220
|
this.#options = options;
|
|
221
|
+
this.#layout = new LayoutRoutes({
|
|
222
|
+
layout: options.layout,
|
|
223
|
+
mutes: options.mutes,
|
|
224
|
+
placeable: () => options.rooms.placeable(),
|
|
225
|
+
home: () => options.rooms.homeKey(),
|
|
226
|
+
owner: (admission) => this.#owner(admission),
|
|
227
|
+
changed: () => options.changed(),
|
|
228
|
+
});
|
|
229
|
+
this.#prompts = options.prompts
|
|
230
|
+
? new PromptApi({ prompts: options.prompts })
|
|
231
|
+
: undefined;
|
|
232
|
+
this.#skillDocs = options.skills
|
|
233
|
+
? new SkillDocApi({
|
|
234
|
+
skills: options.skills.documents,
|
|
235
|
+
catalog: () => options.skills?.catalog() ?? [],
|
|
236
|
+
})
|
|
237
|
+
: undefined;
|
|
238
|
+
this.#team = new TeamApi({
|
|
239
|
+
surface: options.surface.surface,
|
|
240
|
+
rooms: options.rooms,
|
|
241
|
+
directory: options.directory,
|
|
242
|
+
team: options.team,
|
|
243
|
+
skills: options.skills,
|
|
244
|
+
log: options.log,
|
|
245
|
+
cards: options.cards,
|
|
246
|
+
mutes: options.mutes,
|
|
247
|
+
modes: options.modes,
|
|
248
|
+
threads: options.threads,
|
|
249
|
+
layout: options.layout,
|
|
250
|
+
changed: () => options.changed(),
|
|
251
|
+
});
|
|
224
252
|
}
|
|
225
253
|
|
|
226
254
|
/** Answers a path of the app's, or undefined for any other. `rest` is what follows the mount. */
|
|
@@ -228,6 +256,42 @@ export class AppApi {
|
|
|
228
256
|
request: Request,
|
|
229
257
|
rest: string,
|
|
230
258
|
admission: Authenticated,
|
|
259
|
+
): Promise<Response | undefined> {
|
|
260
|
+
const response = await this.#answer(request, rest, admission);
|
|
261
|
+
return response && this.#options.presentation
|
|
262
|
+
? this.#worded(response, this.#options.presentation)
|
|
263
|
+
: response;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** A refusal's `error` in the host's wording; the page words it again from the same catalog and finds the wording already there. */
|
|
267
|
+
async #worded(
|
|
268
|
+
response: Response,
|
|
269
|
+
presentation: ConsolePresentation,
|
|
270
|
+
): Promise<Response> {
|
|
271
|
+
if (response.status < 400) return response;
|
|
272
|
+
const body: unknown = await response
|
|
273
|
+
.clone()
|
|
274
|
+
.json()
|
|
275
|
+
.catch(() => undefined);
|
|
276
|
+
const error = (body as { error?: unknown } | undefined)?.error;
|
|
277
|
+
if (typeof error !== "string") return response;
|
|
278
|
+
const worded = message(presentation, error);
|
|
279
|
+
if (worded === error) return response;
|
|
280
|
+
const headers = new Headers(response.headers);
|
|
281
|
+
headers.delete("content-length");
|
|
282
|
+
return new Response(
|
|
283
|
+
JSON.stringify({ ...(body as object), error: worded }),
|
|
284
|
+
{
|
|
285
|
+
status: response.status,
|
|
286
|
+
headers,
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async #answer(
|
|
292
|
+
request: Request,
|
|
293
|
+
rest: string,
|
|
294
|
+
admission: Authenticated,
|
|
231
295
|
): Promise<Response | undefined> {
|
|
232
296
|
const parts = partsOf(rest);
|
|
233
297
|
if (!parts) return undefined;
|
|
@@ -238,12 +302,17 @@ export class AppApi {
|
|
|
238
302
|
[
|
|
239
303
|
"session",
|
|
240
304
|
"rooms",
|
|
305
|
+
"folders",
|
|
241
306
|
"uploads",
|
|
242
307
|
"files",
|
|
243
308
|
"schedules",
|
|
244
309
|
"agents",
|
|
310
|
+
"groups",
|
|
311
|
+
"archive",
|
|
245
312
|
"models",
|
|
246
313
|
"workspace",
|
|
314
|
+
"prompts",
|
|
315
|
+
"skill-docs",
|
|
247
316
|
].includes(second ?? ""));
|
|
248
317
|
if (!mine) return undefined;
|
|
249
318
|
try {
|
|
@@ -305,6 +374,8 @@ export class AppApi {
|
|
|
305
374
|
models: await this.#options.team.usableModels(),
|
|
306
375
|
} satisfies ModelsView);
|
|
307
376
|
if (area === "rooms") return this.#rooms(request, parts, admission);
|
|
377
|
+
if (area === "folders")
|
|
378
|
+
return this.#layout.folders(request, parts, admission);
|
|
308
379
|
if (area === "uploads" && method === "POST" && !id) {
|
|
309
380
|
const owner = await this.#owner(admission);
|
|
310
381
|
return json(
|
|
@@ -321,8 +392,21 @@ export class AppApi {
|
|
|
321
392
|
return this.#file(id);
|
|
322
393
|
if (area === "workspace") return this.#workspace(request, parts, admission);
|
|
323
394
|
if (area === "schedules") return this.#schedules(method, id, admission);
|
|
324
|
-
|
|
325
|
-
|
|
395
|
+
const owner = () => this.#owner(admission);
|
|
396
|
+
if (area === "agents") return this.#team.agents(request, parts, owner);
|
|
397
|
+
if (area === "groups") return this.#team.groups(request, parts, owner);
|
|
398
|
+
if (area === "archive" && method === "GET" && !id)
|
|
399
|
+
return this.#team.archived(owner);
|
|
400
|
+
if (area === "prompts") {
|
|
401
|
+
if (!this.#prompts)
|
|
402
|
+
throw new HttpError(404, "This host has no prompts to edit.");
|
|
403
|
+
return this.#prompts.handle(request, parts, owner);
|
|
404
|
+
}
|
|
405
|
+
if (area === "skill-docs") {
|
|
406
|
+
if (!this.#skillDocs)
|
|
407
|
+
throw new HttpError(404, "This host has no skills.");
|
|
408
|
+
return this.#skillDocs.handle(request, parts, owner);
|
|
409
|
+
}
|
|
326
410
|
throw new HttpError(404, "There is no such API.");
|
|
327
411
|
}
|
|
328
412
|
|
|
@@ -333,6 +417,8 @@ export class AppApi {
|
|
|
333
417
|
surface: this.#options.surface.surface,
|
|
334
418
|
auth: this.#options.authMode,
|
|
335
419
|
push: true,
|
|
420
|
+
...(this.#options.prompts ? { prompts: true } : {}),
|
|
421
|
+
...(this.#options.skills ? { skillEdit: true } : {}),
|
|
336
422
|
...(this.#options.quietHours
|
|
337
423
|
? { quietHours: this.#options.quietHours }
|
|
338
424
|
: {}),
|
|
@@ -352,7 +438,10 @@ export class AppApi {
|
|
|
352
438
|
const { rooms, log, cards, modes, mutes, conversations } = this.#options;
|
|
353
439
|
if (!key) {
|
|
354
440
|
if (method !== "GET") throw new HttpError(405, "Method not allowed.");
|
|
355
|
-
return json({
|
|
441
|
+
return json({
|
|
442
|
+
rooms: await rooms.list(),
|
|
443
|
+
...(await this.#layout.view()),
|
|
444
|
+
} satisfies RoomsView);
|
|
356
445
|
}
|
|
357
446
|
const room = await rooms.resolve(key);
|
|
358
447
|
if (!room) throw notFound();
|
|
@@ -404,6 +493,12 @@ export class AppApi {
|
|
|
404
493
|
await conversations.startFresh(room.key);
|
|
405
494
|
return json({ ok: true });
|
|
406
495
|
}
|
|
496
|
+
if (
|
|
497
|
+
(action === "folder" || action === "pin") &&
|
|
498
|
+
method === "PUT" &&
|
|
499
|
+
parts.length === 4
|
|
500
|
+
)
|
|
501
|
+
return this.#layout.place(request, room, action, admission);
|
|
407
502
|
if (action === "mute" && method === "PUT" && parts.length === 4) {
|
|
408
503
|
// Only an agent's or group's room has a mute of its own; a thread's posts are its parent's business.
|
|
409
504
|
if (room.readOnly) throw notFound();
|
|
@@ -582,75 +677,4 @@ export class AppApi {
|
|
|
582
677
|
}
|
|
583
678
|
throw new HttpError(405, "Method not allowed.");
|
|
584
679
|
}
|
|
585
|
-
|
|
586
|
-
async #agent(
|
|
587
|
-
request: Request,
|
|
588
|
-
name: string,
|
|
589
|
-
action: string | undefined,
|
|
590
|
-
admission: Authenticated,
|
|
591
|
-
): Promise<Response> {
|
|
592
|
-
const { directory, team } = this.#options;
|
|
593
|
-
const agent = directory.agent(name);
|
|
594
|
-
if (agent?.status !== "active") throw notFound();
|
|
595
|
-
if (!action && request.method === "GET") return json(this.#profile(name));
|
|
596
|
-
if (!action && request.method === "PATCH") {
|
|
597
|
-
await this.#owner(admission);
|
|
598
|
-
const body = await bodyOf(request);
|
|
599
|
-
const patch: AgentPatch = {};
|
|
600
|
-
for (const key of [
|
|
601
|
-
"displayName",
|
|
602
|
-
"prompt",
|
|
603
|
-
"model",
|
|
604
|
-
"thinking",
|
|
605
|
-
] as const) {
|
|
606
|
-
const value = optionalText(body, key);
|
|
607
|
-
if (value !== undefined) patch[key] = value;
|
|
608
|
-
}
|
|
609
|
-
const change: AgentChange = patch;
|
|
610
|
-
await team.update(name, change);
|
|
611
|
-
this.#options.changed();
|
|
612
|
-
return json(this.#profile(name));
|
|
613
|
-
}
|
|
614
|
-
if (action === "character" && request.method === "PUT") {
|
|
615
|
-
await this.#owner(admission);
|
|
616
|
-
const body = await bodyOf(request, MAX_CHARACTER_BYTES);
|
|
617
|
-
const { character } = body;
|
|
618
|
-
if (
|
|
619
|
-
typeof character !== "object" ||
|
|
620
|
-
character === null ||
|
|
621
|
-
Array.isArray(character)
|
|
622
|
-
)
|
|
623
|
-
throw bad("character must be an object.");
|
|
624
|
-
const asked: CharacterRequest = {
|
|
625
|
-
character: character as CharacterRequest["character"],
|
|
626
|
-
};
|
|
627
|
-
// `dress` words a refusal as an agent error; checking here first names the field.
|
|
628
|
-
resolveCharacter(asked.character, characterOf(agent));
|
|
629
|
-
await team.dress(name, asked.character);
|
|
630
|
-
this.#options.changed();
|
|
631
|
-
return json(this.#profile(name));
|
|
632
|
-
}
|
|
633
|
-
throw new HttpError(404, "There is no such API.");
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
#profile(name: string): AgentProfileView {
|
|
637
|
-
const { directory, team, rooms, surface } = this.#options;
|
|
638
|
-
const agent = directory.agent(name);
|
|
639
|
-
if (!agent) throw notFound();
|
|
640
|
-
const effective = team.modelOf(name);
|
|
641
|
-
return {
|
|
642
|
-
name: agent.name,
|
|
643
|
-
displayName: agent.displayName,
|
|
644
|
-
prompt: agent.prompt,
|
|
645
|
-
...(agent.model ? { model: agent.model } : {}),
|
|
646
|
-
...(agent.thinking ? { thinking: agent.thinking } : {}),
|
|
647
|
-
effective: {
|
|
648
|
-
model: effective.model,
|
|
649
|
-
thinking: String(effective.thinking),
|
|
650
|
-
},
|
|
651
|
-
avatar: rooms.avatarOf(agent),
|
|
652
|
-
character: characterOf(agent),
|
|
653
|
-
room: `${surface.surface}:agent.${agent.name}`,
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
680
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { SkillDocument, SkillRegistry } from "pi-roundtable";
|
|
2
|
+
import { SkillConflictError } from "pi-roundtable";
|
|
3
|
+
import type {
|
|
4
|
+
SkillConflict,
|
|
5
|
+
SkillDocCreate,
|
|
6
|
+
SkillDocSave,
|
|
7
|
+
SkillDocView,
|
|
8
|
+
SkillHistoryView,
|
|
9
|
+
SkillView,
|
|
10
|
+
} from "../api-types.ts";
|
|
11
|
+
import { json } from "../http.ts";
|
|
12
|
+
import { bad, bodyOf, HttpError } from "./api-http.ts";
|
|
13
|
+
import type { OwnerOf } from "./team-routes.ts";
|
|
14
|
+
|
|
15
|
+
/** The largest body of a save: a SKILL.md of 12,000 characters may be four bytes each, with its frontmatter. */
|
|
16
|
+
const MAX_SKILL_BYTES = 64 * 1024;
|
|
17
|
+
|
|
18
|
+
export interface SkillDocApiOptions {
|
|
19
|
+
skills: Pick<
|
|
20
|
+
SkillRegistry,
|
|
21
|
+
"document" | "saveDocument" | "createDocument" | "documentHistory"
|
|
22
|
+
>;
|
|
23
|
+
/** The catalog the page lists, so a name it does not list is a 404 before the registry is asked. */
|
|
24
|
+
catalog(): SkillView[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const viewOf = (document: SkillDocument): SkillDocView => ({
|
|
28
|
+
name: document.name,
|
|
29
|
+
source: document.source,
|
|
30
|
+
text: document.text,
|
|
31
|
+
hash: document.hash,
|
|
32
|
+
writable: document.writable,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The app's REST for a skill's SKILL.md: reading it whole, saving an agent-written skill's, making a
|
|
37
|
+
* skill and listing the versions kept. The frontmatter rules are the skill tools': the registry
|
|
38
|
+
* checks them, and a refusal is a 400 with its words. Every call is an owner's; a save names the
|
|
39
|
+
* hash it was edited from, and a skill that changed meanwhile answers 409 with what is there now.
|
|
40
|
+
*/
|
|
41
|
+
export class SkillDocApi {
|
|
42
|
+
readonly #options: SkillDocApiOptions;
|
|
43
|
+
|
|
44
|
+
constructor(options: SkillDocApiOptions) {
|
|
45
|
+
this.#options = options;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async handle(
|
|
49
|
+
request: Request,
|
|
50
|
+
parts: string[],
|
|
51
|
+
owner: OwnerOf,
|
|
52
|
+
): Promise<Response> {
|
|
53
|
+
const { method } = request;
|
|
54
|
+
const [, , name, action] = parts;
|
|
55
|
+
const { skills } = this.#options;
|
|
56
|
+
if (!name) {
|
|
57
|
+
if (method !== "POST") throw new HttpError(405, "Method not allowed.");
|
|
58
|
+
const speaker = await owner();
|
|
59
|
+
const body = await bodyOf(request, MAX_SKILL_BYTES);
|
|
60
|
+
if (typeof body.text !== "string") throw bad("text must be text.");
|
|
61
|
+
const made: SkillDocCreate = { text: body.text };
|
|
62
|
+
return json(
|
|
63
|
+
viewOf(await skills.createDocument(made.text, speaker.name)),
|
|
64
|
+
201,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
// The tier is read before the name is looked up, so a person below the owner learns of no skill.
|
|
68
|
+
const speaker = await owner();
|
|
69
|
+
if (!this.#options.catalog().some((skill) => skill.name === name))
|
|
70
|
+
throw new HttpError(404, "There is no such skill.");
|
|
71
|
+
if (!action && method === "GET")
|
|
72
|
+
return json(viewOf(await skills.document(name)));
|
|
73
|
+
if (!action && method === "PUT") {
|
|
74
|
+
const body = await bodyOf(request, MAX_SKILL_BYTES);
|
|
75
|
+
const { text, baseHash } = body;
|
|
76
|
+
if (typeof text !== "string") throw bad("text must be text.");
|
|
77
|
+
if (typeof baseHash !== "string" || !/^[0-9a-f]{64}$/.test(baseHash))
|
|
78
|
+
throw bad("baseHash must be the hash the skill was read with.");
|
|
79
|
+
const save: SkillDocSave = { text, baseHash };
|
|
80
|
+
try {
|
|
81
|
+
return json(
|
|
82
|
+
viewOf(
|
|
83
|
+
await skills.saveDocument(
|
|
84
|
+
name,
|
|
85
|
+
save.text,
|
|
86
|
+
save.baseHash,
|
|
87
|
+
speaker.name,
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error instanceof SkillConflictError) {
|
|
93
|
+
const refused = new HttpError(
|
|
94
|
+
409,
|
|
95
|
+
"This skill changed while you were editing it.",
|
|
96
|
+
);
|
|
97
|
+
return json(
|
|
98
|
+
{
|
|
99
|
+
error: refused.message,
|
|
100
|
+
conflict: "changed",
|
|
101
|
+
current: viewOf(error.current),
|
|
102
|
+
} satisfies SkillConflict,
|
|
103
|
+
refused.status,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (action === "history" && method === "GET" && parts.length === 4) {
|
|
110
|
+
return json({
|
|
111
|
+
versions: (await skills.documentHistory(name)).map((version) => ({
|
|
112
|
+
id: version.id,
|
|
113
|
+
text: version.text,
|
|
114
|
+
hash: version.hash,
|
|
115
|
+
by: version.by,
|
|
116
|
+
at: version.at.toISOString(),
|
|
117
|
+
})),
|
|
118
|
+
} satisfies SkillHistoryView);
|
|
119
|
+
}
|
|
120
|
+
throw new HttpError(404, "There is no such API.");
|
|
121
|
+
}
|
|
122
|
+
}
|