@vellumai/assistant 0.10.6-staging.1 → 0.10.6
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/openapi.yaml +43 -6
- package/package.json +1 -1
- package/src/__tests__/attachment-stored-path-annotation.test.ts +151 -0
- package/src/__tests__/attachments-store.test.ts +37 -1
- package/src/__tests__/attachments.test.ts +53 -0
- package/src/__tests__/channel-reply-delivery.test.ts +214 -5
- package/src/__tests__/config-loader-backfill.test.ts +16 -36
- package/src/__tests__/image-source-path-reinject.test.ts +155 -14
- package/src/__tests__/managed-profile-guard.test.ts +2 -0
- package/src/__tests__/mtime-cache.test.ts +265 -80
- package/src/__tests__/plugin-config-data-migration.test.ts +2 -4
- package/src/__tests__/plugin-effective-enabled-set.test.ts +9 -6
- package/src/__tests__/workspace-migration-125-repoint-managed-connections-to-vellum.test.ts +140 -0
- package/src/agent/attachments.ts +55 -13
- package/src/cli/commands/platform/__tests__/credits.test.ts +111 -0
- package/src/cli/commands/platform/index.ts +86 -4
- package/src/cli/lib/__tests__/confirm-prompt.test.ts +9 -9
- package/src/cli/lib/diff-plugin.ts +7 -3
- package/src/cli/lib/inspect-plugin.ts +28 -10
- package/src/cli/lib/install-from-github.ts +69 -24
- package/src/cli/lib/ipc-params.ts +3 -3
- package/src/cli/lib/list-installed-plugins.ts +11 -5
- package/src/cli/lib/merge-plugin-tree.ts +15 -9
- package/src/cli/lib/plugin-fingerprint.ts +0 -6
- package/src/cli/lib/publish-plugin.ts +1 -1
- package/src/cli/lib/unknown-command.ts +3 -1
- package/src/cli/lib/upgrade-plugin.ts +2 -5
- package/src/config/__tests__/sync-gated-profiles.test.ts +2 -2
- package/src/config/seed-inference-profiles.ts +5 -4
- package/src/daemon/conversation-lifecycle.ts +37 -15
- package/src/daemon/conversation-messaging.ts +56 -28
- package/src/daemon/conversation.ts +2 -2
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +6 -0
- package/src/hooks/__tests__/hook-live-reload.test.ts +110 -96
- package/src/hooks/hook-loader.ts +57 -144
- package/src/hooks/registry.ts +13 -9
- package/src/persistence/attachments-store.ts +2 -1
- package/src/persistence/conversation-crud.ts +24 -0
- package/src/plugin-api/types.ts +4 -1
- package/src/plugins/mtime-cache.ts +347 -33
- package/src/plugins/pipeline.ts +3 -2
- package/src/providers/__tests__/inference.test.ts +42 -31
- package/src/providers/inference/__tests__/connections-label.test.ts +9 -28
- package/src/providers/inference/auth.ts +1 -2
- package/src/providers/inference/backfill.ts +7 -5
- package/src/providers/inference/connections.ts +54 -30
- package/src/runtime/channel-reply-delivery.ts +83 -24
- package/src/runtime/channel-retry-sweep.ts +4 -0
- package/src/runtime/{slack-no-response.ts → no-response.ts} +26 -6
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +43 -3
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +61 -24
- package/src/runtime/routes/conversation-query-routes.ts +18 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +1 -1
- package/src/runtime/routes/inference-provider-connection-routes.ts +4 -3
- package/src/runtime/routes/platform-routes.ts +82 -1
- package/src/runtime/slack-reply-session.ts +1 -1
- package/src/workspace/migrations/125-repoint-managed-connections-to-vellum.ts +99 -0
- package/src/workspace/migrations/registry.ts +2 -0
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
import { dirname, join, resolve, sep } from "node:path";
|
|
40
40
|
import { promisify } from "node:util";
|
|
41
41
|
|
|
42
|
+
import { PRESERVED_ENTRIES } from "../../plugins/plugin-tree-walk.js";
|
|
42
43
|
import { ensureBun } from "../../util/bun-runtime.js";
|
|
43
44
|
import { getWorkspacePluginsDir } from "../../util/platform.js";
|
|
44
45
|
import {
|
|
@@ -46,7 +47,6 @@ import {
|
|
|
46
47
|
computeFingerprint,
|
|
47
48
|
type Fingerprint,
|
|
48
49
|
parseFingerprint,
|
|
49
|
-
PRESERVED_ENTRIES,
|
|
50
50
|
} from "./plugin-fingerprint.js";
|
|
51
51
|
import {
|
|
52
52
|
fetchMarketplaceEntries,
|
|
@@ -246,7 +246,9 @@ export class PluginSourceUnavailableError extends Error {
|
|
|
246
246
|
* a 403 without it is a genuine authorization failure and stays hard.
|
|
247
247
|
*/
|
|
248
248
|
function isTransientUpstreamStatus(res: Response): boolean {
|
|
249
|
-
if (res.status === 429 || res.status >= 500)
|
|
249
|
+
if (res.status === 429 || res.status >= 500) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
250
252
|
if (res.status === 403) {
|
|
251
253
|
return res.headers.get("x-ratelimit-remaining") === "0";
|
|
252
254
|
}
|
|
@@ -302,7 +304,9 @@ async function resolvePluginSource(
|
|
|
302
304
|
throw err;
|
|
303
305
|
}
|
|
304
306
|
|
|
305
|
-
if (!resolved)
|
|
307
|
+
if (!resolved) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
306
310
|
|
|
307
311
|
return {
|
|
308
312
|
owner: resolved.owner,
|
|
@@ -548,9 +552,13 @@ export function finalizeStagedInstall(
|
|
|
548
552
|
// user config and runtime data.
|
|
549
553
|
if (existsSync(target)) {
|
|
550
554
|
for (const entry of PRESERVED_ENTRIES) {
|
|
551
|
-
if (entry === INSTALL_META_FILENAME)
|
|
555
|
+
if (entry === INSTALL_META_FILENAME) {
|
|
556
|
+
continue;
|
|
557
|
+
} // sidecar is rewritten above
|
|
552
558
|
const src = join(target, entry);
|
|
553
|
-
if (!existsSync(src))
|
|
559
|
+
if (!existsSync(src)) {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
554
562
|
const dest = join(stagingDir, entry);
|
|
555
563
|
const stat = statSync(src);
|
|
556
564
|
if (stat.isDirectory()) {
|
|
@@ -755,8 +763,9 @@ async function copyExternalViaGit(
|
|
|
755
763
|
// A missing repo/ref (or a private one we can't reach) is a hard
|
|
756
764
|
// not-found, surfaced as zero files. Anything else — network loss, a
|
|
757
765
|
// transient GitHub outage — is retryable, so map it to a 503.
|
|
758
|
-
if (isGitRefNotFound(err))
|
|
766
|
+
if (isGitRefNotFound(err)) {
|
|
759
767
|
return { fileCount: 0, commit: null, committedAt: null };
|
|
768
|
+
}
|
|
760
769
|
throw new PluginSourceUnavailableError(
|
|
761
770
|
`git clone failed for ${sourceLabel(source)} @ ${source.ref}: ${subprocessErrorText(err)}`,
|
|
762
771
|
503,
|
|
@@ -865,10 +874,14 @@ async function applyAdapterStub(
|
|
|
865
874
|
stagingDir,
|
|
866
875
|
deps.fetch,
|
|
867
876
|
);
|
|
868
|
-
if (stubFileCount === 0)
|
|
877
|
+
if (stubFileCount === 0) {
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
869
880
|
|
|
870
881
|
const script = resolvePostinstallScript(name, stagingDir);
|
|
871
|
-
if (script === null)
|
|
882
|
+
if (script === null) {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
872
885
|
|
|
873
886
|
const run = deps.runPostinstall ?? defaultPostinstallRunner;
|
|
874
887
|
try {
|
|
@@ -891,7 +904,9 @@ type PackageManifest = Record<string, unknown>;
|
|
|
891
904
|
|
|
892
905
|
/** Parse the `package.json` at `path`, or null if it's absent or unparseable. */
|
|
893
906
|
function readPackageJson(path: string): PackageManifest | null {
|
|
894
|
-
if (!existsSync(path))
|
|
907
|
+
if (!existsSync(path)) {
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
895
910
|
try {
|
|
896
911
|
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
897
912
|
return typeof parsed === "object" &&
|
|
@@ -976,7 +991,9 @@ function resolvePostinstallScript(
|
|
|
976
991
|
stagingDir: string,
|
|
977
992
|
): string | null {
|
|
978
993
|
const pkgPath = join(stagingDir, "package.json");
|
|
979
|
-
if (!existsSync(pkgPath))
|
|
994
|
+
if (!existsSync(pkgPath)) {
|
|
995
|
+
return null;
|
|
996
|
+
}
|
|
980
997
|
|
|
981
998
|
let parsed: unknown;
|
|
982
999
|
try {
|
|
@@ -993,7 +1010,9 @@ function resolvePostinstallScript(
|
|
|
993
1010
|
typeof scripts === "object" && scripts !== null && "postinstall" in scripts
|
|
994
1011
|
? (scripts as { postinstall?: unknown }).postinstall
|
|
995
1012
|
: undefined;
|
|
996
|
-
if (typeof command !== "string" || command.trim() === "")
|
|
1013
|
+
if (typeof command !== "string" || command.trim() === "") {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
997
1016
|
|
|
998
1017
|
const match = /^bun\s+(\S+)$/.exec(command.trim());
|
|
999
1018
|
if (!match) {
|
|
@@ -1005,7 +1024,9 @@ function resolvePostinstallScript(
|
|
|
1005
1024
|
}
|
|
1006
1025
|
|
|
1007
1026
|
let rel = match[1]!;
|
|
1008
|
-
if (rel.startsWith("./"))
|
|
1027
|
+
if (rel.startsWith("./")) {
|
|
1028
|
+
rel = rel.slice(2);
|
|
1029
|
+
}
|
|
1009
1030
|
if (!/\.(?:ts|mts|cts|mjs|cjs|js)$/.test(rel)) {
|
|
1010
1031
|
throw new PluginPostinstallError(
|
|
1011
1032
|
name,
|
|
@@ -1074,7 +1095,9 @@ function pluginPostinstallEnv(bun: string): NodeJS.ProcessEnv {
|
|
|
1074
1095
|
.filter(Boolean)
|
|
1075
1096
|
.join(":"),
|
|
1076
1097
|
};
|
|
1077
|
-
if (process.env.HOME)
|
|
1098
|
+
if (process.env.HOME) {
|
|
1099
|
+
env.HOME = process.env.HOME;
|
|
1100
|
+
}
|
|
1078
1101
|
return env;
|
|
1079
1102
|
}
|
|
1080
1103
|
|
|
@@ -1090,7 +1113,9 @@ function copyTreeSkippingGit(srcRoot: string, destDir: string): number {
|
|
|
1090
1113
|
for (const entry of readdirSync(absDir, { withFileTypes: true })) {
|
|
1091
1114
|
// Drop git metadata and symlinks: the loader follows neither, and a
|
|
1092
1115
|
// symlink could otherwise point outside the staging tree.
|
|
1093
|
-
if (relDir === "" && entry.name === ".git")
|
|
1116
|
+
if (relDir === "" && entry.name === ".git") {
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1094
1119
|
// Drop a top-level `bunfig.toml`. The adapter postinstall runs `bun` with
|
|
1095
1120
|
// its cwd at the staged root, and Bun auto-loads `$cwd/bunfig.toml` as
|
|
1096
1121
|
// project config — including a `preload` list it executes before the
|
|
@@ -1101,15 +1126,21 @@ function copyTreeSkippingGit(srcRoot: string, destDir: string): number {
|
|
|
1101
1126
|
// consumes `bunfig.toml`. Match case-insensitively because the macOS
|
|
1102
1127
|
// install target's filesystem is case-insensitive, where Bun would still
|
|
1103
1128
|
// open a clone-supplied `BUNFIG.TOML`.
|
|
1104
|
-
if (relDir === "" && entry.name.toLowerCase() === "bunfig.toml")
|
|
1105
|
-
|
|
1129
|
+
if (relDir === "" && entry.name.toLowerCase() === "bunfig.toml") {
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
if (entry.isSymbolicLink()) {
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1106
1135
|
|
|
1107
1136
|
const rel = relDir ? join(relDir, entry.name) : entry.name;
|
|
1108
1137
|
if (entry.isDirectory()) {
|
|
1109
1138
|
walk(rel);
|
|
1110
1139
|
continue;
|
|
1111
1140
|
}
|
|
1112
|
-
if (!entry.isFile())
|
|
1141
|
+
if (!entry.isFile()) {
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1113
1144
|
|
|
1114
1145
|
const dest = join(destDir, rel);
|
|
1115
1146
|
mkdirSync(dirname(dest), { recursive: true });
|
|
@@ -1168,7 +1199,9 @@ export const defaultGitRunner: GitRunner = async (args, opts) => {
|
|
|
1168
1199
|
function pluginGitEnv(): NodeJS.ProcessEnv {
|
|
1169
1200
|
const env: NodeJS.ProcessEnv = {};
|
|
1170
1201
|
for (const [key, value] of Object.entries(process.env)) {
|
|
1171
|
-
if (value !== undefined && !key.startsWith("GIT_"))
|
|
1202
|
+
if (value !== undefined && !key.startsWith("GIT_")) {
|
|
1203
|
+
env[key] = value;
|
|
1204
|
+
}
|
|
1172
1205
|
}
|
|
1173
1206
|
env.GIT_TERMINAL_PROMPT = "0";
|
|
1174
1207
|
const extraPaths = ["/opt/homebrew/bin", "/usr/local/bin"];
|
|
@@ -1199,12 +1232,16 @@ interface WriteInstallMetaParams {
|
|
|
1199
1232
|
*/
|
|
1200
1233
|
function readStagedPackageVersion(stagingDir: string): string | undefined {
|
|
1201
1234
|
const pkgPath = join(stagingDir, "package.json");
|
|
1202
|
-
if (!existsSync(pkgPath))
|
|
1235
|
+
if (!existsSync(pkgPath)) {
|
|
1236
|
+
return undefined;
|
|
1237
|
+
}
|
|
1203
1238
|
try {
|
|
1204
1239
|
const parsed: unknown = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
1205
1240
|
if (typeof parsed === "object" && parsed !== null) {
|
|
1206
1241
|
const version = (parsed as Record<string, unknown>).version;
|
|
1207
|
-
if (typeof version === "string" && version.length > 0)
|
|
1242
|
+
if (typeof version === "string" && version.length > 0) {
|
|
1243
|
+
return version;
|
|
1244
|
+
}
|
|
1208
1245
|
}
|
|
1209
1246
|
} catch {
|
|
1210
1247
|
// fall through to undefined
|
|
@@ -1267,7 +1304,9 @@ function writeInstallMeta(
|
|
|
1267
1304
|
*/
|
|
1268
1305
|
export function readInstallMeta(pluginDir: string): InstallMeta | null {
|
|
1269
1306
|
const metaPath = join(pluginDir, INSTALL_META_FILENAME);
|
|
1270
|
-
if (!existsSync(metaPath))
|
|
1307
|
+
if (!existsSync(metaPath)) {
|
|
1308
|
+
return null;
|
|
1309
|
+
}
|
|
1271
1310
|
|
|
1272
1311
|
let parsed: unknown;
|
|
1273
1312
|
try {
|
|
@@ -1343,12 +1382,16 @@ async function copyDir(
|
|
|
1343
1382
|
fetchFn: FetchLike,
|
|
1344
1383
|
): Promise<number> {
|
|
1345
1384
|
const entries = await listDir(owner, repo, apiPath, ref, fetchFn);
|
|
1346
|
-
if (entries === null)
|
|
1385
|
+
if (entries === null) {
|
|
1386
|
+
return 0;
|
|
1387
|
+
}
|
|
1347
1388
|
|
|
1348
1389
|
let count = 0;
|
|
1349
1390
|
for (const entry of entries) {
|
|
1350
1391
|
// The daemon loader follows neither symlinks nor submodules; skip them.
|
|
1351
|
-
if (entry.type === "symlink" || entry.type === "submodule")
|
|
1392
|
+
if (entry.type === "symlink" || entry.type === "submodule") {
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1352
1395
|
assertSafeFilename("entry name", entry.name);
|
|
1353
1396
|
|
|
1354
1397
|
if (entry.type === "dir") {
|
|
@@ -1402,7 +1445,9 @@ async function listDir(
|
|
|
1402
1445
|
`/contents/${encodeURIComponent(apiPath).replaceAll("%2F", "/")}?ref=${encodeURIComponent(ref)}`;
|
|
1403
1446
|
|
|
1404
1447
|
const res = await githubFetch(url, "application/vnd.github+json", fetchFn);
|
|
1405
|
-
if (res.status === 404)
|
|
1448
|
+
if (res.status === 404) {
|
|
1449
|
+
return null;
|
|
1450
|
+
}
|
|
1406
1451
|
if (!res.ok) {
|
|
1407
1452
|
const label = `GitHub contents listing failed for ${apiPath} @ ${ref}: HTTP ${res.status}`;
|
|
1408
1453
|
if (isTransientUpstreamStatus(res)) {
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
/** Keys that are CLI-presentation concerns, not IPC params. */
|
|
11
11
|
const CLI_ONLY_KEYS = new Set(["json", "verbose"]);
|
|
12
12
|
|
|
13
|
-
export function optsToQueryParams(
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
export function optsToQueryParams(opts: Record<string, unknown>): {
|
|
14
|
+
queryParams: Record<string, string>;
|
|
15
|
+
} {
|
|
16
16
|
const queryParams: Record<string, string> = {};
|
|
17
17
|
for (const [k, v] of Object.entries(opts)) {
|
|
18
18
|
if (CLI_ONLY_KEYS.has(k)) continue;
|
|
@@ -206,9 +206,7 @@ export function listAllPlugins(
|
|
|
206
206
|
// ── User plugins ───────────────────────────────────────────────────────
|
|
207
207
|
// Filter out default-plugin stub directories (created by `plugins disable
|
|
208
208
|
// default-<name>`) so they don't show up as duplicate user entries.
|
|
209
|
-
const defaultNames = new Set(
|
|
210
|
-
readDefaultPluginManifests().map((m) => m.name),
|
|
211
|
-
);
|
|
209
|
+
const defaultNames = new Set(readDefaultPluginManifests().map((m) => m.name));
|
|
212
210
|
const userPlugins: AllPluginInfo[] = listInstalledPlugins(opts)
|
|
213
211
|
.filter((entry) => !defaultNames.has(entry.name))
|
|
214
212
|
.map((entry) => ({
|
|
@@ -255,7 +253,12 @@ export function listAllPlugins(
|
|
|
255
253
|
);
|
|
256
254
|
// enabledDefault and disabledDefault keep repo array order (no sort).
|
|
257
255
|
|
|
258
|
-
return [
|
|
256
|
+
return [
|
|
257
|
+
...enabledUser,
|
|
258
|
+
...disabledUser,
|
|
259
|
+
...enabledDefault,
|
|
260
|
+
...disabledDefault,
|
|
261
|
+
];
|
|
259
262
|
}
|
|
260
263
|
|
|
261
264
|
interface DefaultPluginManifest {
|
|
@@ -309,7 +312,10 @@ function getPluginInstallDate(plugin: AllPluginInfo): number {
|
|
|
309
312
|
const metaPath = join(plugin.target, "install-meta.json");
|
|
310
313
|
try {
|
|
311
314
|
if (existsSync(metaPath)) {
|
|
312
|
-
const raw = JSON.parse(readFileSync(metaPath, "utf8")) as Record<
|
|
315
|
+
const raw = JSON.parse(readFileSync(metaPath, "utf8")) as Record<
|
|
316
|
+
string,
|
|
317
|
+
unknown
|
|
318
|
+
>;
|
|
313
319
|
if (typeof raw.installedAt === "string") {
|
|
314
320
|
const ms = Date.parse(raw.installedAt);
|
|
315
321
|
if (Number.isFinite(ms)) return ms;
|
|
@@ -44,10 +44,8 @@ import { tmpdir } from "node:os";
|
|
|
44
44
|
import { dirname, join } from "node:path";
|
|
45
45
|
import { promisify } from "node:util";
|
|
46
46
|
|
|
47
|
-
import {
|
|
48
|
-
|
|
49
|
-
PRESERVED_ENTRIES,
|
|
50
|
-
} from "./plugin-fingerprint.js";
|
|
47
|
+
import { PRESERVED_ENTRIES } from "../../plugins/plugin-tree-walk.js";
|
|
48
|
+
import { computeFingerprint } from "./plugin-fingerprint.js";
|
|
51
49
|
|
|
52
50
|
const execFileAsync = promisify(execFile);
|
|
53
51
|
|
|
@@ -122,7 +120,9 @@ interface MergedFile {
|
|
|
122
120
|
function isBinary(buf: Buffer): boolean {
|
|
123
121
|
const len = Math.min(buf.length, 8000);
|
|
124
122
|
for (let i = 0; i < len; i++) {
|
|
125
|
-
if (buf[i] === 0)
|
|
123
|
+
if (buf[i] === 0) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
126
|
}
|
|
127
127
|
return false;
|
|
128
128
|
}
|
|
@@ -153,10 +153,12 @@ async function threeWayMergeFile(
|
|
|
153
153
|
labels: ConflictLabels,
|
|
154
154
|
): Promise<MergedFile> {
|
|
155
155
|
if (isBinary(ours) || isBinary(base) || isBinary(theirs)) {
|
|
156
|
-
if (ours.equals(base))
|
|
156
|
+
if (ours.equals(base)) {
|
|
157
157
|
return { content: theirs, conflicted: false, binaryConflicted: false };
|
|
158
|
-
|
|
158
|
+
}
|
|
159
|
+
if (theirs.equals(base)) {
|
|
159
160
|
return { content: ours, conflicted: false, binaryConflicted: false };
|
|
161
|
+
}
|
|
160
162
|
if (strategy === "assistant") {
|
|
161
163
|
return { content: ours, conflicted: false, binaryConflicted: true };
|
|
162
164
|
}
|
|
@@ -287,8 +289,12 @@ export async function mergePluginTree({
|
|
|
287
289
|
labels,
|
|
288
290
|
);
|
|
289
291
|
keep(rel, merged.content);
|
|
290
|
-
if (merged.conflicted)
|
|
291
|
-
|
|
292
|
+
if (merged.conflicted) {
|
|
293
|
+
conflicts.push(rel);
|
|
294
|
+
}
|
|
295
|
+
if (merged.binaryConflicted) {
|
|
296
|
+
binaryConflicts.push(rel);
|
|
297
|
+
}
|
|
292
298
|
}
|
|
293
299
|
continue;
|
|
294
300
|
}
|
|
@@ -159,12 +159,6 @@ export function parseFingerprint(value: unknown): Fingerprint | null {
|
|
|
159
159
|
return { algorithm: "sha256", files };
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
// The preserved-entries constant lives with the shared walk so install
|
|
163
|
-
// fingerprinting and the live-reload source fingerprint can't disagree on
|
|
164
|
-
// what counts as the plugin's source tree; re-exported here for the
|
|
165
|
-
// install/upgrade/diff callers that treat this module as the fingerprint API.
|
|
166
|
-
export { PRESERVED_ENTRIES } from "../../plugins/plugin-tree-walk.js";
|
|
167
|
-
|
|
168
162
|
/**
|
|
169
163
|
* Aggregate SHA-256 digest over a tree's contents, returned as `v2:<hex>`.
|
|
170
164
|
*
|
|
@@ -46,7 +46,9 @@ export function detectUnknownCommand(
|
|
|
46
46
|
if (known.has(firstPositional)) return null;
|
|
47
47
|
|
|
48
48
|
const suggestion = findClosestCommand(firstPositional, [...known]);
|
|
49
|
-
return suggestion
|
|
49
|
+
return suggestion
|
|
50
|
+
? { token: firstPositional, suggestion }
|
|
51
|
+
: { token: firstPositional };
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/**
|
|
@@ -32,6 +32,7 @@ import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
|
32
32
|
import { tmpdir } from "node:os";
|
|
33
33
|
import { dirname, join } from "node:path";
|
|
34
34
|
|
|
35
|
+
import { PRESERVED_ENTRIES } from "../../plugins/plugin-tree-walk.js";
|
|
35
36
|
import { getWorkspacePluginsDir } from "../../util/platform.js";
|
|
36
37
|
import {
|
|
37
38
|
inspectPlugin,
|
|
@@ -55,11 +56,7 @@ import {
|
|
|
55
56
|
sanitizePluginName,
|
|
56
57
|
} from "./install-from-github.js";
|
|
57
58
|
import { type ConflictLabels, mergePluginTree } from "./merge-plugin-tree.js";
|
|
58
|
-
import {
|
|
59
|
-
computeFingerprint,
|
|
60
|
-
fingerprintsEqual,
|
|
61
|
-
PRESERVED_ENTRIES,
|
|
62
|
-
} from "./plugin-fingerprint.js";
|
|
59
|
+
import { computeFingerprint, fingerprintsEqual } from "./plugin-fingerprint.js";
|
|
63
60
|
import { PluginNotInstalledError } from "./uninstall-plugin.js";
|
|
64
61
|
|
|
65
62
|
/**
|
|
@@ -110,7 +110,7 @@ describe("reconcileFlagGatedProfiles", () => {
|
|
|
110
110
|
const raw = readConfig();
|
|
111
111
|
const osBeta = raw.llm.profiles["os-beta"]!;
|
|
112
112
|
expect(osBeta.model).toBe("MiniMaxAI/MiniMax-M3");
|
|
113
|
-
expect(osBeta.provider_connection).toBe("
|
|
113
|
+
expect(osBeta.provider_connection).toBe("vellum");
|
|
114
114
|
expect(osBeta.provider).toBe("together");
|
|
115
115
|
expect(osBeta.source).toBe("managed");
|
|
116
116
|
expect(osBeta.label).toBe("OS Beta");
|
|
@@ -164,7 +164,7 @@ describe("reconcileFlagGatedProfiles", () => {
|
|
|
164
164
|
expect(after.status).toBe("disabled");
|
|
165
165
|
expect(after.topP).toBe(0.8);
|
|
166
166
|
expect(after.model).toBe("MiniMaxAI/MiniMax-M3");
|
|
167
|
-
expect(after.provider_connection).toBe("
|
|
167
|
+
expect(after.provider_connection).toBe("vellum");
|
|
168
168
|
expect(after.effort).toBe("low");
|
|
169
169
|
});
|
|
170
170
|
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
getConnection,
|
|
5
5
|
MANAGED_CONNECTION_NAMES,
|
|
6
6
|
PROVIDERS_REQUIRING_BASE_URL_AND_MODELS,
|
|
7
|
+
VELLUM_MANAGED_CONNECTION_NAME,
|
|
7
8
|
} from "../providers/inference/connections.js";
|
|
8
9
|
import { PROVIDER_CATALOG } from "../providers/model-catalog.js";
|
|
9
10
|
import { resolveModelIntent } from "../providers/model-intents.js";
|
|
@@ -49,7 +50,7 @@ const MANAGED_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
|
|
|
49
50
|
balanced: {
|
|
50
51
|
model: "accounts/fireworks/models/glm-5p2",
|
|
51
52
|
provider: "fireworks",
|
|
52
|
-
connectionName:
|
|
53
|
+
connectionName: VELLUM_MANAGED_CONNECTION_NAME,
|
|
53
54
|
source: "managed",
|
|
54
55
|
label: "Balanced",
|
|
55
56
|
description: "Good balance of quality, cost, and speed",
|
|
@@ -64,7 +65,7 @@ const MANAGED_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
|
|
|
64
65
|
"quality-optimized": {
|
|
65
66
|
intent: "quality-optimized",
|
|
66
67
|
provider: "anthropic",
|
|
67
|
-
connectionName:
|
|
68
|
+
connectionName: VELLUM_MANAGED_CONNECTION_NAME,
|
|
68
69
|
source: "managed",
|
|
69
70
|
label: "Quality",
|
|
70
71
|
description: "High-quality results with the most capable model",
|
|
@@ -85,7 +86,7 @@ const MANAGED_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
|
|
|
85
86
|
"cost-optimized": {
|
|
86
87
|
model: "accounts/fireworks/models/deepseek-v4-flash",
|
|
87
88
|
provider: "fireworks",
|
|
88
|
-
connectionName:
|
|
89
|
+
connectionName: VELLUM_MANAGED_CONNECTION_NAME,
|
|
89
90
|
source: "managed",
|
|
90
91
|
label: "Speed",
|
|
91
92
|
description: "Fastest responses at lower cost (DeepSeek V4 Flash)",
|
|
@@ -154,7 +155,7 @@ export const OS_BETA_FEATURE_FLAG_KEY = "os-beta";
|
|
|
154
155
|
export const OS_BETA_PROFILE_TEMPLATE: ManagedProfileTemplate = {
|
|
155
156
|
intent: "balanced",
|
|
156
157
|
provider: "together",
|
|
157
|
-
connectionName:
|
|
158
|
+
connectionName: VELLUM_MANAGED_CONNECTION_NAME,
|
|
158
159
|
source: "managed",
|
|
159
160
|
label: "OS Beta",
|
|
160
161
|
description: "Good balance of quality, cost, and speed, in beta",
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* can delegate without exposing its full surface.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import {
|
|
8
|
+
formatImageSourceAnnotation,
|
|
9
|
+
formatStoredPathAnnotation,
|
|
10
|
+
} from "../agent/attachments.js";
|
|
7
11
|
import { getConfig } from "../config/loader.js";
|
|
8
12
|
import type { EventBus } from "../events/bus.js";
|
|
9
13
|
import type { AssistantDomainEvents } from "../events/domain-events.js";
|
|
@@ -32,14 +36,19 @@ import type { SurfaceData, SurfaceType } from "./message-protocol.js";
|
|
|
32
36
|
const log = getLogger("conversation-lifecycle");
|
|
33
37
|
|
|
34
38
|
/**
|
|
35
|
-
* Re-inject
|
|
39
|
+
* Re-inject attachment path annotations into message content blocks.
|
|
36
40
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
+
* The LLM-facing content omits path annotations at persistence time, so we
|
|
42
|
+
* re-inject them when loading history from the DB, from two metadata keys:
|
|
43
|
+
* `imageSourcePaths` (where desktop-attached images came from) and
|
|
44
|
+
* `attachmentStoredPaths` (the canonical, collision-suffixed copies in the
|
|
45
|
+
* conversation's attachments/ directory), both keyed by
|
|
46
|
+
* `${position}:${filename}`. The rebuilt block must stay byte-identical to
|
|
47
|
+
* the one `enrichMessageWithSourcePaths` appends at persist time so reloads
|
|
48
|
+
* and forks keep provider prefix-cache parity. Only user messages are
|
|
49
|
+
* annotated.
|
|
41
50
|
*/
|
|
42
|
-
export function
|
|
51
|
+
export function reinjectAttachmentPathAnnotations(
|
|
43
52
|
content: ContentBlock[],
|
|
44
53
|
role: string,
|
|
45
54
|
metadataJson: string | null,
|
|
@@ -47,17 +56,30 @@ export function reinjectImageSourcePaths(
|
|
|
47
56
|
if (role !== "user" || !metadataJson) return content;
|
|
48
57
|
try {
|
|
49
58
|
const meta = JSON.parse(metadataJson);
|
|
50
|
-
|
|
59
|
+
const lines: string[] = [];
|
|
60
|
+
if (meta.imageSourcePaths && typeof meta.imageSourcePaths === "object") {
|
|
61
|
+
for (const p of Object.values(meta.imageSourcePaths)) {
|
|
62
|
+
if (typeof p === "string") {
|
|
63
|
+
lines.push(formatImageSourceAnnotation(p));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
meta.attachmentStoredPaths &&
|
|
69
|
+
typeof meta.attachmentStoredPaths === "object"
|
|
70
|
+
) {
|
|
71
|
+
for (const [key, p] of Object.entries(meta.attachmentStoredPaths)) {
|
|
72
|
+
if (typeof p !== "string") {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const filename = key.slice(key.indexOf(":") + 1);
|
|
76
|
+
lines.push(formatStoredPathAnnotation(filename, p));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (lines.length === 0) {
|
|
51
80
|
return content;
|
|
52
81
|
}
|
|
53
|
-
const
|
|
54
|
-
(v): v is string => typeof v === "string",
|
|
55
|
-
);
|
|
56
|
-
if (paths.length === 0) return content;
|
|
57
|
-
const annotation = paths
|
|
58
|
-
.map((p) => `[Attached image source: ${p}]`)
|
|
59
|
-
.join("\n");
|
|
60
|
-
return [...content, { type: "text" as const, text: annotation }];
|
|
82
|
+
return [...content, { type: "text" as const, text: lines.join("\n") }];
|
|
61
83
|
} catch {
|
|
62
84
|
// metadata parse failure — skip annotation, not critical
|
|
63
85
|
return content;
|