@camox/api-contract 0.44.0 → 0.45.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.
@@ -0,0 +1,277 @@
1
+ import { z } from "zod";
2
+ import { AgentEvent } from "@earendil-works/pi-durable";
3
+ //#region src/threads.d.ts
4
+ /**
5
+ * The kind of the transcript entry recording an editor's message, with its sender and the
6
+ * operation id the studio sent it with as data (`editorMessageDataSchema`).
7
+ */
8
+ export declare const EDITOR_MESSAGE_ENTRY = "camox.editor-message";
9
+ /** The kind of the transcript entry recording that an editor stopped a run, with them as data. */
10
+ export declare const RUN_STOPPED_ENTRY = "camox.run-stopped";
11
+ /** Who sent a message in a thread. */
12
+ export declare const threadSenderSchema: z.ZodObject<{
13
+ id: z.ZodString;
14
+ name: z.ZodString;
15
+ image: z.ZodNullable<z.ZodString>;
16
+ }, z.core.$strip>;
17
+ export type ThreadSender = z.infer<typeof threadSenderSchema>;
18
+ /** Whether `data` names a sender. */
19
+ export declare const isSender: (data: unknown) => data is ThreadSender;
20
+ /** What an editor's message entry records: who sent it, and the operation id it was sent with. */
21
+ export declare const editorMessageDataSchema: z.ZodObject<{
22
+ id: z.ZodString;
23
+ name: z.ZodString;
24
+ image: z.ZodNullable<z.ZodString>;
25
+ operationId: z.ZodOptional<z.ZodString>;
26
+ }, z.core.$strip>;
27
+ /**
28
+ * A draft a Live thread started with its first write, becoming its thread, and the operation ids
29
+ * of the messages of the run that made the write: the studio that sent one of them enters the
30
+ * draft with the thread.
31
+ */
32
+ export declare const startedDraftSchema: z.ZodObject<{
33
+ draftId: z.ZodString;
34
+ operationIds: z.ZodArray<z.ZodString>;
35
+ }, z.core.$strip>;
36
+ export type StartedDraft = z.infer<typeof startedDraftSchema>;
37
+ /** How a write touched what it names. */
38
+ export declare const touchKindSchema: z.ZodEnum<{
39
+ created: "created";
40
+ edited: "edited";
41
+ moved: "moved";
42
+ deleted: "deleted";
43
+ settings: "settings";
44
+ }>;
45
+ export type TouchKind = z.infer<typeof touchKindSchema>;
46
+ export declare const touchSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
47
+ kind: z.ZodEnum<{
48
+ created: "created";
49
+ edited: "edited";
50
+ moved: "moved";
51
+ deleted: "deleted";
52
+ settings: "settings";
53
+ }>;
54
+ locale: z.ZodOptional<z.ZodString>;
55
+ entity: z.ZodLiteral<"page">;
56
+ pageId: z.ZodString;
57
+ blockId: z.ZodOptional<z.ZodString>;
58
+ }, z.core.$strip>, z.ZodObject<{
59
+ kind: z.ZodEnum<{
60
+ created: "created";
61
+ edited: "edited";
62
+ moved: "moved";
63
+ deleted: "deleted";
64
+ settings: "settings";
65
+ }>;
66
+ locale: z.ZodOptional<z.ZodString>;
67
+ entity: z.ZodLiteral<"synced-block">;
68
+ blockType: z.ZodString;
69
+ }, z.core.$strip>, z.ZodObject<{
70
+ kind: z.ZodEnum<{
71
+ created: "created";
72
+ edited: "edited";
73
+ moved: "moved";
74
+ deleted: "deleted";
75
+ settings: "settings";
76
+ }>;
77
+ locale: z.ZodOptional<z.ZodString>;
78
+ entity: z.ZodLiteral<"layout">;
79
+ layoutId: z.ZodString;
80
+ blockId: z.ZodOptional<z.ZodString>;
81
+ }, z.core.$strip>], "entity">;
82
+ export type Touch = z.infer<typeof touchSchema>;
83
+ /**
84
+ * What one code-mode script's successful writes touched, in write order, and the operation ids
85
+ * of the messages of the run it belongs to: only the studio that sent one of them follows it.
86
+ */
87
+ export declare const touchedBatchSchema: z.ZodObject<{
88
+ operationIds: z.ZodArray<z.ZodString>;
89
+ touches: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
90
+ kind: z.ZodEnum<{
91
+ created: "created";
92
+ edited: "edited";
93
+ moved: "moved";
94
+ deleted: "deleted";
95
+ settings: "settings";
96
+ }>;
97
+ locale: z.ZodOptional<z.ZodString>;
98
+ entity: z.ZodLiteral<"page">;
99
+ pageId: z.ZodString;
100
+ blockId: z.ZodOptional<z.ZodString>;
101
+ }, z.core.$strip>, z.ZodObject<{
102
+ kind: z.ZodEnum<{
103
+ created: "created";
104
+ edited: "edited";
105
+ moved: "moved";
106
+ deleted: "deleted";
107
+ settings: "settings";
108
+ }>;
109
+ locale: z.ZodOptional<z.ZodString>;
110
+ entity: z.ZodLiteral<"synced-block">;
111
+ blockType: z.ZodString;
112
+ }, z.core.$strip>, z.ZodObject<{
113
+ kind: z.ZodEnum<{
114
+ created: "created";
115
+ edited: "edited";
116
+ moved: "moved";
117
+ deleted: "deleted";
118
+ settings: "settings";
119
+ }>;
120
+ locale: z.ZodOptional<z.ZodString>;
121
+ entity: z.ZodLiteral<"layout">;
122
+ layoutId: z.ZodString;
123
+ blockId: z.ZodOptional<z.ZodString>;
124
+ }, z.core.$strip>], "entity">>;
125
+ }, z.core.$strip>;
126
+ export type TouchedBatch = z.infer<typeof touchedBatchSchema>;
127
+ /** A message the studio shows in a thread. */
128
+ export type ThreadMessage = {
129
+ id: string;
130
+ from: "editor";
131
+ sender: ThreadSender;
132
+ text: string;
133
+ } |
134
+ /** The agent's final message of a turn, in Markdown. */
135
+ {
136
+ id: string;
137
+ from: "agent";
138
+ text: string;
139
+ } |
140
+ /**
141
+ * In place of the agent's message, for a turn that ended without one: the model failed (after
142
+ * Pi's retries), or its answer was cut short. Sending again retries.
143
+ */
144
+ {
145
+ id: string;
146
+ from: "failed";
147
+ } |
148
+ /** In place of the agent's message, for a turn an editor stopped: `sender` stopped it. */
149
+ {
150
+ id: string;
151
+ from: "stopped";
152
+ sender: ThreadSender;
153
+ };
154
+ /** Where a Live thread's messages went when it started a draft, beside its transcript. */
155
+ type LiveThreadHandoff = {
156
+ /**
157
+ * The draft this Live thread started with its first write, whose thread it became. The studio
158
+ * that sent a message of that run enters the draft with the thread; other tabs on Live move to
159
+ * the editor's fresh Live thread.
160
+ */
161
+ startedDraft?: StartedDraft;
162
+ /** The draft the editor's previous Live thread continued in, for a fresh Live thread. */
163
+ continuedIn?: StartedDraft;
164
+ };
165
+ /** Everything the studio shows for a thread. */
166
+ export type ThreadView = LiveThreadHandoff & {
167
+ messages: readonly ThreadMessage[];
168
+ /** Whether a run is going: the agent is working on a turn, and stopping would end it. */
169
+ running: boolean;
170
+ };
171
+ export declare const EMPTY_THREAD_VIEW: ThreadView;
172
+ /**
173
+ * What a thread's view is folded from: the steps of its transcript that matter to the studio,
174
+ * and whether a run is going. The studio and the tests fold Pi's events into this, and derive
175
+ * the view from it with `threadViewOf`, so a snapshot and the events that led to it show the same.
176
+ */
177
+ export type ThreadState = LiveThreadHandoff & {
178
+ steps: readonly ThreadStep[];
179
+ running: boolean;
180
+ };
181
+ export declare const EMPTY_THREAD_STATE: ThreadState;
182
+ /** One transcript entry, as far as the studio's view is concerned. */
183
+ type ThreadStep = {
184
+ id: string;
185
+ } & ({
186
+ type: "editor";
187
+ sender: ThreadSender;
188
+ text: string;
189
+ } | {
190
+ type: "answer";
191
+ text: string;
192
+ } |
193
+ /** A round of tool calls, or their results: the run goes on after it. */
194
+ {
195
+ type: "tools";
196
+ } |
197
+ /** A model call that ended without an answer: failed, aborted, or cut short. */
198
+ {
199
+ type: "unfinished";
200
+ } | {
201
+ type: "stopped";
202
+ sender: ThreadSender;
203
+ });
204
+ /** Studio → thread. */
205
+ export declare const threadClientMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
206
+ type: z.ZodLiteral<"send">;
207
+ operationId: z.ZodString;
208
+ text: z.ZodString;
209
+ }, z.core.$strip>, z.ZodObject<{
210
+ type: z.ZodLiteral<"stop">;
211
+ }, z.core.$strip>, z.ZodObject<{
212
+ type: z.ZodLiteral<"resync">;
213
+ }, z.core.$strip>], "type">;
214
+ export type ThreadClientMessage = z.infer<typeof threadClientMessageSchema>;
215
+ /** Thread → studio. */
216
+ export type ThreadServerMessage =
217
+ /**
218
+ * Pi's agent events. The first batch after connecting or resyncing starts with a `snapshot`,
219
+ * which replaces whatever the studio folded before.
220
+ */
221
+ {
222
+ type: "events";
223
+ events: readonly AgentEvent[];
224
+ } |
225
+ /** The thread recorded the message sent with this operation id (once, however often sent). */
226
+ {
227
+ type: "accepted";
228
+ operationId: string;
229
+ } |
230
+ /** This Live thread started a draft with its first write, and became that draft's thread. */
231
+ {
232
+ type: "started-draft";
233
+ startedDraft: StartedDraft;
234
+ } |
235
+ /**
236
+ * The editor's previous Live thread continued in this draft: this one is fresh. Sent before
237
+ * the snapshot.
238
+ */
239
+ {
240
+ type: "continued-in";
241
+ continuedIn: StartedDraft;
242
+ } |
243
+ /**
244
+ * A code-mode script that wrote something ended, failed or not: what its successful writes
245
+ * touched. Sent once per such script, live only: a snapshot never replays past touches.
246
+ */
247
+ {
248
+ type: "touched";
249
+ touched: TouchedBatch;
250
+ } | {
251
+ type: "error";
252
+ operationId?: string;
253
+ message: string;
254
+ };
255
+ /**
256
+ * The code a thread closes its sockets with once it ended with its draft, and for good: it's
257
+ * deleted, so reconnecting reaches nothing. A development draft, which stays open, gets a new
258
+ * thread the next time the studio connects.
259
+ */
260
+ export declare const THREAD_ENDED_CLOSE_CODE = 4000;
261
+ /**
262
+ * What the studio shows for a thread. A turn starts with an editor's message, and messages sent
263
+ * while it runs join it. It ends with the agent's final message; text written beside tool calls
264
+ * is never shown. A stopped turn shows who stopped it instead, and whatever its run wrote after
265
+ * the stop is left out. A turn that ended without either shows a failed line in its place: its
266
+ * run is over, or a later message started another run after its last model call came back
267
+ * unfinished. While Pi retries a failed call, the run goes on, so the turn shows as working.
268
+ */
269
+ export declare function threadViewOf({ steps, running, ...drafts }: ThreadState): ThreadView;
270
+ /** Folds one of Pi's agent events into the thread's state. */
271
+ export declare function reduceThreadState(state: ThreadState, event: AgentEvent): ThreadState;
272
+ /** Folds a batch of Pi's agent events into the thread's state. */
273
+ export declare const reduceThreadEvents: (state: ThreadState, events: readonly AgentEvent[]) => ThreadState;
274
+ /** Folds what the thread sent over its socket into the thread's state. */
275
+ export declare function reduceThreadFrame(state: ThreadState, frame: ThreadServerMessage): ThreadState;
276
+ //#endregion
277
+ export {};
@@ -0,0 +1,263 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/threads.ts
4
+ /**
5
+ * The kind of the transcript entry recording an editor's message, with its sender and the
6
+ * operation id the studio sent it with as data (`editorMessageDataSchema`).
7
+ */
8
+ const EDITOR_MESSAGE_ENTRY = "camox.editor-message";
9
+ /** The kind of the transcript entry recording that an editor stopped a run, with them as data. */
10
+ const RUN_STOPPED_ENTRY = "camox.run-stopped";
11
+ /** Who sent a message in a thread. */
12
+ const threadSenderSchema = z.object({
13
+ id: z.string(),
14
+ name: z.string(),
15
+ image: z.string().nullable()
16
+ });
17
+ /** Whether `data` names a sender. */
18
+ const isSender = (data) => threadSenderSchema.safeParse(data).success;
19
+ /** What an editor's message entry records: who sent it, and the operation id it was sent with. */
20
+ const editorMessageDataSchema = threadSenderSchema.extend({ operationId: z.string().optional() });
21
+ /**
22
+ * A draft a Live thread started with its first write, becoming its thread, and the operation ids
23
+ * of the messages of the run that made the write: the studio that sent one of them enters the
24
+ * draft with the thread.
25
+ */
26
+ const startedDraftSchema = z.object({
27
+ draftId: z.string(),
28
+ operationIds: z.array(z.string())
29
+ });
30
+ /** How a write touched what it names. */
31
+ const touchKindSchema = z.enum([
32
+ "created",
33
+ "edited",
34
+ "moved",
35
+ "deleted",
36
+ "settings"
37
+ ]);
38
+ /**
39
+ * One thing a script's successful write touched, named so the studio can locate it: a Page (and
40
+ * its Block, if any), a Synced Block's shared content, by its block definition, or a Layout (and
41
+ * its Block, if any). `settings` is a Page's settings only (nickname, path, SEO, layout). `locale`
42
+ * is the Locale the write was in, for a localized tool. Writes to Collection Records, Files and
43
+ * Locales aren't on the Canvas, so they're left out.
44
+ */
45
+ const touchBaseSchema = z.object({
46
+ kind: touchKindSchema,
47
+ locale: z.string().optional()
48
+ });
49
+ const touchSchema = z.discriminatedUnion("entity", [
50
+ touchBaseSchema.extend({
51
+ entity: z.literal("page"),
52
+ pageId: z.string(),
53
+ blockId: z.string().optional()
54
+ }),
55
+ touchBaseSchema.extend({
56
+ entity: z.literal("synced-block"),
57
+ /** The Synced Block's block definition id. */
58
+ blockType: z.string()
59
+ }),
60
+ touchBaseSchema.extend({
61
+ entity: z.literal("layout"),
62
+ layoutId: z.string(),
63
+ blockId: z.string().optional()
64
+ })
65
+ ]);
66
+ /**
67
+ * What one code-mode script's successful writes touched, in write order, and the operation ids
68
+ * of the messages of the run it belongs to: only the studio that sent one of them follows it.
69
+ */
70
+ const touchedBatchSchema = z.object({
71
+ operationIds: z.array(z.string()),
72
+ touches: z.array(touchSchema)
73
+ });
74
+ const EMPTY_THREAD_VIEW = {
75
+ messages: [],
76
+ running: false
77
+ };
78
+ const EMPTY_THREAD_STATE = {
79
+ steps: [],
80
+ running: false
81
+ };
82
+ /** Studio → thread. */
83
+ const threadClientMessageSchema = z.discriminatedUnion("type", [
84
+ z.object({
85
+ type: z.literal("send"),
86
+ operationId: z.string().min(1),
87
+ text: z.string().min(1)
88
+ }),
89
+ z.object({ type: z.literal("stop") }),
90
+ z.object({ type: z.literal("resync") })
91
+ ]);
92
+ /**
93
+ * The code a thread closes its sockets with once it ended with its draft, and for good: it's
94
+ * deleted, so reconnecting reaches nothing. A development draft, which stays open, gets a new
95
+ * thread the next time the studio connects.
96
+ */
97
+ const THREAD_ENDED_CLOSE_CODE = 4e3;
98
+ const textOf = (content) => typeof content === "string" ? content : content.flatMap((part) => part.type === "text" ? [part.text] : []).join("").trim();
99
+ /** What an assistant message is to the thread: a final answer, a tool round, or neither. */
100
+ function assistantStep(id, message) {
101
+ if (message.content.some((part) => part.type === "toolCall")) return {
102
+ id,
103
+ type: "tools"
104
+ };
105
+ const text = message.stopReason === "stop" ? textOf(message.content) : "";
106
+ return text ? {
107
+ id,
108
+ type: "answer",
109
+ text
110
+ } : {
111
+ id,
112
+ type: "unfinished"
113
+ };
114
+ }
115
+ /** The sender an entry's data names, if any, without other fields it may carry. */
116
+ function senderOf(data) {
117
+ if (!isSender(data)) return void 0;
118
+ const { id, name, image } = data;
119
+ return {
120
+ id,
121
+ name,
122
+ image
123
+ };
124
+ }
125
+ /** The step an entry is, if it's one the view depends on. */
126
+ function threadStepOf(entry) {
127
+ const id = String(entry.id);
128
+ const sender = senderOf(entry.data);
129
+ if (entry.kind === "camox.run-stopped" && sender) return {
130
+ id,
131
+ type: "stopped",
132
+ sender
133
+ };
134
+ const message = entry.model?.find((candidate) => candidate.role !== "system");
135
+ if (!message) return void 0;
136
+ if (entry.kind === "camox.editor-message" && message.role === "user" && sender) return {
137
+ id,
138
+ type: "editor",
139
+ sender,
140
+ text: textOf(message.content)
141
+ };
142
+ if (message.role === "assistant") return assistantStep(id, message);
143
+ if (message.role === "toolResult") return {
144
+ id,
145
+ type: "tools"
146
+ };
147
+ }
148
+ /**
149
+ * What the studio shows for a thread. A turn starts with an editor's message, and messages sent
150
+ * while it runs join it. It ends with the agent's final message; text written beside tool calls
151
+ * is never shown. A stopped turn shows who stopped it instead, and whatever its run wrote after
152
+ * the stop is left out. A turn that ended without either shows a failed line in its place: its
153
+ * run is over, or a later message started another run after its last model call came back
154
+ * unfinished. While Pi retries a failed call, the run goes on, so the turn shows as working.
155
+ */
156
+ function threadViewOf({ steps, running, ...drafts }) {
157
+ const messages = [];
158
+ let waiting;
159
+ let unfinished = false;
160
+ /** Ends the turn waiting for the agent's message, if any. */
161
+ const endTurn = () => {
162
+ waiting = void 0;
163
+ unfinished = false;
164
+ };
165
+ /** Ends the turn waiting for the agent's message with a failed line, if any. */
166
+ const fail = () => {
167
+ if (waiting !== void 0) messages.push({
168
+ id: `${waiting}:failed`,
169
+ from: "failed"
170
+ });
171
+ endTurn();
172
+ };
173
+ for (const step of steps) switch (step.type) {
174
+ case "editor":
175
+ if (unfinished) fail();
176
+ messages.push({
177
+ id: step.id,
178
+ from: "editor",
179
+ sender: step.sender,
180
+ text: step.text
181
+ });
182
+ waiting = step.id;
183
+ break;
184
+ case "stopped":
185
+ if (waiting === void 0) break;
186
+ messages.push({
187
+ id: step.id,
188
+ from: "stopped",
189
+ sender: step.sender
190
+ });
191
+ endTurn();
192
+ break;
193
+ case "answer":
194
+ if (waiting === void 0 && messages.at(-1)?.from === "stopped") break;
195
+ messages.push({
196
+ id: step.id,
197
+ from: "agent",
198
+ text: step.text
199
+ });
200
+ endTurn();
201
+ break;
202
+ case "tools":
203
+ unfinished = false;
204
+ break;
205
+ case "unfinished": unfinished = true;
206
+ }
207
+ if (!running) fail();
208
+ return {
209
+ ...drafts,
210
+ messages,
211
+ running
212
+ };
213
+ }
214
+ const append = (state, entry) => {
215
+ const step = entry && threadStepOf(entry);
216
+ if (!step || state.steps.some((known) => known.id === step.id)) return state;
217
+ return {
218
+ ...state,
219
+ steps: [...state.steps, step]
220
+ };
221
+ };
222
+ /** Folds one of Pi's agent events into the thread's state. */
223
+ function reduceThreadState(state, event) {
224
+ switch (event.type) {
225
+ case "snapshot": return {
226
+ ...state,
227
+ steps: event.entries.flatMap((entry) => threadStepOf(entry) ?? []),
228
+ running: event.run !== void 0
229
+ };
230
+ case "run_start": return {
231
+ ...state,
232
+ running: true
233
+ };
234
+ case "run_end": return {
235
+ ...state,
236
+ running: false
237
+ };
238
+ case "message_end":
239
+ case "entry_appended":
240
+ case "tool_execution_end": return append(state, event.entry);
241
+ default: return state;
242
+ }
243
+ }
244
+ /** Folds a batch of Pi's agent events into the thread's state. */
245
+ const reduceThreadEvents = (state, events) => events.reduce(reduceThreadState, state);
246
+ /** Folds what the thread sent over its socket into the thread's state. */
247
+ function reduceThreadFrame(state, frame) {
248
+ switch (frame.type) {
249
+ case "events": return reduceThreadEvents(state, frame.events);
250
+ case "started-draft": return {
251
+ ...state,
252
+ startedDraft: frame.startedDraft
253
+ };
254
+ case "continued-in": return {
255
+ ...state,
256
+ continuedIn: frame.continuedIn
257
+ };
258
+ default: return state;
259
+ }
260
+ }
261
+
262
+ //#endregion
263
+ export { EDITOR_MESSAGE_ENTRY, EMPTY_THREAD_STATE, EMPTY_THREAD_VIEW, RUN_STOPPED_ENTRY, THREAD_ENDED_CLOSE_CODE, editorMessageDataSchema, isSender, reduceThreadEvents, reduceThreadFrame, reduceThreadState, startedDraftSchema, threadClientMessageSchema, threadSenderSchema, threadViewOf, touchKindSchema, touchSchema, touchedBatchSchema };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camox/api-contract",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "src",
@@ -15,23 +15,70 @@
15
15
  "./query-keys": {
16
16
  "types": "./dist/query-keys.d.ts",
17
17
  "default": "./dist/query-keys.js"
18
+ },
19
+ "./ids": {
20
+ "types": "./dist/ids.d.ts",
21
+ "default": "./dist/ids.js"
22
+ },
23
+ "./drafts": {
24
+ "types": "./dist/drafts.d.ts",
25
+ "default": "./dist/drafts.js"
26
+ },
27
+ "./icon-catalog": {
28
+ "types": "./dist/icon-catalog.d.ts",
29
+ "default": "./dist/icon-catalog.js"
30
+ },
31
+ "./analytics": {
32
+ "types": "./dist/analytics.d.ts",
33
+ "default": "./dist/analytics.js"
34
+ },
35
+ "./threads": {
36
+ "types": "./dist/threads.d.ts",
37
+ "default": "./dist/threads.js"
38
+ },
39
+ "./projects": {
40
+ "types": "./dist/projects.d.ts",
41
+ "default": "./dist/projects.js"
18
42
  }
