@vellumai/assistant 0.11.5 → 0.11.6-staging.1
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/AGENTS.md +5 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +70 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +16 -1
- package/node_modules/@vellumai/gateway-client/src/index.ts +6 -2
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +121 -61
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/openapi.yaml +421 -15
- package/package.json +1 -1
- package/scripts/sync-web-search-catalog.ts +6 -0
- package/src/__tests__/app-pin-store.test.ts +149 -0
- package/src/__tests__/channel-availability-routes.test.ts +23 -1
- package/src/__tests__/channel-readiness-discord.test.ts +231 -0
- package/src/__tests__/channel-readiness-service.test.ts +126 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +141 -0
- package/src/__tests__/channel-reply-delivery.test.ts +4 -4
- package/src/__tests__/client-os-metadata-persistence.test.ts +23 -10
- package/src/__tests__/conversation-delete-watch-timeline.test.ts +231 -0
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-seed-composer.test.ts +8 -0
- package/src/__tests__/conversation-slash-commands.test.ts +8 -0
- package/src/__tests__/disk-pressure-policy.test.ts +6 -0
- package/src/__tests__/gemini-provider.test.ts +138 -0
- package/src/__tests__/history-repair.test.ts +105 -3
- package/src/__tests__/identity-routes.test.ts +1 -0
- package/src/__tests__/llm-catalog-parity.test.ts +45 -0
- package/src/__tests__/migration-import-from-path.test.ts +349 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +102 -0
- package/src/__tests__/oauth-commands-routes.test.ts +89 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +7 -6
- package/src/__tests__/openai-provider.test.ts +18 -0
- package/src/__tests__/openai-responses-provider.test.ts +18 -0
- package/src/__tests__/platform-callback-registration.test.ts +184 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +71 -3
- package/src/__tests__/pricing.test.ts +2 -2
- package/src/__tests__/public-ingress-urls.test.ts +36 -0
- package/src/__tests__/resolve-trust-class.test.ts +0 -48
- package/src/__tests__/sanitize-config-for-transfer.test.ts +28 -0
- package/src/__tests__/secret-routes-platform-proxy.test.ts +49 -0
- package/src/__tests__/settings-routes.test.ts +85 -3
- package/src/__tests__/web-search-catalog-parity.test.ts +8 -0
- package/src/agent/history-repair/history-repair.ts +45 -14
- package/src/agent/loop.ts +4 -1
- package/src/api/constants/profile-config-validation.ts +60 -0
- package/src/api/events/tool-result.ts +6 -1
- package/src/api/events/watch-retro-completed.ts +52 -0
- package/src/api/index.ts +11 -0
- package/src/apps/app-pin-reconciler.ts +92 -0
- package/src/apps/app-pin-store.ts +125 -0
- package/src/channels/gateway-channel-socket-health.ts +32 -0
- package/src/channels/gateway-discord-admission.ts +32 -0
- package/src/channels/types.ts +20 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
- package/src/cli/commands/__tests__/inference-profiles.test.ts +16 -4
- package/src/cli/commands/__tests__/inference-providers.test.ts +67 -2
- package/src/cli/commands/channels/__tests__/channels.test.ts +85 -0
- package/src/cli/commands/channels/index.ts +45 -31
- package/src/cli/commands/inference-profiles.ts +56 -3
- package/src/cli/commands/inference-providers.ts +28 -2
- package/src/cli/commands/oauth/index.help.ts +7 -1
- package/src/cli/commands/oauth/request.test.ts +290 -0
- package/src/cli/commands/oauth/request.ts +57 -41
- package/src/cli/lib/bundled-marketplace.json +14 -1
- package/src/cli/lib/open-browser.test.ts +67 -0
- package/src/cli/lib/open-browser.ts +24 -5
- package/src/config/__tests__/profile-materialization.test.ts +26 -0
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +6 -0
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/profile-materialization.ts +29 -0
- package/src/config/sanitize-for-transfer.ts +16 -0
- package/src/config/schemas/llm.ts +7 -0
- package/src/config/schemas/services.ts +6 -0
- package/src/context/outbound-sanitize.ts +6 -0
- package/src/daemon/__tests__/lifecycle-watch-timeline-sweep.test.ts +98 -0
- package/src/daemon/conversation-error.ts +24 -2
- package/src/daemon/conversation-slash.ts +6 -15
- package/src/daemon/daemon-control.ts +1 -0
- package/src/daemon/disk-pressure-policy.ts +7 -1
- package/src/daemon/handlers/__tests__/config-ingress-tunnel-records.test.ts +208 -0
- package/src/daemon/handlers/config-ingress.ts +115 -5
- package/src/daemon/lifecycle.ts +26 -0
- package/src/daemon/message-types/web-activity.ts +3 -2
- package/src/daemon/trust-context.ts +0 -37
- package/src/inbound/__tests__/tunnel-probe.test.ts +448 -0
- package/src/inbound/platform-callback-registration.ts +28 -2
- package/src/inbound/public-ingress-urls.ts +12 -0
- package/src/inbound/tunnel-probe.ts +261 -0
- package/src/live-voice/__tests__/live-voice-connection.test.ts +25 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +8 -2
- package/src/live-voice/__tests__/live-voice-session-manager.test.ts +212 -10
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +5 -2
- package/src/live-voice/live-voice-connection.ts +46 -8
- package/src/live-voice/live-voice-manager.ts +25 -0
- package/src/live-voice/live-voice-session-manager.ts +318 -2
- package/src/live-voice/live-voice-session.ts +52 -2
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +126 -68
- package/src/messaging/providers/channel-transport.ts +64 -47
- package/src/messaging/providers/discord/send.test.ts +46 -1
- package/src/messaging/providers/discord/send.ts +51 -0
- package/src/messaging/providers/discord/transport.ts +26 -3
- package/src/messaging/providers/index.ts +22 -47
- package/src/messaging/providers/slack/send.test.ts +83 -26
- package/src/messaging/providers/slack/send.ts +120 -51
- package/src/messaging/providers/slack/stream-tasks.test.ts +26 -0
- package/src/messaging/providers/slack/stream-tasks.ts +39 -0
- package/src/messaging/providers/slack/transport.ts +24 -22
- package/src/messaging/providers/telegram-bot/send.test.ts +109 -12
- package/src/messaging/providers/telegram-bot/send.ts +43 -0
- package/src/messaging/providers/telegram-bot/transport.ts +25 -8
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +30 -7
- package/src/notifications/adapters/telegram.ts +48 -1
- package/src/notifications/assistant-reply-producer.ts +7 -7
- package/src/notifications/conversation-seed-composer.ts +7 -2
- package/src/oauth/byo-connection.test.ts +63 -0
- package/src/oauth/byo-connection.ts +16 -15
- package/src/oauth/connection.test.ts +111 -0
- package/src/oauth/connection.ts +142 -1
- package/src/oauth/platform-connection.test.ts +34 -0
- package/src/oauth/platform-connection.ts +28 -5
- package/src/oauth/seed-providers.ts +15 -1
- package/src/permissions/types.ts +3 -1
- package/src/persistence/conversation-crud.ts +46 -0
- package/src/persistence/conversation-types.ts +11 -9
- package/src/persistence/db-async-query.ts +2 -1
- package/src/persistence/db-maintenance.ts +15 -0
- package/src/persistence/embeddings/qdrant-manager.ts +1 -0
- package/src/persistence/migrations/367-create-watch-timeline-entries.ts +46 -0
- package/src/persistence/migrations/368-watch-timeline-screenshot-blob.ts +33 -0
- package/src/persistence/migrations/369-create-app-pins.ts +37 -0
- package/src/persistence/migrations/__tests__/367-create-watch-timeline-entries.test.ts +98 -0
- package/src/persistence/migrations/__tests__/368-watch-timeline-screenshot-blob.test.ts +98 -0
- package/src/persistence/schema/index.ts +1 -0
- package/src/persistence/schema/infrastructure.ts +17 -0
- package/src/persistence/schema/watch.ts +29 -0
- package/src/persistence/steps.ts +6 -0
- package/src/plugins/mtime-cache.ts +11 -0
- package/src/providers/__tests__/retry-network-error.test.ts +84 -0
- package/src/providers/connection-resolution.ts +23 -1
- package/src/providers/content-blocks.ts +9 -0
- package/src/providers/fetch-provider-catalog.ts +19 -0
- package/src/providers/gemini/client.ts +13 -5
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +92 -0
- package/src/providers/inference/__tests__/profile-config-validation.test.ts +39 -0
- package/src/providers/inference/__tests__/profile-probe-classify.test.ts +66 -0
- package/src/providers/inference/adapter-factory.ts +0 -9
- package/src/providers/inference/credential-rotation.ts +61 -0
- package/src/providers/inference/endpoint-probe.ts +115 -0
- package/src/providers/inference/profile-probe.ts +256 -0
- package/src/providers/model-catalog.ts +170 -125
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +17 -1
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +42 -60
- package/src/providers/openai/__tests__/connection-error-wrap.test.ts +44 -0
- package/src/providers/openai/__tests__/orphan-tool-result-guard.test.ts +34 -2
- package/src/providers/openai/api-error-normalization.ts +16 -2
- package/src/providers/openai/chat-completions-provider.ts +75 -29
- package/src/providers/openai/responses-provider.ts +5 -2
- package/src/providers/openrouter/client.ts +0 -1
- package/src/providers/provider-send-message.ts +11 -0
- package/src/providers/retry.ts +6 -0
- package/src/providers/search-provider-catalog.ts +20 -0
- package/src/providers/vercel-ai-gateway/client.ts +0 -1
- package/src/runtime/AGENTS.md +1 -0
- package/src/runtime/__tests__/desktop-presence.test.ts +27 -4
- package/src/runtime/__tests__/host-observe.test.ts +302 -0
- package/src/runtime/channel-readiness-service.ts +214 -14
- package/src/runtime/channel-readiness-types.ts +49 -2
- package/src/runtime/channel-reply-delivery.ts +2 -2
- package/src/runtime/desktop-presence.ts +24 -21
- package/src/runtime/host-observe.ts +246 -0
- package/src/runtime/http-server.ts +181 -1
- package/src/runtime/migrations/__tests__/staged-import-path.test.ts +104 -0
- package/src/runtime/migrations/staged-import-path.ts +116 -0
- package/src/runtime/routes/__tests__/app-pin-routes.test.ts +383 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +80 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +118 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +20 -0
- package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +508 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +35 -56
- package/src/runtime/routes/__tests__/watch-routes-guardian-cache.test.ts +139 -0
- package/src/runtime/routes/__tests__/watch-routes.test.ts +598 -0
- package/src/runtime/routes/app-management-routes.ts +140 -29
- package/src/runtime/routes/channel-availability-routes.ts +1 -0
- package/src/runtime/routes/channel-readiness-routes.ts +14 -2
- package/src/runtime/routes/conversation-query-routes.ts +10 -0
- package/src/runtime/routes/guardian-approval-interception.ts +24 -33
- package/src/runtime/routes/host-cu-routes.ts +18 -0
- package/src/runtime/routes/identity-routes.ts +2 -0
- package/src/runtime/routes/inbound-message-handler.ts +10 -7
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +166 -308
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +158 -335
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profiles-routes.ts +232 -31
- package/src/runtime/routes/inference-provider-connection-routes.ts +24 -4
- package/src/runtime/routes/ingress-status-routes.ts +180 -0
- package/src/runtime/routes/live-voice-routes.test.ts +40 -1
- package/src/runtime/routes/live-voice-routes.ts +34 -0
- package/src/runtime/routes/migration-routes.ts +218 -10
- package/src/runtime/routes/oauth-commands-routes.ts +23 -16
- package/src/runtime/routes/plugins-routes.ts +12 -28
- package/src/runtime/routes/question-routes.ts +6 -0
- package/src/runtime/routes/secret-routes.ts +7 -27
- package/src/runtime/routes/settings-routes.ts +9 -6
- package/src/runtime/routes/watch-routes.ts +807 -0
- package/src/runtime/slack-reply-session.test.ts +230 -121
- package/src/runtime/slack-reply-session.ts +113 -81
- package/src/runtime/{slack-task-progress.test.ts → task-progress.test.ts} +1 -28
- package/src/runtime/{slack-task-progress.ts → task-progress.ts} +30 -51
- package/src/security/__tests__/untrusted-content.test.ts +42 -0
- package/src/security/untrusted-content.ts +28 -9
- package/src/telemetry/__tests__/live-voice-funnel.test.ts +108 -0
- package/src/telemetry/live-voice-funnel.ts +75 -8
- package/src/tools/credentials/store.ts +18 -6
- package/src/tools/network/__tests__/firecrawl-compat.test.ts +77 -0
- package/src/tools/network/__tests__/web-fetch-fastcrw.test.ts +169 -0
- package/src/tools/network/__tests__/web-search.test.ts +97 -2
- package/src/tools/network/firecrawl-compat.ts +90 -0
- package/src/tools/network/web-fetch.ts +142 -62
- package/src/tools/network/web-search.ts +141 -55
- package/src/tools/types.ts +2 -1
- package/src/util/oauth-request-body.test.ts +74 -0
- package/src/util/oauth-request-body.ts +60 -0
- package/src/util/worker-process.ts +1 -0
- package/src/watch/__tests__/watch-retro.test.ts +665 -0
- package/src/watch/__tests__/watch-session-manager.test.ts +566 -0
- package/src/watch/__tests__/watch-timeline.test.ts +670 -0
- package/src/watch/watch-retro.ts +480 -0
- package/src/watch/watch-session-manager.ts +575 -0
- package/src/watch/watch-timeline.ts +848 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level-based reconciler converging `app_pins` onto the apps that exist.
|
|
3
|
+
*
|
|
4
|
+
* A workspace app's id is a UUID, so a pin left behind when one is deleted can
|
|
5
|
+
* never be adopted and the app list simply never returns it. A plugin app's id
|
|
6
|
+
* is a path identity, `plugins~<plugin>~<appDir>`, and `listPluginApps` omits a
|
|
7
|
+
* plugin that is disabled or uninstalled. Retiring one and bringing it back
|
|
8
|
+
* rebuilds the same id, so a pin left behind returns to the sidebar with it,
|
|
9
|
+
* for an app the user had no way to unpin while it was hidden. Converging on
|
|
10
|
+
* every plugin-set change is what keeps a retirement from being undone.
|
|
11
|
+
*
|
|
12
|
+
* Triggers mirror `schedule/plugin-schedule-reconciler.ts`, which solves the
|
|
13
|
+
* same problem for plugin-declared schedules: daemon startup
|
|
14
|
+
* (`daemon/lifecycle.ts`), plugin-set convergence (`plugins/mtime-cache.ts`),
|
|
15
|
+
* and a periodic backstop sweep (`runtime/http-server.ts`) that covers the
|
|
16
|
+
* `.disabled` sentinel, which the CLI flips out of process without poking the
|
|
17
|
+
* source reconcile.
|
|
18
|
+
*
|
|
19
|
+
* Self-contained in the same way: it never throws, and it checks database
|
|
20
|
+
* readiness itself rather than relying on its callers to be ordered after
|
|
21
|
+
* migrations.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { getDbMigrationReadiness } from "../daemon/daemon-readiness.js";
|
|
25
|
+
import { publishAppsChanged } from "../runtime/sync/resource-sync-events.js";
|
|
26
|
+
import { getLogger } from "../util/logger.js";
|
|
27
|
+
import { listAppPins, removeAppPin } from "./app-pin-store.js";
|
|
28
|
+
import { listApps, listPluginApps } from "./app-store.js";
|
|
29
|
+
|
|
30
|
+
const log = getLogger("app-pin-reconciler");
|
|
31
|
+
|
|
32
|
+
/** How often the backstop sweep converges, matching the schedule reconciler. */
|
|
33
|
+
const SWEEP_INTERVAL_MS = 60_000;
|
|
34
|
+
|
|
35
|
+
let sweepTimer: ReturnType<typeof setInterval> | null = null;
|
|
36
|
+
let sweepInProgress = false;
|
|
37
|
+
|
|
38
|
+
/** Drop every pin whose app is not currently installed. Never throws. */
|
|
39
|
+
export function reconcileAppPins(): void {
|
|
40
|
+
if (!getDbMigrationReadiness().ready) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const live = new Set([
|
|
45
|
+
...listApps().map((app) => app.id),
|
|
46
|
+
...listPluginApps().map((app) => app.id),
|
|
47
|
+
]);
|
|
48
|
+
const stale = listAppPins().filter((pin) => !live.has(pin.appId));
|
|
49
|
+
for (const pin of stale) {
|
|
50
|
+
removeAppPin(pin.appId);
|
|
51
|
+
}
|
|
52
|
+
if (stale.length > 0) {
|
|
53
|
+
log.info(
|
|
54
|
+
{ removed: stale.map((pin) => pin.appId) },
|
|
55
|
+
"Dropped pins for apps that are no longer installed",
|
|
56
|
+
);
|
|
57
|
+
/* A pass has no originating client, and every other pin write announces
|
|
58
|
+
itself, so a client would otherwise keep rendering a pin this just
|
|
59
|
+
deleted until some unrelated refetch. */
|
|
60
|
+
publishAppsChanged();
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
log.error({ err }, "app pin reconcile failed");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Start the periodic backstop sweep. Idempotent. */
|
|
68
|
+
export function startAppPinReconcileSweep(): void {
|
|
69
|
+
if (sweepTimer) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
sweepTimer = setInterval(() => {
|
|
73
|
+
if (sweepInProgress) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
sweepInProgress = true;
|
|
77
|
+
try {
|
|
78
|
+
reconcileAppPins();
|
|
79
|
+
} finally {
|
|
80
|
+
sweepInProgress = false;
|
|
81
|
+
}
|
|
82
|
+
}, SWEEP_INTERVAL_MS);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Stop the periodic sweep. Used in tests and shutdown. */
|
|
86
|
+
export function stopAppPinReconcileSweep(): void {
|
|
87
|
+
if (sweepTimer) {
|
|
88
|
+
clearInterval(sweepTimer);
|
|
89
|
+
sweepTimer = null;
|
|
90
|
+
}
|
|
91
|
+
sweepInProgress = false;
|
|
92
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which apps the user pinned to the sidebar, and in what order.
|
|
3
|
+
*
|
|
4
|
+
* Rows hold ids alone. Name and icon are read from the app itself at list
|
|
5
|
+
* time, so a renamed app renames its pin, and a pin whose app is gone surfaces
|
|
6
|
+
* nowhere.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { eq, max } from "drizzle-orm";
|
|
10
|
+
|
|
11
|
+
import { getDb } from "../persistence/db-connection.js";
|
|
12
|
+
import { appPins } from "../persistence/schema/index.js";
|
|
13
|
+
|
|
14
|
+
export interface AppPin {
|
|
15
|
+
appId: string;
|
|
16
|
+
/** Fractional index. Ascending is sidebar order; the values are not dense. */
|
|
17
|
+
sortPosition: number;
|
|
18
|
+
/** An id from the client's pinned-app colour registry. Absent when unset. */
|
|
19
|
+
color?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function toPin(row: {
|
|
23
|
+
appId: string;
|
|
24
|
+
sortPosition: number;
|
|
25
|
+
color: string | null;
|
|
26
|
+
}): AppPin {
|
|
27
|
+
return {
|
|
28
|
+
appId: row.appId,
|
|
29
|
+
sortPosition: row.sortPosition,
|
|
30
|
+
...(row.color === null ? {} : { color: row.color }),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Every pin, in sidebar order. */
|
|
35
|
+
export function listAppPins(): AppPin[] {
|
|
36
|
+
return getDb()
|
|
37
|
+
.select()
|
|
38
|
+
.from(appPins)
|
|
39
|
+
.orderBy(appPins.sortPosition)
|
|
40
|
+
.all()
|
|
41
|
+
.map(toPin);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getAppPin(appId: string): AppPin | null {
|
|
45
|
+
const row = getDb()
|
|
46
|
+
.select()
|
|
47
|
+
.from(appPins)
|
|
48
|
+
.where(eq(appPins.appId, appId))
|
|
49
|
+
.get();
|
|
50
|
+
return row ? toPin(row) : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** One past the last pin, so pinning appends. */
|
|
54
|
+
function nextSortPosition(): number {
|
|
55
|
+
const highest = getDb()
|
|
56
|
+
.select({ value: max(appPins.sortPosition) })
|
|
57
|
+
.from(appPins)
|
|
58
|
+
.get();
|
|
59
|
+
return (highest?.value ?? 0) + 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AppPinUpdate {
|
|
63
|
+
/** Pin or unpin. Omitted leaves the app's pinned state as it stands. */
|
|
64
|
+
pinned?: boolean;
|
|
65
|
+
/** Set (string) or clear (`null`) the colour. Omitted leaves it as it stands. */
|
|
66
|
+
color?: string | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Apply a pin change and return the app's resulting pin, or `null` once it is
|
|
71
|
+
* no longer pinned.
|
|
72
|
+
*
|
|
73
|
+
* A colour for an app that is not pinned is dropped, so a colour can never
|
|
74
|
+
* conjure a pin that unpinning just removed.
|
|
75
|
+
*/
|
|
76
|
+
export function updateAppPin(
|
|
77
|
+
appId: string,
|
|
78
|
+
update: AppPinUpdate,
|
|
79
|
+
): AppPin | null {
|
|
80
|
+
if (update.pinned === false) {
|
|
81
|
+
removeAppPin(appId);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const existing = getAppPin(appId);
|
|
86
|
+
if (!existing && update.pinned !== true) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const color = update.color === undefined ? existing?.color : update.color;
|
|
91
|
+
const db = getDb();
|
|
92
|
+
const sortPosition = existing?.sortPosition ?? nextSortPosition();
|
|
93
|
+
|
|
94
|
+
if (existing) {
|
|
95
|
+
db.update(appPins)
|
|
96
|
+
.set({ color: color ?? null })
|
|
97
|
+
.where(eq(appPins.appId, appId))
|
|
98
|
+
.run();
|
|
99
|
+
} else {
|
|
100
|
+
/* `onConflictDoUpdate` rather than a plain insert: two clients pinning the
|
|
101
|
+
same app at once would otherwise race on the primary key. */
|
|
102
|
+
db.insert(appPins)
|
|
103
|
+
.values({
|
|
104
|
+
appId,
|
|
105
|
+
sortPosition,
|
|
106
|
+
color: color ?? null,
|
|
107
|
+
createdAt: Date.now(),
|
|
108
|
+
})
|
|
109
|
+
.onConflictDoUpdate({
|
|
110
|
+
target: appPins.appId,
|
|
111
|
+
set: { color: color ?? null },
|
|
112
|
+
})
|
|
113
|
+
.run();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { appId, sortPosition, ...(color == null ? {} : { color }) };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Drop an app's pin as part of deleting it, so the pin does not outlive the app
|
|
121
|
+
* it points at. A no-op when the app was not pinned.
|
|
122
|
+
*/
|
|
123
|
+
export function removeAppPin(appId: string): void {
|
|
124
|
+
getDb().delete(appPins).where(eq(appPins.appId, appId)).run();
|
|
125
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon-side reader for whether a socket-backed channel is receiving.
|
|
3
|
+
*
|
|
4
|
+
* The gateway holds the inbound sockets; the readiness surface that reports
|
|
5
|
+
* channel health to the user lives here. This is the relay between them.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type ChannelSocketHealthIpcParams,
|
|
10
|
+
type ChannelSocketHealthIpcResponse,
|
|
11
|
+
ChannelSocketHealthIpcResponseSchema,
|
|
12
|
+
} from "@vellumai/gateway-client/gateway-ipc-contracts";
|
|
13
|
+
|
|
14
|
+
import { ipcCallPersistentValidated } from "../ipc/gateway-validated-call.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Ask the gateway whether `channel`'s inbound socket is live.
|
|
18
|
+
*
|
|
19
|
+
* Throws on transport failure rather than reporting a channel as down: an
|
|
20
|
+
* unreachable gateway is a fact about the gateway, and a caller that renders
|
|
21
|
+
* it as "Slack is disconnected" would be inventing an outage. Callers are
|
|
22
|
+
* expected to treat a throw as indeterminate.
|
|
23
|
+
*/
|
|
24
|
+
export async function readChannelSocketHealth(
|
|
25
|
+
channel: ChannelSocketHealthIpcParams["channel"],
|
|
26
|
+
): Promise<ChannelSocketHealthIpcResponse> {
|
|
27
|
+
return ipcCallPersistentValidated(
|
|
28
|
+
"channel_socket_health",
|
|
29
|
+
{ channel },
|
|
30
|
+
ChannelSocketHealthIpcResponseSchema,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon-side reader for how much Discord admits.
|
|
3
|
+
*
|
|
4
|
+
* The gateway owns the allow-list and the drop decision it feeds; the
|
|
5
|
+
* readiness surface that reports channel setup to the user lives here. This is
|
|
6
|
+
* the relay between them, and it exists for the same reason the socket-health
|
|
7
|
+
* relay does: the fact lives on one side of the boundary and is reported from
|
|
8
|
+
* the other.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type DiscordAdmissionIpcResponse,
|
|
13
|
+
DiscordAdmissionIpcResponseSchema,
|
|
14
|
+
} from "@vellumai/gateway-client/gateway-ipc-contracts";
|
|
15
|
+
|
|
16
|
+
import { ipcCallPersistentValidated } from "../ipc/gateway-validated-call.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Ask the gateway how many channels Discord's allow-list admits.
|
|
20
|
+
*
|
|
21
|
+
* Throws on transport failure rather than reporting zero: an unreachable
|
|
22
|
+
* gateway is a fact about the gateway, and answering zero would tell an
|
|
23
|
+
* operator their allow-list is empty when nobody looked. Callers treat a throw
|
|
24
|
+
* as indeterminate.
|
|
25
|
+
*/
|
|
26
|
+
export async function readDiscordAdmission(): Promise<DiscordAdmissionIpcResponse> {
|
|
27
|
+
return ipcCallPersistentValidated(
|
|
28
|
+
"discord_admission",
|
|
29
|
+
{},
|
|
30
|
+
DiscordAdmissionIpcResponseSchema,
|
|
31
|
+
);
|
|
32
|
+
}
|
package/src/channels/types.ts
CHANGED
|
@@ -111,6 +111,26 @@ export const CHANNEL_METADATA: Partial<Record<ChannelId, ChannelInfo>> = {
|
|
|
111
111
|
"I'd like to verify a contact's Telegram identity. Can you walk me through it?",
|
|
112
112
|
},
|
|
113
113
|
},
|
|
114
|
+
discord: {
|
|
115
|
+
id: "discord",
|
|
116
|
+
label: "Discord",
|
|
117
|
+
subtitle: "Message your assistant from Discord",
|
|
118
|
+
// A Lucide name, per this field's contract, and the same one Slack
|
|
119
|
+
// carries: both name their text channels with a hash. A client with room
|
|
120
|
+
// for a brand mark uses one, which is how Slack's header already works.
|
|
121
|
+
icon: "hash",
|
|
122
|
+
// Verification needs nothing channel-specific here. The inbound intercept
|
|
123
|
+
// that matches a code takes the source channel as data, Discord inbound
|
|
124
|
+
// reaches it through the same `handleInbound` every channel uses, and a
|
|
125
|
+
// Discord user id canonicalizes as opaque, which is what it is.
|
|
126
|
+
supportsVerification: true,
|
|
127
|
+
setupMessages: {
|
|
128
|
+
guardian:
|
|
129
|
+
"I'd like to verify my identity as your guardian on Discord. Can you help me set that up?",
|
|
130
|
+
contact:
|
|
131
|
+
"I'd like to verify a contact's Discord identity. Can you walk me through it?",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
114
134
|
phone: {
|
|
115
135
|
id: "phone",
|
|
116
136
|
label: "Phone Calling",
|
|
@@ -112,7 +112,7 @@ mock.module("../../../messaging/providers/slack/send.js", () => ({
|
|
|
112
112
|
},
|
|
113
113
|
sendSlackStreamOp: async () => ({ ok: true, ts: "1700000000.000200" }),
|
|
114
114
|
sendSlackReaction: async () => {},
|
|
115
|
-
|
|
115
|
+
sendSlackAgentSessionStatus: async () => {},
|
|
116
116
|
sendSlackAttachments: async () => ({
|
|
117
117
|
allFailed: false,
|
|
118
118
|
failureCount: 0,
|
|
@@ -11,6 +11,7 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
|
11
11
|
import { Command } from "commander";
|
|
12
12
|
|
|
13
13
|
let lastIpcCall: { method: string; params?: any } | null = null;
|
|
14
|
+
let ipcCalls: { method: string; params?: any }[] = [];
|
|
14
15
|
let mockIpcResult: { ok: boolean; result?: unknown; error?: string } = {
|
|
15
16
|
ok: true,
|
|
16
17
|
result: {},
|
|
@@ -19,6 +20,7 @@ let mockIpcResult: { ok: boolean; result?: unknown; error?: string } = {
|
|
|
19
20
|
mock.module("../../../ipc/cli-client.js", () => ({
|
|
20
21
|
cliIpcCall: async (method: string, params?: Record<string, unknown>) => {
|
|
21
22
|
lastIpcCall = { method, params };
|
|
23
|
+
ipcCalls.push({ method, params });
|
|
22
24
|
return mockIpcResult;
|
|
23
25
|
},
|
|
24
26
|
}));
|
|
@@ -36,6 +38,7 @@ const { inferenceHelp } = await import("../inference.help.js");
|
|
|
36
38
|
|
|
37
39
|
beforeEach(() => {
|
|
38
40
|
lastIpcCall = null;
|
|
41
|
+
ipcCalls = [];
|
|
39
42
|
mockIpcResult = { ok: true, result: {} };
|
|
40
43
|
process.exitCode = 0;
|
|
41
44
|
});
|
|
@@ -136,8 +139,13 @@ describe("profiles create", () => {
|
|
|
136
139
|
"--allow-unlisted",
|
|
137
140
|
"--allow-unavailable",
|
|
138
141
|
]);
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
const createCall = ipcCalls.find(
|
|
143
|
+
(c) => c.method === "inference_profiles_create",
|
|
144
|
+
);
|
|
145
|
+
expect(createCall).toBeDefined();
|
|
146
|
+
// The save is followed by the advisory probe call.
|
|
147
|
+
expect(ipcCalls.at(-1)?.method).toBe("inference_profiles_validate");
|
|
148
|
+
expect(createCall?.params?.body).toEqual({
|
|
141
149
|
name: "p",
|
|
142
150
|
provider: "anthropic",
|
|
143
151
|
model: "claude-opus-4-8",
|
|
@@ -243,11 +251,15 @@ describe("profiles update", () => {
|
|
|
243
251
|
result: { ok: true, name: "p", entry: {}, warnings: [] },
|
|
244
252
|
};
|
|
245
253
|
await run(["profiles", "update", "p", "--effort", "low"]);
|
|
246
|
-
|
|
247
|
-
|
|
254
|
+
const updateCall = ipcCalls.find(
|
|
255
|
+
(c) => c.method === "inference_profiles_update",
|
|
256
|
+
);
|
|
257
|
+
expect(updateCall?.params).toEqual({
|
|
248
258
|
pathParams: { name: "p" },
|
|
249
259
|
body: { effort: "low" },
|
|
250
260
|
});
|
|
261
|
+
// The save is followed by the advisory probe call.
|
|
262
|
+
expect(ipcCalls.at(-1)?.method).toBe("inference_profiles_validate");
|
|
251
263
|
});
|
|
252
264
|
|
|
253
265
|
test("refuses an empty update", async () => {
|
|
@@ -11,6 +11,8 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
|
11
11
|
import { Command } from "commander";
|
|
12
12
|
|
|
13
13
|
let ipcCalls: { method: string; params?: any }[] = [];
|
|
14
|
+
let openedUrls: string[] = [];
|
|
15
|
+
let storedSecrets: Array<{ key: string; value: string }> = [];
|
|
14
16
|
let mockIpcResult: { ok: boolean; result?: unknown; error?: string } = {
|
|
15
17
|
ok: true,
|
|
16
18
|
result: {},
|
|
@@ -30,6 +32,35 @@ mock.module("../../../util/logger.js", () => ({
|
|
|
30
32
|
new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
|
|
31
33
|
}));
|
|
32
34
|
|
|
35
|
+
mock.module("../../lib/open-browser.js", () => ({
|
|
36
|
+
openInHostBrowser: (url: string) => {
|
|
37
|
+
openedUrls.push(url);
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
mock.module("../../../security/oauth2.js", () => ({
|
|
42
|
+
startOAuth2Flow: async (
|
|
43
|
+
_config: unknown,
|
|
44
|
+
callbacks: { openUrl: (url: string) => void },
|
|
45
|
+
) => {
|
|
46
|
+
callbacks.openUrl("https://auth.openai.com/oauth/authorize?state=test");
|
|
47
|
+
return {
|
|
48
|
+
tokens: {
|
|
49
|
+
accessToken: "test-access-token",
|
|
50
|
+
refreshToken: "test-refresh-token",
|
|
51
|
+
expiresIn: 3600,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
mock.module("../../../security/secure-keys.js", () => ({
|
|
58
|
+
setSecureKeyAsync: async (key: string, value: string) => {
|
|
59
|
+
storedSecrets.push({ key, value });
|
|
60
|
+
return true;
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
63
|
+
|
|
33
64
|
const { attachProvidersSubcommand } = await import("../inference-providers.js");
|
|
34
65
|
const { applyCommandHelp } = await import("../../lib/cli-command-help.js");
|
|
35
66
|
const { inferenceHelp } = await import("../inference.help.js");
|
|
@@ -48,6 +79,8 @@ function lastIpcCall(): { method: string; params?: any } | null {
|
|
|
48
79
|
|
|
49
80
|
beforeEach(() => {
|
|
50
81
|
ipcCalls = [];
|
|
82
|
+
openedUrls = [];
|
|
83
|
+
storedSecrets = [];
|
|
51
84
|
mockIpcResult = { ok: true, result: CONNECTION_RESULT };
|
|
52
85
|
process.exitCode = 0;
|
|
53
86
|
});
|
|
@@ -58,13 +91,19 @@ afterEach(() => {
|
|
|
58
91
|
|
|
59
92
|
async function run(
|
|
60
93
|
args: string[],
|
|
61
|
-
): Promise<{ stdout: string; exitCode: number }> {
|
|
94
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
62
95
|
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
96
|
+
const originalErrorWrite = process.stderr.write.bind(process.stderr);
|
|
63
97
|
const chunks: string[] = [];
|
|
98
|
+
const errorChunks: string[] = [];
|
|
64
99
|
process.stdout.write = ((chunk: unknown) => {
|
|
65
100
|
chunks.push(typeof chunk === "string" ? chunk : String(chunk));
|
|
66
101
|
return true;
|
|
67
102
|
}) as typeof process.stdout.write;
|
|
103
|
+
process.stderr.write = ((chunk: unknown) => {
|
|
104
|
+
errorChunks.push(typeof chunk === "string" ? chunk : String(chunk));
|
|
105
|
+
return true;
|
|
106
|
+
}) as typeof process.stderr.write;
|
|
68
107
|
|
|
69
108
|
const prevExit = process.exitCode;
|
|
70
109
|
process.exitCode = 0;
|
|
@@ -79,11 +118,13 @@ async function run(
|
|
|
79
118
|
// swallow commander exits
|
|
80
119
|
} finally {
|
|
81
120
|
process.stdout.write = originalWrite;
|
|
121
|
+
process.stderr.write = originalErrorWrite;
|
|
82
122
|
}
|
|
83
123
|
const stdout = chunks.join("");
|
|
124
|
+
const stderr = errorChunks.join("");
|
|
84
125
|
const exitCode = (process.exitCode as number) ?? 0;
|
|
85
126
|
process.exitCode = prevExit;
|
|
86
|
-
return { stdout, exitCode };
|
|
127
|
+
return { stdout, stderr, exitCode };
|
|
87
128
|
}
|
|
88
129
|
|
|
89
130
|
describe("providers create — derived auth", () => {
|
|
@@ -484,3 +525,27 @@ describe("deprecated providers connections alias", () => {
|
|
|
484
525
|
expect(stdout).toContain("No providers found.");
|
|
485
526
|
});
|
|
486
527
|
});
|
|
528
|
+
|
|
529
|
+
describe("providers login-chatgpt", () => {
|
|
530
|
+
test("delivers the authorization URL through the browser helper and stderr", async () => {
|
|
531
|
+
const { stdout, stderr, exitCode } = await run([
|
|
532
|
+
"providers",
|
|
533
|
+
"login-chatgpt",
|
|
534
|
+
"--json",
|
|
535
|
+
]);
|
|
536
|
+
|
|
537
|
+
expect(exitCode).toBe(0);
|
|
538
|
+
expect(JSON.parse(stdout)).toMatchObject({ ok: true });
|
|
539
|
+
expect(stderr).toContain(
|
|
540
|
+
"https://auth.openai.com/oauth/authorize?state=test",
|
|
541
|
+
);
|
|
542
|
+
expect(openedUrls).toEqual([
|
|
543
|
+
"https://auth.openai.com/oauth/authorize?state=test",
|
|
544
|
+
]);
|
|
545
|
+
expect(storedSecrets).toContainEqual({
|
|
546
|
+
key: "credential/chatgpt/access_token",
|
|
547
|
+
value: "test-access-token",
|
|
548
|
+
});
|
|
549
|
+
expect(lastIpcCall()?.method).toBe("inference_provider_connections_update");
|
|
550
|
+
});
|
|
551
|
+
});
|
|
@@ -91,6 +91,91 @@ describe("assistant channels", () => {
|
|
|
91
91
|
});
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
describe("two-axis rendering", () => {
|
|
95
|
+
/**
|
|
96
|
+
* A configured channel whose delivery stopped is the state this command
|
|
97
|
+
* used to have no word for. Setup is finished, so telling the reader to
|
|
98
|
+
* finish setup sends them to re-enter credentials that are already
|
|
99
|
+
* correct, and the operational reason for the outage is the only thing
|
|
100
|
+
* they can act on.
|
|
101
|
+
*/
|
|
102
|
+
const configuredButDown = (health: string) =>
|
|
103
|
+
emptySnapshot("slack", {
|
|
104
|
+
ready: false,
|
|
105
|
+
setupStatus: "ready",
|
|
106
|
+
health,
|
|
107
|
+
localChecks: [
|
|
108
|
+
{
|
|
109
|
+
name: "inbound_delivery",
|
|
110
|
+
passed: health !== "failing",
|
|
111
|
+
kind: "operational",
|
|
112
|
+
message: "Slack Socket Mode holds no live connection",
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("list calls a configured channel that stopped delivering down, not unfinished", async () => {
|
|
118
|
+
mockResponses = [
|
|
119
|
+
{
|
|
120
|
+
ok: true,
|
|
121
|
+
result: { success: true, snapshots: [configuredButDown("failing")] },
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
const out = await runCli("channels", "list");
|
|
125
|
+
expect(out).toContain("not delivering");
|
|
126
|
+
expect(out).not.toContain("incomplete");
|
|
127
|
+
expect(out).not.toContain("not configured");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("list distinguishes a verdict it could not obtain from an outage", async () => {
|
|
131
|
+
mockResponses = [
|
|
132
|
+
{
|
|
133
|
+
ok: true,
|
|
134
|
+
result: { success: true, snapshots: [configuredButDown("unknown")] },
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
const out = await runCli("channels", "list");
|
|
138
|
+
expect(out).toContain("state unknown");
|
|
139
|
+
expect(out).not.toContain("not delivering");
|
|
140
|
+
expect(out).not.toContain("incomplete");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("setup that genuinely did not finish still reads incomplete", async () => {
|
|
144
|
+
// The sensitivity check on the two above: the word has to survive for
|
|
145
|
+
// the state it was always correct for.
|
|
146
|
+
mockResponses = [
|
|
147
|
+
{
|
|
148
|
+
ok: true,
|
|
149
|
+
result: {
|
|
150
|
+
success: true,
|
|
151
|
+
snapshots: [
|
|
152
|
+
emptySnapshot("slack", {
|
|
153
|
+
ready: false,
|
|
154
|
+
setupStatus: "incomplete",
|
|
155
|
+
}),
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
const out = await runCli("channels", "list");
|
|
161
|
+
expect(out).toContain("incomplete");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("the detail header agrees with its own glyph", async () => {
|
|
165
|
+
// It used to print the raw setupStatus beside a glyph derived from both
|
|
166
|
+
// axes, so an outage rendered as a warning sign next to the word ready.
|
|
167
|
+
mockResponses = [
|
|
168
|
+
{
|
|
169
|
+
ok: true,
|
|
170
|
+
result: { success: true, snapshots: [configuredButDown("failing")] },
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
const out = await runCli("channels", "get", "slack");
|
|
174
|
+
expect(out).toContain("slack — not delivering");
|
|
175
|
+
expect(out).not.toContain("slack — ready");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
94
179
|
describe("get", () => {
|
|
95
180
|
test("always re-probes via the refresh route (live, never cached)", async () => {
|
|
96
181
|
mockResponses = [
|