@vellumai/assistant 0.12.0-staging.1 → 0.12.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/Dockerfile +5 -0
- package/docs/architecture/turn-actor.md +9 -0
- package/knip.json +1 -0
- package/node_modules/@vellumai/app-icons/package.json +18 -0
- package/node_modules/@vellumai/app-icons/src/index.test.ts +85 -0
- package/node_modules/@vellumai/app-icons/src/index.ts +387 -0
- package/node_modules/@vellumai/app-icons/tsconfig.json +20 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
- package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +10 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +15 -2
- package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
- package/openapi.yaml +628 -0
- package/package.json +3 -1
- package/scripts/smoke-container-workspace-dependencies.ts +19 -0
- package/src/__tests__/app-builder-icon-names.test.ts +29 -0
- package/src/__tests__/assistant-attachment-directive.test.ts +4 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +2 -0
- package/src/__tests__/conversation-attachments.test.ts +142 -0
- package/src/__tests__/conversation-delete-activation-progress.test.ts +145 -0
- package/src/__tests__/conversation-event-sink.test.ts +50 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +10 -2
- package/src/__tests__/conversation-queue.test.ts +297 -0
- package/src/__tests__/credential-routes.test.ts +185 -6
- package/src/__tests__/drain-kick-guard.test.ts +2 -0
- package/src/__tests__/drain-requeue-on-contention.test.ts +6 -0
- package/src/__tests__/messaging-send-tool.test.ts +42 -0
- package/src/__tests__/oauth-commands-routes.test.ts +47 -0
- package/src/__tests__/subagent-manager-notify.test.ts +25 -4
- package/src/activation/progress-store.test.ts +1564 -0
- package/src/activation/progress-store.ts +1296 -0
- package/src/activation/turn-hooks.test.ts +246 -0
- package/src/activation/turn-hooks.ts +126 -0
- package/src/api/events/subagent-status-changed.ts +7 -5
- package/src/api/responses/activation.ts +136 -0
- package/src/apps/app-store.ts +8 -0
- package/src/cli/__tests__/catalog-search-help.test.ts +7 -5
- package/src/cli/commands/__tests__/cli-test-harness.ts +12 -3
- package/src/cli/commands/channels/__tests__/channels.test.ts +2 -0
- package/src/cli/commands/channels/__tests__/request.test.ts +191 -0
- package/src/cli/commands/channels/index.help.ts +70 -18
- package/src/cli/commands/channels/index.ts +17 -6
- package/src/cli/commands/channels/request.ts +66 -0
- package/src/cli/commands/oauth/request.test.ts +2 -0
- package/src/cli/commands/oauth/request.ts +227 -186
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -1
- package/src/config/bundled-skills/app-builder/TOOLS.json +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +8 -4
- package/src/config/feature-flag-registry.json +35 -5
- package/src/daemon/assistant-attachments.ts +11 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +5 -0
- package/src/daemon/conversation-agent-loop.ts +71 -3
- package/src/daemon/conversation-attachments.ts +42 -1
- package/src/daemon/conversation-event-sink.ts +25 -0
- package/src/daemon/conversation-process.ts +69 -21
- package/src/daemon/conversation-store.ts +4 -3
- package/src/daemon/conversation-surfaces.ts +19 -4
- package/src/daemon/message-types/sync.ts +2 -0
- package/src/ipc/assistant-server.ts +2 -0
- package/src/ipc/routes/__tests__/activation-sync-ipc-routes.test.ts +51 -0
- package/src/ipc/routes/activation-sync-ipc-routes.ts +42 -0
- package/src/notifications/AGENTS.md +1 -1
- package/src/notifications/__tests__/proactive-home-thread.test.ts +111 -0
- package/src/notifications/conversation-pairing.ts +47 -5
- package/src/notifications/delivered-post-record.ts +3 -0
- package/src/persistence/__tests__/slack-thread-root-evidence.test.ts +102 -0
- package/src/persistence/conversation-crud.ts +39 -0
- package/src/persistence/delivery-crud.ts +13 -0
- package/src/plugins/AGENTS.md +1 -0
- package/src/runtime/auth/__tests__/route-policy.test.ts +37 -0
- package/src/runtime/routes/__tests__/user-routes-notices.test.ts +248 -0
- package/src/runtime/routes/activation-routes.test.ts +415 -0
- package/src/runtime/routes/activation-routes.ts +172 -0
- package/src/runtime/routes/credential-routes.ts +46 -3
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/oauth-commands-routes.ts +21 -8
- package/src/runtime/routes/platform-managed-credentials.ts +48 -0
- package/src/runtime/routes/secret-routes.ts +3 -12
- package/src/runtime/routes/user-route-resolution.ts +21 -0
- package/src/runtime/routes/user-routes.ts +112 -9
- package/src/runtime/sync/activation-sidecar-publish.test.ts +78 -0
- package/src/runtime/sync/documents-sidecar-publish.test.ts +3 -0
- package/src/runtime/sync/resource-sync-events.ts +23 -0
- package/src/runtime/sync/worker-daemon-notify.test.ts +36 -0
- package/src/runtime/sync/worker-daemon-notify.ts +39 -1
- package/src/tools/apps/executors.ts +8 -8
package/Dockerfile
CHANGED
|
@@ -108,6 +108,7 @@ RUN ln -sf /usr/local/bin/bun /usr/local/bin/bunx
|
|
|
108
108
|
COPY --link --from=builder /app /app
|
|
109
109
|
|
|
110
110
|
# Copy source after installing dependencies for better layer caching.
|
|
111
|
+
COPY packages/app-icons /app/packages/app-icons
|
|
111
112
|
COPY packages/avatar-catalog /app/packages/avatar-catalog
|
|
112
113
|
COPY packages/avatar-manifest /app/packages/avatar-manifest
|
|
113
114
|
COPY packages/ces-client /app/packages/ces-client
|
|
@@ -122,6 +123,10 @@ COPY packages/twilio-client /app/packages/twilio-client
|
|
|
122
123
|
|
|
123
124
|
COPY assistant /app/assistant
|
|
124
125
|
|
|
126
|
+
# Resolve production workspace dependencies from the filesystem layout shipped
|
|
127
|
+
# in the image.
|
|
128
|
+
RUN bun run scripts/smoke-container-workspace-dependencies.ts
|
|
129
|
+
|
|
125
130
|
# The repo-root .dockerignore decides which skills (and files) ship.
|
|
126
131
|
COPY skills /app/skills
|
|
127
132
|
|
|
@@ -60,6 +60,15 @@ and restore the prior value afterwards, each guarding the restore so a turn that
|
|
|
60
60
|
started in between is not clobbered. They are supplying the acting actor for
|
|
61
61
|
their run, and are covered by this contract.
|
|
62
62
|
|
|
63
|
+
A queued message commits to a run at its drain, not at its enqueue. The drains
|
|
64
|
+
(`drainSingleMessage` and `drainBatch` in `conversation-process.ts`) stamp the
|
|
65
|
+
queued sender the way `processMessage` stamps its committing actor, and re-scope
|
|
66
|
+
the resident history to them, so a turn drained behind another actor's turn
|
|
67
|
+
runs as its sender everywhere the resting actor is read, not only in the
|
|
68
|
+
per-turn snapshot. A steered drain skips the re-scope: its sender owned the
|
|
69
|
+
turn it cut off, and the resident history may carry the in-memory repair of the
|
|
70
|
+
abandoned `tool_use`.
|
|
71
|
+
|
|
63
72
|
`call-controller` keeps its own `trustContext` on its own object and never reads
|
|
64
73
|
the conversation's. It is outside this contract.
|
|
65
74
|
|
package/knip.json
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vellumai/app-icons",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"typecheck": "bunx tsc --noEmit",
|
|
12
|
+
"test": "bun test src/"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/bun": "1.3.10",
|
|
16
|
+
"typescript": "5.9.3"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
APP_ICON_NAMES,
|
|
5
|
+
bridgeEmojiAppIcon,
|
|
6
|
+
EMOJI_ICON_NAMES,
|
|
7
|
+
isAppIconName,
|
|
8
|
+
isEmojiAppIcon,
|
|
9
|
+
normalizeAppIcon,
|
|
10
|
+
} from "./index.js";
|
|
11
|
+
|
|
12
|
+
describe("APP_ICON_NAMES", () => {
|
|
13
|
+
test("is kebab-case and free of duplicates", () => {
|
|
14
|
+
expect(new Set(APP_ICON_NAMES).size).toBe(APP_ICON_NAMES.length);
|
|
15
|
+
for (const name of APP_ICON_NAMES) {
|
|
16
|
+
expect(name).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
|
|
17
|
+
}
|
|
18
|
+
expect(isAppIconName("calculator")).toBe(true);
|
|
19
|
+
expect(isAppIconName("not-an-icon")).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("every emoji maps to a registry name", () => {
|
|
23
|
+
for (const name of Object.values(EMOJI_ICON_NAMES)) {
|
|
24
|
+
expect(isAppIconName(name)).toBe(true);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("isEmojiAppIcon", () => {
|
|
30
|
+
test("accepts an emoji, a sequence, and a keycap", () => {
|
|
31
|
+
expect(isEmojiAppIcon("☕")).toBe(true);
|
|
32
|
+
expect(isEmojiAppIcon("☕️")).toBe(true);
|
|
33
|
+
expect(isEmojiAppIcon("👨💻")).toBe(true);
|
|
34
|
+
expect(isEmojiAppIcon("1️⃣")).toBe(true);
|
|
35
|
+
expect(isEmojiAppIcon("👍🏽")).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("rejects text, text around an emoji, and bare marks", () => {
|
|
39
|
+
expect(isEmojiAppIcon("coffee")).toBe(false);
|
|
40
|
+
expect(isEmojiAppIcon("coffee ☕")).toBe(false);
|
|
41
|
+
expect(isEmojiAppIcon("☕ coffee")).toBe(false);
|
|
42
|
+
expect(isEmojiAppIcon("\u200D")).toBe(false);
|
|
43
|
+
expect(isEmojiAppIcon("")).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("bridgeEmojiAppIcon", () => {
|
|
48
|
+
test("maps a known emoji to its name, with or without the selector", () => {
|
|
49
|
+
expect(bridgeEmojiAppIcon("🔢")).toBe("calculator");
|
|
50
|
+
expect(bridgeEmojiAppIcon("☕️")).toBe("coffee");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("passes everything else through, prototype names included", () => {
|
|
54
|
+
expect(bridgeEmojiAppIcon("calculator")).toBe("calculator");
|
|
55
|
+
expect(bridgeEmojiAppIcon("1️⃣")).toBe("1️⃣");
|
|
56
|
+
expect(bridgeEmojiAppIcon("constructor")).toBe("constructor");
|
|
57
|
+
expect(bridgeEmojiAppIcon(undefined)).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("normalizeAppIcon", () => {
|
|
62
|
+
test("keeps a registry name, normalised to its kebab-case key", () => {
|
|
63
|
+
expect(normalizeAppIcon("calculator")).toBe("calculator");
|
|
64
|
+
expect(normalizeAppIcon(" Calculator ")).toBe("calculator");
|
|
65
|
+
expect(normalizeAppIcon("List-Todo")).toBe("list-todo");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("maps a known emoji and keeps an unknown one", () => {
|
|
69
|
+
expect(normalizeAppIcon("☕")).toBe("coffee");
|
|
70
|
+
expect(normalizeAppIcon("🔢")).toBe("calculator");
|
|
71
|
+
expect(normalizeAppIcon("1️⃣")).toBe("1️⃣");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("drops URLs, text around an emoji, unknown names, empties, non-strings", () => {
|
|
75
|
+
expect(normalizeAppIcon("https://example.com/icon.png")).toBeUndefined();
|
|
76
|
+
expect(normalizeAppIcon("http://x/y.svg")).toBeUndefined();
|
|
77
|
+
expect(normalizeAppIcon("coffee ☕")).toBeUndefined();
|
|
78
|
+
expect(normalizeAppIcon("not-an-icon")).toBeUndefined();
|
|
79
|
+
expect(normalizeAppIcon("constructor")).toBeUndefined();
|
|
80
|
+
expect(normalizeAppIcon("")).toBeUndefined();
|
|
81
|
+
expect(normalizeAppIcon(" ")).toBeUndefined();
|
|
82
|
+
expect(normalizeAppIcon(42)).toBeUndefined();
|
|
83
|
+
expect(normalizeAppIcon(undefined)).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The icons an app can wear, shared by the assistant (which picks and stores
|
|
3
|
+
* one on an app's manifest) and the web client (which draws it).
|
|
4
|
+
*
|
|
5
|
+
* An app's `icon` is a Lucide kebab-case name from {@link APP_ICON_NAMES}.
|
|
6
|
+
* The assistant chooses it at `app_create`; the web client keeps the
|
|
7
|
+
* name-to-glyph map, typed against {@link AppIconName} so a name added here
|
|
8
|
+
* without a glyph there fails its typecheck. A manifest may also carry an
|
|
9
|
+
* emoji in place of a name; {@link EMOJI_ICON_NAMES} maps the common ones to
|
|
10
|
+
* the name each stands for, and both sides apply the map.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const APP_ICON_NAMES = [
|
|
14
|
+
"calculator",
|
|
15
|
+
"calendar",
|
|
16
|
+
"list-todo",
|
|
17
|
+
"list-checks",
|
|
18
|
+
"square-check",
|
|
19
|
+
"timer",
|
|
20
|
+
"clock",
|
|
21
|
+
"alarm-clock",
|
|
22
|
+
"notebook-pen",
|
|
23
|
+
"sticky-note",
|
|
24
|
+
"pencil",
|
|
25
|
+
"file-text",
|
|
26
|
+
"clipboard-list",
|
|
27
|
+
"bookmark",
|
|
28
|
+
"book",
|
|
29
|
+
"book-open",
|
|
30
|
+
"chart-bar",
|
|
31
|
+
"chart-line",
|
|
32
|
+
"chart-pie",
|
|
33
|
+
"table",
|
|
34
|
+
"square-kanban",
|
|
35
|
+
"database",
|
|
36
|
+
"gauge",
|
|
37
|
+
"activity",
|
|
38
|
+
"target",
|
|
39
|
+
"flag",
|
|
40
|
+
"trophy",
|
|
41
|
+
"wallet",
|
|
42
|
+
"dollar-sign",
|
|
43
|
+
"piggy-bank",
|
|
44
|
+
"credit-card",
|
|
45
|
+
"receipt",
|
|
46
|
+
"percent",
|
|
47
|
+
"shopping-cart",
|
|
48
|
+
"package",
|
|
49
|
+
"gift",
|
|
50
|
+
"ticket",
|
|
51
|
+
"mail",
|
|
52
|
+
"inbox",
|
|
53
|
+
"message-square",
|
|
54
|
+
"phone",
|
|
55
|
+
"bell",
|
|
56
|
+
"users",
|
|
57
|
+
"contact",
|
|
58
|
+
"music",
|
|
59
|
+
"headphones",
|
|
60
|
+
"mic",
|
|
61
|
+
"video",
|
|
62
|
+
"film",
|
|
63
|
+
"tv",
|
|
64
|
+
"play",
|
|
65
|
+
"image",
|
|
66
|
+
"camera",
|
|
67
|
+
"gamepad-2",
|
|
68
|
+
"puzzle",
|
|
69
|
+
"party-popper",
|
|
70
|
+
"smile",
|
|
71
|
+
"map",
|
|
72
|
+
"map-pin",
|
|
73
|
+
"compass",
|
|
74
|
+
"globe",
|
|
75
|
+
"plane",
|
|
76
|
+
"car",
|
|
77
|
+
"bus",
|
|
78
|
+
"bike",
|
|
79
|
+
"ship",
|
|
80
|
+
"truck",
|
|
81
|
+
"house",
|
|
82
|
+
"bed",
|
|
83
|
+
"briefcase",
|
|
84
|
+
"graduation-cap",
|
|
85
|
+
"languages",
|
|
86
|
+
"brain",
|
|
87
|
+
"lightbulb",
|
|
88
|
+
"heart",
|
|
89
|
+
"heart-pulse",
|
|
90
|
+
"dumbbell",
|
|
91
|
+
"pill",
|
|
92
|
+
"stethoscope",
|
|
93
|
+
"baby",
|
|
94
|
+
"paw-print",
|
|
95
|
+
"utensils",
|
|
96
|
+
"coffee",
|
|
97
|
+
"wine",
|
|
98
|
+
"beer",
|
|
99
|
+
"cake",
|
|
100
|
+
"apple",
|
|
101
|
+
"carrot",
|
|
102
|
+
"salad",
|
|
103
|
+
"egg",
|
|
104
|
+
"fish",
|
|
105
|
+
"cloud",
|
|
106
|
+
"sun",
|
|
107
|
+
"moon",
|
|
108
|
+
"umbrella",
|
|
109
|
+
"snowflake",
|
|
110
|
+
"thermometer",
|
|
111
|
+
"droplets",
|
|
112
|
+
"flame",
|
|
113
|
+
"leaf",
|
|
114
|
+
"mountain",
|
|
115
|
+
"code",
|
|
116
|
+
"terminal",
|
|
117
|
+
"cpu",
|
|
118
|
+
"bot",
|
|
119
|
+
"wifi",
|
|
120
|
+
"lock",
|
|
121
|
+
"key",
|
|
122
|
+
"shield",
|
|
123
|
+
"settings",
|
|
124
|
+
"wrench",
|
|
125
|
+
"plug",
|
|
126
|
+
"battery",
|
|
127
|
+
"search",
|
|
128
|
+
"link",
|
|
129
|
+
"hash",
|
|
130
|
+
"layers",
|
|
131
|
+
"folder-open",
|
|
132
|
+
"palette",
|
|
133
|
+
"pen-tool",
|
|
134
|
+
"ruler",
|
|
135
|
+
"scale",
|
|
136
|
+
"scissors",
|
|
137
|
+
"shirt",
|
|
138
|
+
"newspaper",
|
|
139
|
+
"repeat",
|
|
140
|
+
"shuffle",
|
|
141
|
+
"volume-2",
|
|
142
|
+
"speaker",
|
|
143
|
+
"star",
|
|
144
|
+
"sparkles",
|
|
145
|
+
"zap",
|
|
146
|
+
"rocket",
|
|
147
|
+
"home",
|
|
148
|
+
] as const;
|
|
149
|
+
|
|
150
|
+
export type AppIconName = (typeof APP_ICON_NAMES)[number];
|
|
151
|
+
|
|
152
|
+
const APP_ICON_NAME_SET: ReadonlySet<string> = new Set(APP_ICON_NAMES);
|
|
153
|
+
|
|
154
|
+
export function isAppIconName(value: string): value is AppIconName {
|
|
155
|
+
return APP_ICON_NAME_SET.has(value);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Emoji a manifest may carry as its icon, each mapped to the registry name
|
|
160
|
+
* it stands for. Keys carry no variation selector; the lookup strips it.
|
|
161
|
+
*/
|
|
162
|
+
export const EMOJI_ICON_NAMES: Readonly<Record<string, AppIconName>> = {
|
|
163
|
+
"🔢": "calculator",
|
|
164
|
+
"🧮": "calculator",
|
|
165
|
+
"📅": "calendar",
|
|
166
|
+
"🗓": "calendar",
|
|
167
|
+
"✅": "list-todo",
|
|
168
|
+
"☑": "list-checks",
|
|
169
|
+
"⏱": "timer",
|
|
170
|
+
"⏲": "timer",
|
|
171
|
+
"⏰": "alarm-clock",
|
|
172
|
+
"🕐": "clock",
|
|
173
|
+
"🕒": "clock",
|
|
174
|
+
"📝": "notebook-pen",
|
|
175
|
+
"🗒": "sticky-note",
|
|
176
|
+
"✏": "pencil",
|
|
177
|
+
"📄": "file-text",
|
|
178
|
+
"📋": "clipboard-list",
|
|
179
|
+
"🔖": "bookmark",
|
|
180
|
+
"📚": "book",
|
|
181
|
+
"📖": "book-open",
|
|
182
|
+
"📔": "notebook-pen",
|
|
183
|
+
"📊": "chart-bar",
|
|
184
|
+
"📈": "chart-line",
|
|
185
|
+
"📉": "chart-line",
|
|
186
|
+
"🥧": "chart-pie",
|
|
187
|
+
"🗄": "database",
|
|
188
|
+
"🎯": "target",
|
|
189
|
+
"🚩": "flag",
|
|
190
|
+
"🏆": "trophy",
|
|
191
|
+
"🏁": "flag",
|
|
192
|
+
"💰": "wallet",
|
|
193
|
+
"💵": "dollar-sign",
|
|
194
|
+
"💲": "dollar-sign",
|
|
195
|
+
"🐷": "piggy-bank",
|
|
196
|
+
"💳": "credit-card",
|
|
197
|
+
"🧾": "receipt",
|
|
198
|
+
"🛒": "shopping-cart",
|
|
199
|
+
"🛍": "shopping-cart",
|
|
200
|
+
"📦": "package",
|
|
201
|
+
"🎁": "gift",
|
|
202
|
+
"🎟": "ticket",
|
|
203
|
+
"🎫": "ticket",
|
|
204
|
+
"✉": "mail",
|
|
205
|
+
"📧": "mail",
|
|
206
|
+
"📨": "mail",
|
|
207
|
+
"📥": "inbox",
|
|
208
|
+
"💬": "message-square",
|
|
209
|
+
"🗨": "message-square",
|
|
210
|
+
"📞": "phone",
|
|
211
|
+
"☎": "phone",
|
|
212
|
+
"🔔": "bell",
|
|
213
|
+
"👥": "users",
|
|
214
|
+
"👤": "contact",
|
|
215
|
+
"🎵": "music",
|
|
216
|
+
"🎶": "music",
|
|
217
|
+
"🎧": "headphones",
|
|
218
|
+
"🎤": "mic",
|
|
219
|
+
"🎙": "mic",
|
|
220
|
+
"📹": "video",
|
|
221
|
+
"🎬": "film",
|
|
222
|
+
"🎥": "film",
|
|
223
|
+
"📺": "tv",
|
|
224
|
+
"▶": "play",
|
|
225
|
+
"🖼": "image",
|
|
226
|
+
"📷": "camera",
|
|
227
|
+
"📸": "camera",
|
|
228
|
+
"🎮": "gamepad-2",
|
|
229
|
+
"🕹": "gamepad-2",
|
|
230
|
+
"🧩": "puzzle",
|
|
231
|
+
"🎉": "party-popper",
|
|
232
|
+
"🎊": "party-popper",
|
|
233
|
+
"😀": "smile",
|
|
234
|
+
"🙂": "smile",
|
|
235
|
+
"🗺": "map",
|
|
236
|
+
"📍": "map-pin",
|
|
237
|
+
"🧭": "compass",
|
|
238
|
+
"🌍": "globe",
|
|
239
|
+
"🌎": "globe",
|
|
240
|
+
"🌏": "globe",
|
|
241
|
+
"✈": "plane",
|
|
242
|
+
"🚗": "car",
|
|
243
|
+
"🚌": "bus",
|
|
244
|
+
"🚲": "bike",
|
|
245
|
+
"🚢": "ship",
|
|
246
|
+
"🚚": "truck",
|
|
247
|
+
"🏠": "house",
|
|
248
|
+
"🏡": "house",
|
|
249
|
+
"🛏": "bed",
|
|
250
|
+
"💼": "briefcase",
|
|
251
|
+
"🎓": "graduation-cap",
|
|
252
|
+
"🌐": "languages",
|
|
253
|
+
"🧠": "brain",
|
|
254
|
+
"💡": "lightbulb",
|
|
255
|
+
"❤": "heart",
|
|
256
|
+
"💗": "heart-pulse",
|
|
257
|
+
"🏋": "dumbbell",
|
|
258
|
+
"💪": "dumbbell",
|
|
259
|
+
"💊": "pill",
|
|
260
|
+
"🩺": "stethoscope",
|
|
261
|
+
"👶": "baby",
|
|
262
|
+
"🐾": "paw-print",
|
|
263
|
+
"🐶": "paw-print",
|
|
264
|
+
"🐱": "paw-print",
|
|
265
|
+
"🍽": "utensils",
|
|
266
|
+
"🍴": "utensils",
|
|
267
|
+
"☕": "coffee",
|
|
268
|
+
"🍷": "wine",
|
|
269
|
+
"🍺": "beer",
|
|
270
|
+
"🍰": "cake",
|
|
271
|
+
"🎂": "cake",
|
|
272
|
+
"🍎": "apple",
|
|
273
|
+
"🥕": "carrot",
|
|
274
|
+
"🥗": "salad",
|
|
275
|
+
"🥚": "egg",
|
|
276
|
+
"🐟": "fish",
|
|
277
|
+
"☁": "cloud",
|
|
278
|
+
"🌤": "cloud",
|
|
279
|
+
"☀": "sun",
|
|
280
|
+
"🌞": "sun",
|
|
281
|
+
"🌙": "moon",
|
|
282
|
+
"☂": "umbrella",
|
|
283
|
+
"🌧": "umbrella",
|
|
284
|
+
"❄": "snowflake",
|
|
285
|
+
"🌡": "thermometer",
|
|
286
|
+
"💧": "droplets",
|
|
287
|
+
"🔥": "flame",
|
|
288
|
+
"🌱": "leaf",
|
|
289
|
+
"🍃": "leaf",
|
|
290
|
+
"🏔": "mountain",
|
|
291
|
+
"⛰": "mountain",
|
|
292
|
+
"💻": "code",
|
|
293
|
+
"👨💻": "code",
|
|
294
|
+
"🖥": "terminal",
|
|
295
|
+
"⌨": "terminal",
|
|
296
|
+
"🤖": "bot",
|
|
297
|
+
"📡": "wifi",
|
|
298
|
+
"🔒": "lock",
|
|
299
|
+
"🔐": "lock",
|
|
300
|
+
"🔑": "key",
|
|
301
|
+
"🛡": "shield",
|
|
302
|
+
"⚙": "settings",
|
|
303
|
+
"🔧": "wrench",
|
|
304
|
+
"🛠": "wrench",
|
|
305
|
+
"🔌": "plug",
|
|
306
|
+
"🔋": "battery",
|
|
307
|
+
"🔍": "search",
|
|
308
|
+
"🔎": "search",
|
|
309
|
+
"🔗": "link",
|
|
310
|
+
"#️⃣": "hash",
|
|
311
|
+
"📁": "folder-open",
|
|
312
|
+
"📂": "folder-open",
|
|
313
|
+
"🎨": "palette",
|
|
314
|
+
"🖌": "pen-tool",
|
|
315
|
+
"📏": "ruler",
|
|
316
|
+
"⚖": "scale",
|
|
317
|
+
"✂": "scissors",
|
|
318
|
+
"👕": "shirt",
|
|
319
|
+
"📰": "newspaper",
|
|
320
|
+
"🔁": "repeat",
|
|
321
|
+
"🔀": "shuffle",
|
|
322
|
+
"🔊": "volume-2",
|
|
323
|
+
"⭐": "star",
|
|
324
|
+
"🌟": "star",
|
|
325
|
+
"✨": "sparkles",
|
|
326
|
+
"⚡": "zap",
|
|
327
|
+
"🚀": "rocket",
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const VARIATION_SELECTORS = /[\uFE0E\uFE0F]/gu;
|
|
331
|
+
|
|
332
|
+
/* A value that is nothing but emoji: pictographs, emoji-presentation
|
|
333
|
+
characters, skin-tone modifiers, the joiner that composes a sequence, the
|
|
334
|
+
variation selectors, and keycap sequences (a digit, `#` or `*` with the
|
|
335
|
+
keycap mark). Anchored, so text with an emoji in it is not an emoji. */
|
|
336
|
+
const EMOJI_ONLY =
|
|
337
|
+
/^(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|\p{Emoji_Modifier}|\u200D|\uFE0E|\uFE0F|[0-9#*]\uFE0F?\u20E3)+$/u;
|
|
338
|
+
/* At least one glyph-bearing character, so a bare joiner or selector is not
|
|
339
|
+
an emoji either. */
|
|
340
|
+
const EMOJI_GLYPH = /\p{Extended_Pictographic}|\p{Emoji_Presentation}|\u20E3/u;
|
|
341
|
+
|
|
342
|
+
/** True when `value` is an emoji (or emoji sequence) and nothing else. */
|
|
343
|
+
export function isEmojiAppIcon(value: string): boolean {
|
|
344
|
+
return EMOJI_ONLY.test(value) && EMOJI_GLYPH.test(value);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* A stored icon as a client should read it: an emoji the map knows becomes
|
|
349
|
+
* its registry name, and anything else passes through untouched, so reading
|
|
350
|
+
* a manifest never loses an icon the map cannot place.
|
|
351
|
+
*/
|
|
352
|
+
export function bridgeEmojiAppIcon(
|
|
353
|
+
icon: string | undefined,
|
|
354
|
+
): string | undefined {
|
|
355
|
+
if (!icon) {
|
|
356
|
+
return icon;
|
|
357
|
+
}
|
|
358
|
+
const key = icon.trim().replace(VARIATION_SELECTORS, "");
|
|
359
|
+
/* An own-property check: a bare index would find `constructor` and its
|
|
360
|
+
kin on the object's prototype. */
|
|
361
|
+
return Object.hasOwn(EMOJI_ICON_NAMES, key) ? EMOJI_ICON_NAMES[key] : icon;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* The icon to persist on an app's manifest for a value the model passed: a
|
|
366
|
+
* registry name (any case) as its kebab-case key, an emoji as the name it
|
|
367
|
+
* maps to (or as itself when the map has none), and `undefined` for anything
|
|
368
|
+
* else. URLs and text around an emoji are dropped: they would render as raw
|
|
369
|
+
* strings, and an image icon is `app_generate_icon`'s job.
|
|
370
|
+
*/
|
|
371
|
+
export function normalizeAppIcon(raw: unknown): string | undefined {
|
|
372
|
+
if (typeof raw !== "string") {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
const trimmed = raw.trim();
|
|
376
|
+
if (trimmed === "" || /^https?:\/\//i.test(trimmed)) {
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
const name = trimmed.toLowerCase();
|
|
380
|
+
if (isAppIconName(name)) {
|
|
381
|
+
return name;
|
|
382
|
+
}
|
|
383
|
+
if (isEmojiAppIcon(trimmed)) {
|
|
384
|
+
return bridgeEmojiAppIcon(trimmed);
|
|
385
|
+
}
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"sourceMap": true,
|
|
14
|
+
"outDir": "./dist",
|
|
15
|
+
"rootDir": "./src",
|
|
16
|
+
"types": ["bun"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
botProviderForChannel,
|
|
4
5
|
CHANNEL_BOT_PROVIDER,
|
|
5
6
|
CHANNEL_IDS,
|
|
7
|
+
channelForBotProvider,
|
|
6
8
|
isChannelBotProvider,
|
|
7
9
|
isChannelUserIntegration,
|
|
8
10
|
isChannelId,
|
|
@@ -41,6 +43,35 @@ describe("isChannelId", () => {
|
|
|
41
43
|
});
|
|
42
44
|
});
|
|
43
45
|
|
|
46
|
+
describe("botProviderForChannel and channelForBotProvider", () => {
|
|
47
|
+
test("are inverses over every entry of the map", () => {
|
|
48
|
+
for (const [channelId, botProviderKey] of Object.entries(
|
|
49
|
+
CHANNEL_BOT_PROVIDER,
|
|
50
|
+
)) {
|
|
51
|
+
expect(botProviderForChannel(channelId)).toBe(botProviderKey);
|
|
52
|
+
expect(channelForBotProvider(botProviderKey)).toBe(
|
|
53
|
+
channelId as keyof typeof CHANNEL_BOT_PROVIDER,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("answer nothing for a channel without a bot, a person's grant, or an unknown key", () => {
|
|
59
|
+
expect(botProviderForChannel("phone")).toBeUndefined();
|
|
60
|
+
expect(botProviderForChannel("vellum")).toBeUndefined();
|
|
61
|
+
expect(botProviderForChannel("not-a-channel")).toBeUndefined();
|
|
62
|
+
// `slack` is the person's integration standing beside the bot's key.
|
|
63
|
+
expect(channelForBotProvider("slack")).toBeUndefined();
|
|
64
|
+
expect(channelForBotProvider("google")).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("never resolve an inherited object property as a channel or a key", () => {
|
|
68
|
+
for (const name of ["constructor", "toString", "__proto__"]) {
|
|
69
|
+
expect(botProviderForChannel(name)).toBeUndefined();
|
|
70
|
+
expect(channelForBotProvider(name)).toBeUndefined();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
44
75
|
describe("isChannelUserIntegration", () => {
|
|
45
76
|
test("names the grant standing beside a bot of the same brand", () => {
|
|
46
77
|
expect(isChannelUserIntegration("slack")).toBe(true);
|
package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts
CHANGED
|
@@ -93,9 +93,34 @@ export const CHANNEL_BOT_PROVIDER = {
|
|
|
93
93
|
* connected" question, for any provider key.
|
|
94
94
|
*/
|
|
95
95
|
export function isChannelBotProvider(providerKey: string): boolean {
|
|
96
|
-
return (
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
return channelForBotProvider(providerKey) !== undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The provider key holding a channel's bot credential, or `undefined` when
|
|
101
|
+
* the channel has none, or is not a channel at all. Walks the map's own
|
|
102
|
+
* entries, so an inherited object property name is never a channel.
|
|
103
|
+
*/
|
|
104
|
+
export function botProviderForChannel(channelId: string): string | undefined {
|
|
105
|
+
return Object.entries(CHANNEL_BOT_PROVIDER).find(
|
|
106
|
+
([id]) => id === channelId,
|
|
107
|
+
)?.[1];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The channel whose bot credential a provider key holds, or `undefined` when
|
|
112
|
+
* the key is not a bot's. The inverse of `botProviderForChannel`, for a
|
|
113
|
+
* surface that has only the provider key in hand and needs to speak in the
|
|
114
|
+
* channel's vocabulary.
|
|
115
|
+
*/
|
|
116
|
+
export function channelForBotProvider(
|
|
117
|
+
providerKey: string,
|
|
118
|
+
): keyof typeof CHANNEL_BOT_PROVIDER | undefined {
|
|
119
|
+
return (
|
|
120
|
+
Object.entries(CHANNEL_BOT_PROVIDER) as Array<
|
|
121
|
+
[keyof typeof CHANNEL_BOT_PROVIDER, string]
|
|
122
|
+
>
|
|
123
|
+
).find(([, key]) => key === providerKey)?.[0];
|
|
99
124
|
}
|
|
100
125
|
|
|
101
126
|
/**
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
botProviderForChannel,
|
|
4
5
|
CHANNEL_BOT_PROVIDER,
|
|
5
6
|
CHANNEL_IDS,
|
|
7
|
+
channelForBotProvider,
|
|
6
8
|
isChannelBotProvider,
|
|
7
9
|
isChannelUserIntegration,
|
|
8
10
|
isChannelId,
|
|
@@ -41,6 +43,35 @@ describe("isChannelId", () => {
|
|
|
41
43
|
});
|
|
42
44
|
});
|
|
43
45
|
|
|
46
|
+
describe("botProviderForChannel and channelForBotProvider", () => {
|
|
47
|
+
test("are inverses over every entry of the map", () => {
|
|
48
|
+
for (const [channelId, botProviderKey] of Object.entries(
|
|
49
|
+
CHANNEL_BOT_PROVIDER,
|
|
50
|
+
)) {
|
|
51
|
+
expect(botProviderForChannel(channelId)).toBe(botProviderKey);
|
|
52
|
+
expect(channelForBotProvider(botProviderKey)).toBe(
|
|
53
|
+
channelId as keyof typeof CHANNEL_BOT_PROVIDER,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("answer nothing for a channel without a bot, a person's grant, or an unknown key", () => {
|
|
59
|
+
expect(botProviderForChannel("phone")).toBeUndefined();
|
|
60
|
+
expect(botProviderForChannel("vellum")).toBeUndefined();
|
|
61
|
+
expect(botProviderForChannel("not-a-channel")).toBeUndefined();
|
|
62
|
+
// `slack` is the person's integration standing beside the bot's key.
|
|
63
|
+
expect(channelForBotProvider("slack")).toBeUndefined();
|
|
64
|
+
expect(channelForBotProvider("google")).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("never resolve an inherited object property as a channel or a key", () => {
|
|
68
|
+
for (const name of ["constructor", "toString", "__proto__"]) {
|
|
69
|
+
expect(botProviderForChannel(name)).toBeUndefined();
|
|
70
|
+
expect(channelForBotProvider(name)).toBeUndefined();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
44
75
|
describe("isChannelUserIntegration", () => {
|
|
45
76
|
test("names the grant standing beside a bot of the same brand", () => {
|
|
46
77
|
expect(isChannelUserIntegration("slack")).toBe(true);
|