19
43
  },
20
44
  "dependencies": {
21
45
  "@iconify/types": "^2.0.0",
22
46
  "@iconify/utils": "^3.1.7",
23
- "zod": "^4.4.3"
47
+ "zod": "^4.6.7"
24
48
  },
25
49
  "devDependencies": {
26
- "dts-bundle-generator": "^9.5.1",
27
- "esbuild": "^0.25.12"
50
+ "@earendil-works/pi-ai": "1.3.0",
51
+ "@earendil-works/pi-durable": "1.3.0"
52
+ },
53
+ "peerDependencies": {
54
+ "@earendil-works/pi-ai": "1.3.0",
55
+ "@earendil-works/pi-durable": "1.3.0"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "@earendil-works/pi-ai": {
59
+ "optional": true
60
+ },
61
+ "@earendil-works/pi-durable": {
62
+ "optional": true
63
+ }
28
64
  },
29
65
  "nx": {
30
66
  "tags": [
31
67
  "type:pkg"
32
- ]
68
+ ],
69
+ "targets": {
70
+ "build": {
71
+ "inputs": [
72
+ "production",
73
+ "^production",
74
+ "{workspaceRoot}/apps/api/src/**/*",
75
+ "{workspaceRoot}/packages/ai-tools/src/**/*",
76
+ "!{workspaceRoot}/**/*.test.ts"
77
+ ]
78
+ }
79
+ }
33
80
  },
34
81
  "scripts": {
35
- "build": "node build.mjs"
82
+ "build": "vp pack"
36
83
  }
37
84
  }
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * A GA4 measurement ID: `G-` followed by uppercase letters and digits. Shared so the dashboard
5
+ * checks an ID by the API's own rule before saving it.
6
+ */
7
+ export const ga4MeasurementIdSchema = /* @__PURE__ */ z
8
+ .string()
9
+ .regex(/^G-[A-Z0-9]+$/, "Not a GA4 measurement ID");
10
+
11
+ /**
12
+ * A PostHog project API key: `phc_` followed by letters and digits. The prefix tells it apart from
13
+ * a personal API key, which is secret and must never reach a public page.
14
+ */
15
+ export const posthogProjectApiKeySchema = /* @__PURE__ */ z
16
+ .string()
17
+ .regex(/^phc_[A-Za-z0-9]+$/, "Not a PostHog project API key");
18
+
19
+ /** The PostHog Cloud region a project lives in. */
20
+ export const posthogRegionSchema = /* @__PURE__ */ z.enum(["us", "eu"]);
21
+
22
+ export type PosthogRegion = z.output<typeof posthogRegionSchema>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Artifact titles, shared by the SDK (frame titles, studio sidebar) and the API (Markdown
3
+ * Representation, write warnings), so every surface names an artifact the same way.
4
+ */
5
+
6
+ export const UNTITLED_ARTIFACT = "Untitled artifact";
7
+
8
+ const entities: Record<string, string> = {
9
+ amp: "&",
10
+ lt: "<",
11
+ gt: ">",
12
+ quot: '"',
13
+ apos: "'",
14
+ nbsp: " ",
15
+ };
16
+
17
+ function decodeEntities(text: string) {
18
+ return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (match, entity: string) => {
19
+ if (entity[0] !== "#") return entities[entity.toLowerCase()] ?? match;
20
+ const code =
21
+ entity[1] === "x" || entity[1] === "X"
22
+ ? Number.parseInt(entity.slice(2), 16)
23
+ : Number(entity.slice(1));
24
+ return Number.isNaN(code) ? match : String.fromCodePoint(code);
25
+ });
26
+ }
27
+
28
+ /** The non-empty `<title>` of an artifact source, if it has one. */
29
+ export function artifactSourceTitle(source: string): string | undefined {
30
+ const match = /<title\b[^>]*>([\s\S]*?)<\/title\s*>/i.exec(source);
31
+ const title = match ? decodeEntities(match[1]).replace(/\s+/g, " ").trim() : "";
32
+ return title || undefined;
33
+ }
34
+
35
+ /** The artifact's name: its source's `<title>`, or "Untitled artifact". */
36
+ export function artifactTitle(source: string): string {
37
+ return artifactSourceTitle(source) ?? UNTITLED_ARTIFACT;
38
+ }