@vellumai/assistant 0.9.0 → 0.9.1-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/ARCHITECTURE.md +18 -34
- package/bun.lock +7 -8
- package/docs/activation-funnel-telemetry.md +4 -4
- package/docs/architecture/security.md +29 -28
- package/docs/stt-provider-onboarding.md +3 -5
- package/docs/workflows-testing.md +13 -44
- package/docs/workflows.md +3 -5
- package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
- package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
- package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
- package/node_modules/@vellumai/gateway-client/src/index.ts +17 -6
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
- package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
- package/openapi.yaml +135 -59
- package/package.json +2 -1
- package/scripts/sync-llm-catalog.ts +6 -15
- package/scripts/sync-web-search-catalog.ts +3 -11
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +14 -26
- package/src/__tests__/agent-loop-compaction-strip.test.ts +240 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
- package/src/__tests__/app-dir-path-guard.test.ts +0 -1
- package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
- package/src/__tests__/avatar-identity-sync.test.ts +2 -27
- package/src/__tests__/btw-routes.test.ts +6 -8
- package/src/__tests__/checker.test.ts +0 -3
- package/src/__tests__/config-loader-backfill.test.ts +103 -6
- package/src/__tests__/config-watcher.test.ts +0 -18
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
- package/src/__tests__/credential-broker.test.ts +449 -1
- package/src/__tests__/credential-execution-tools.test.ts +0 -1
- package/src/__tests__/credential-prompt-route.test.ts +3 -4
- package/src/__tests__/credential-routes.test.ts +360 -0
- package/src/__tests__/credential-security-invariants.test.ts +2 -13
- package/src/__tests__/dynamic-page-surface.test.ts +101 -1
- package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
- package/src/__tests__/gateway-only-guard.test.ts +3 -7
- package/src/__tests__/identity-routes.test.ts +0 -189
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/invite-redemption-service.test.ts +4 -4
- package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
- package/src/__tests__/llm-catalog-parity.test.ts +0 -22
- package/src/__tests__/llm-resolver.test.ts +49 -24
- package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
- package/src/__tests__/onboarding-persona-write.test.ts +1 -1
- package/src/__tests__/persona-resolver.test.ts +11 -14
- package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
- package/src/__tests__/registry.test.ts +2 -7
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
- package/src/__tests__/schedule-routes.test.ts +0 -30
- package/src/__tests__/schedule-tools.test.ts +2 -18
- package/src/__tests__/skill-execute-input.test.ts +46 -1
- package/src/__tests__/skill-runtime-path.test.ts +2 -3
- package/src/__tests__/subagent-tools.test.ts +116 -0
- package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
- package/src/__tests__/token-manager.test.ts +519 -0
- package/src/__tests__/tool-executor.test.ts +0 -79
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +6 -6
- package/src/__tests__/voice-invite-redemption.test.ts +2 -2
- package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
- package/src/__tests__/workspace-greetings.test.ts +152 -0
- package/src/agent/loop.ts +25 -5
- package/src/api/README.md +6 -6
- package/src/api/responses/conversation-message.ts +2 -4
- package/src/api/responses/home.ts +0 -4
- package/src/approvals/guardian-request-resolvers.ts +2 -2
- package/src/calls/relay-access-wait.ts +1 -1
- package/src/calls/voice-session-bridge.ts +2 -2
- package/src/cli/commands/plugins.ts +143 -2
- package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +313 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +253 -2
- package/src/cli/lib/diff-plugin.ts +346 -0
- package/src/cli/lib/install-from-github.ts +105 -17
- package/src/cli/lib/merge-plugin-tree.ts +228 -0
- package/src/cli/lib/plugin-fingerprint.ts +14 -0
- package/src/cli/lib/upgrade-plugin.ts +270 -10
- package/src/cli/program.ts +0 -2
- package/src/config/bundled-skills/subagent/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
- package/src/config/bundled-skills/workflows/SKILL.md +0 -1
- package/src/config/bundled-tool-registry.ts +2 -7
- package/src/config/call-site-defaults.ts +12 -2
- package/src/config/feature-flag-registry.json +1 -17
- package/src/config/inference-profile-validation.ts +26 -0
- package/src/config/loader.ts +4 -0
- package/src/config/profile-order.ts +28 -0
- package/src/config/schemas/elevenlabs.ts +0 -1
- package/src/config/schemas/platform.ts +0 -8
- package/src/config/seed-inference-profiles.ts +25 -20
- package/src/contacts/contact-store.ts +87 -96
- package/src/contacts/contacts-write.ts +5 -21
- package/src/context/compactor.ts +2 -2
- package/src/credential-execution/process-manager.ts +55 -14
- package/src/credential-execution/prompted-credential.ts +2 -3
- package/src/daemon/config-watcher.ts +0 -4
- package/src/daemon/conversation-agent-loop.ts +15 -4
- package/src/daemon/conversation-slash.ts +2 -23
- package/src/daemon/conversation-tool-setup.ts +11 -3
- package/src/daemon/conversation.ts +2 -0
- package/src/daemon/handlers/config-channels.ts +20 -16
- package/src/daemon/handlers/config-slack-channel.ts +2 -3
- package/src/daemon/lifecycle.ts +0 -7
- package/src/daemon/message-types/conversations.ts +3 -3
- package/src/daemon/message-types/sync.ts +0 -1
- package/src/daemon/orphan-reaper.test.ts +0 -19
- package/src/daemon/orphan-reaper.ts +2 -24
- package/src/daemon/server.ts +0 -10
- package/src/home/relationship-state.ts +2 -4
- package/src/memory/__tests__/memory-retrospective-job.test.ts +195 -401
- package/src/memory/bookmark-crud.ts +1 -2
- package/src/memory/db-init.ts +7 -17
- package/src/memory/embedding-backend.ts +23 -0
- package/src/memory/embedding-billing-breaker.ts +96 -0
- package/src/memory/jobs-store.ts +25 -13
- package/src/memory/jobs-worker.ts +52 -0
- package/src/memory/memory-retrospective-constants.ts +4 -4
- package/src/memory/memory-retrospective-job.ts +19 -227
- package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +62 -0
- package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +311 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/run-migrations.ts +41 -0
- package/src/memory/migrations/validate-migration-state.ts +1 -1
- package/src/memory/schema/contacts.ts +0 -4
- package/src/messaging/providers/slack/adapter.ts +1 -1
- package/src/notifications/adapters/shared.ts +29 -0
- package/src/notifications/adapters/slack.ts +5 -32
- package/src/notifications/adapters/telegram.ts +2 -20
- package/src/notifications/broadcaster.ts +10 -1
- package/src/notifications/home-feed-side-effect.ts +4 -3
- package/src/notifications/notification-utils.ts +17 -19
- package/src/notifications/types.ts +7 -0
- package/src/oauth/AGENTS.md +5 -24
- package/src/plugin-api/constants.ts +1 -1
- package/src/plugin-api/index.ts +6 -1
- package/src/plugin-api/model-profiles.ts +33 -0
- package/src/plugin-api/types.ts +50 -2
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +54 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +107 -7
- package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
- package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
- package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +1 -1
- package/src/prompts/persona-resolver.ts +2 -2
- package/src/runtime/AGENTS.md +0 -1
- package/src/runtime/actor-trust-resolver.ts +12 -44
- package/src/runtime/btw-sidechain.ts +3 -6
- package/src/runtime/channel-approval-types.ts +18 -45
- package/src/runtime/channel-invite-transports/telegram.ts +4 -4
- package/src/runtime/channel-verification-service.ts +4 -3
- package/src/runtime/invite-redemption-service.ts +3 -3
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +218 -1
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
- package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
- package/src/runtime/routes/btw-routes.ts +1 -27
- package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +2 -2
- package/src/runtime/routes/credential-routes.ts +40 -16
- package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
- package/src/runtime/routes/identity-routes.ts +1 -296
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +171 -5
- package/src/runtime/routes/schedule-routes.ts +0 -22
- package/src/runtime/routes/workflow-routes.test.ts +4 -43
- package/src/runtime/routes/workflow-routes.ts +0 -28
- package/src/runtime/routes/workspace-greetings.ts +55 -0
- package/src/runtime/sync/resource-sync-events.ts +1 -11
- package/src/schedule/inference-profile.ts +2 -14
- package/src/subagent/manager.ts +6 -0
- package/src/subagent/types.ts +6 -0
- package/src/tools/AGENTS.md +3 -3
- package/src/tools/browser/browser-execution.ts +1 -1
- package/src/tools/network/web-search-error.ts +1 -1
- package/src/tools/permission-checker.ts +1 -1
- package/src/tools/schedule/create.ts +3 -9
- package/src/tools/schedule/update.ts +2 -10
- package/src/tools/side-effects.ts +2 -17
- package/src/tools/skills/execute.ts +34 -0
- package/src/tools/subagent/spawn.ts +34 -9
- package/src/tools/tool-approval-handler.ts +1 -3
- package/src/tools/tool-manifest.ts +0 -2
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/workflows/run-workflow.test.ts +8 -18
- package/src/util/platform.ts +2 -2
- package/src/workflows/capabilities.ts +2 -3
- package/src/workflows/run-manager.test.ts +0 -25
- package/src/workflows/run-manager.ts +2 -24
- package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
- package/src/__tests__/credential-security-e2e.test.ts +0 -362
- package/src/__tests__/credential-vault-unit.test.ts +0 -1528
- package/src/__tests__/credential-vault.test.ts +0 -1706
- package/src/__tests__/identity-intro-cache.test.ts +0 -315
- package/src/__tests__/secret-onetime-send.test.ts +0 -182
- package/src/cli/commands/__tests__/task.test.ts +0 -914
- package/src/cli/commands/task.ts +0 -771
- package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
- package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
- package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
- package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
- package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
- package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
- package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
- package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
- package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
- package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
- package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
- package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
- package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
- package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
- package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
- package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
- package/src/memory/preloaded-apps.ts +0 -116
- package/src/runtime/routes/identity-intro-cache.ts +0 -172
- package/src/tools/credentials/vault.ts +0 -712
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Show the unified diff of local edits to an installed plugin, against the
|
|
3
|
+
* exact commit it was installed at.
|
|
4
|
+
*
|
|
5
|
+
* `plugins inspect` already reports *which* files drifted via the install-time
|
|
6
|
+
* per-file fingerprint (see {@link ./plugin-fingerprint}), but that digest is
|
|
7
|
+
* one-way — it cannot reconstruct the original bytes, so it can't show *what*
|
|
8
|
+
* changed. Drift here is classified the same way `inspect` does — against the
|
|
9
|
+
* fingerprint recorded at install — so the two surfaces always agree and a
|
|
10
|
+
* curated adapter overlay that changed since install never misreads as local
|
|
11
|
+
* drift.
|
|
12
|
+
*
|
|
13
|
+
* Showing *what* changed still needs the baseline *bytes*, which the fingerprint
|
|
14
|
+
* cannot reconstruct. Those are re-derived by re-materializing the recorded
|
|
15
|
+
* commit (from `install-meta.json`) through the *same* pipeline install used
|
|
16
|
+
* (see {@link ./install-from-github.materializePluginTree}): a shallow clone at
|
|
17
|
+
* the immutable SHA plus the curated adapter overlay. The adapter overlay is
|
|
18
|
+
* fetched from the canonical repo's current ref, not the install-time ref (which
|
|
19
|
+
* is not recorded), so a re-materialized file can diverge from what install
|
|
20
|
+
* produced. Each baseline file is therefore verified against the recorded
|
|
21
|
+
* fingerprint digest before it is diffed: on a mismatch the install-time bytes
|
|
22
|
+
* cannot be faithfully reconstructed, so the file is flagged rather than diffed
|
|
23
|
+
* against a fabricated baseline.
|
|
24
|
+
*
|
|
25
|
+
* The baseline is always the recorded install commit, not the marketplace's
|
|
26
|
+
* current pin: this answers "what local changes have been made since install",
|
|
27
|
+
* independent of marketplace movement. Comparing against the latest pin is the
|
|
28
|
+
* separate concern `plugins upgrade --dry-run` already covers.
|
|
29
|
+
*
|
|
30
|
+
* Designed for direct programmatic use with injected dependencies, mirroring
|
|
31
|
+
* the sibling plugin libraries. The CLI command `assistant plugins diff <name>`
|
|
32
|
+
* is a thin wrapper that supplies production deps and formats the result.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
36
|
+
import { tmpdir } from "node:os";
|
|
37
|
+
import { join } from "node:path";
|
|
38
|
+
|
|
39
|
+
import { createTwoFilesPatch } from "diff";
|
|
40
|
+
|
|
41
|
+
import { getWorkspacePluginsDir } from "../../util/platform.js";
|
|
42
|
+
import {
|
|
43
|
+
DEFAULT_PLUGIN_REF,
|
|
44
|
+
type FetchLike,
|
|
45
|
+
type GitRunner,
|
|
46
|
+
INSTALL_META_FILENAME,
|
|
47
|
+
materializePluginTree,
|
|
48
|
+
type PluginFetchSource,
|
|
49
|
+
PluginNotFoundError,
|
|
50
|
+
type PostinstallRunner,
|
|
51
|
+
readInstallMeta,
|
|
52
|
+
sanitizePluginName,
|
|
53
|
+
} from "./install-from-github.js";
|
|
54
|
+
import { readInstalledPlugin } from "./list-installed-plugins.js";
|
|
55
|
+
import {
|
|
56
|
+
compareFingerprint,
|
|
57
|
+
computeFingerprint,
|
|
58
|
+
type Fingerprint,
|
|
59
|
+
} from "./plugin-fingerprint.js";
|
|
60
|
+
import { PluginNotInstalledError } from "./uninstall-plugin.js";
|
|
61
|
+
|
|
62
|
+
/** How a file drifted from the install-time baseline. */
|
|
63
|
+
export type PluginFileDiffStatus = "modified" | "added" | "removed";
|
|
64
|
+
|
|
65
|
+
/** Unified diff of a single drifted file. */
|
|
66
|
+
export interface PluginFileDiff {
|
|
67
|
+
/** POSIX-relative path within the plugin root. */
|
|
68
|
+
readonly path: string;
|
|
69
|
+
/** Whether the file was edited, newly added, or deleted since install. */
|
|
70
|
+
readonly status: PluginFileDiffStatus;
|
|
71
|
+
/**
|
|
72
|
+
* Unified diff (`--- a/… / +++ b/…`) of the file's bytes. For a binary file
|
|
73
|
+
* this is a short `Binary files differ` marker instead of a line diff, since
|
|
74
|
+
* a line-based patch of non-text content is noise. When {@link
|
|
75
|
+
* PluginFileDiff.reconstructed} is false this is a short explanatory marker
|
|
76
|
+
* rather than a patch, since the install-time bytes are unavailable.
|
|
77
|
+
*/
|
|
78
|
+
readonly diff: string;
|
|
79
|
+
/** True when either side was detected as binary (NUL byte present). */
|
|
80
|
+
readonly binary: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* True when the install-time baseline for this file was faithfully recovered
|
|
83
|
+
* (its re-materialized bytes hash-match the digest recorded at install).
|
|
84
|
+
* False when the baseline could not be reconstructed — e.g. the curated
|
|
85
|
+
* adapter overlay the file was built from has changed since install — in
|
|
86
|
+
* which case {@link PluginFileDiff.diff} is a marker, not a real patch.
|
|
87
|
+
* Always true for `added` files, whose baseline is the empty side.
|
|
88
|
+
*/
|
|
89
|
+
readonly reconstructed: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Resolved diff of an installed plugin against its install-time baseline. */
|
|
93
|
+
export interface PluginDiffResult {
|
|
94
|
+
/** Install name. Matches `assistant plugins install <name>`. */
|
|
95
|
+
readonly name: string;
|
|
96
|
+
/** Absolute path to the installed plugin directory. */
|
|
97
|
+
readonly target: string;
|
|
98
|
+
/** Commit the baseline was re-materialized from (the recorded install SHA). */
|
|
99
|
+
readonly commit: string;
|
|
100
|
+
/** ISO-8601 committer timestamp (UTC) of {@link PluginDiffResult.commit}; `null` when unrecorded. */
|
|
101
|
+
readonly committedAt: string | null;
|
|
102
|
+
/** True when the on-disk tree exactly matches the re-materialized baseline. */
|
|
103
|
+
readonly clean: boolean;
|
|
104
|
+
/** One entry per drifted file, sorted by path. Empty when `clean`. */
|
|
105
|
+
readonly files: readonly PluginFileDiff[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The installed copy carries no resolvable commit, so there is no immutable
|
|
110
|
+
* baseline to re-materialize and diff against.
|
|
111
|
+
*/
|
|
112
|
+
export class PluginDiffUnavailableError extends Error {
|
|
113
|
+
constructor(
|
|
114
|
+
readonly pluginName: string,
|
|
115
|
+
reason: string,
|
|
116
|
+
) {
|
|
117
|
+
super(`Plugin "${pluginName}" cannot be diffed: ${reason}.`);
|
|
118
|
+
this.name = "PluginDiffUnavailableError";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Options that control which plugin to diff. */
|
|
123
|
+
export interface DiffPluginOptions {
|
|
124
|
+
/** Install name (kebab-case directory name). */
|
|
125
|
+
readonly name: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Dependencies injected by the caller. */
|
|
129
|
+
export interface DiffPluginDeps {
|
|
130
|
+
/** HTTP client used to fetch any curated adapter stub. Production callers pass `globalThis.fetch.bind(globalThis)`. */
|
|
131
|
+
readonly fetch: FetchLike;
|
|
132
|
+
/** Override the workspace plugins directory. Falls back to the live workspace. */
|
|
133
|
+
readonly workspacePluginsDir?: string;
|
|
134
|
+
/** Override the git runner used to clone the baseline. Forwarded to {@link materializePluginTree}. */
|
|
135
|
+
readonly runGit?: GitRunner;
|
|
136
|
+
/** Override the postinstall adapter runner. Forwarded to {@link materializePluginTree}. */
|
|
137
|
+
readonly runPostinstall?: PostinstallRunner;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A NUL byte in the leading bytes is the heuristic git uses to flag a blob as binary. */
|
|
141
|
+
function isBinary(buf: Buffer): boolean {
|
|
142
|
+
const len = Math.min(buf.length, 8000);
|
|
143
|
+
for (let i = 0; i < len; i++) {
|
|
144
|
+
if (buf[i] === 0) return true;
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface FileContent {
|
|
150
|
+
readonly text: string;
|
|
151
|
+
readonly binary: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readContent(absPath: string): FileContent {
|
|
155
|
+
const buf = readFileSync(absPath);
|
|
156
|
+
const binary = isBinary(buf);
|
|
157
|
+
return { text: binary ? "" : buf.toString("utf8"), binary };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function makeDiff(
|
|
161
|
+
path: string,
|
|
162
|
+
status: PluginFileDiffStatus,
|
|
163
|
+
before: FileContent | null,
|
|
164
|
+
after: FileContent | null,
|
|
165
|
+
reconstructed: boolean,
|
|
166
|
+
): PluginFileDiff {
|
|
167
|
+
if (!reconstructed) {
|
|
168
|
+
return {
|
|
169
|
+
path,
|
|
170
|
+
status,
|
|
171
|
+
diff: `Baseline unavailable (${status}): the install-time content of this file could not be reconstructed — the curated adapter overlay it was built from has changed since install. Reinstall with 'plugins install <name> --force' to refresh the baseline.`,
|
|
172
|
+
binary: false,
|
|
173
|
+
reconstructed: false,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const binary = (before?.binary ?? false) || (after?.binary ?? false);
|
|
177
|
+
if (binary) {
|
|
178
|
+
return {
|
|
179
|
+
path,
|
|
180
|
+
status,
|
|
181
|
+
diff: `Binary files differ (${status})`,
|
|
182
|
+
binary: true,
|
|
183
|
+
reconstructed: true,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
// `/dev/null` on the absent side and `a/`–`b/` prefixes mirror `git diff`, so
|
|
187
|
+
// the output is familiar and consumable by tools that parse unified diffs.
|
|
188
|
+
const oldName = status === "added" ? "/dev/null" : `a/${path}`;
|
|
189
|
+
const newName = status === "removed" ? "/dev/null" : `b/${path}`;
|
|
190
|
+
const diff = createTwoFilesPatch(
|
|
191
|
+
oldName,
|
|
192
|
+
newName,
|
|
193
|
+
before?.text ?? "",
|
|
194
|
+
after?.text ?? "",
|
|
195
|
+
undefined,
|
|
196
|
+
undefined,
|
|
197
|
+
{ context: 3 },
|
|
198
|
+
);
|
|
199
|
+
return { path, status, diff, binary: false, reconstructed: true };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Recover the install-time bytes of `path` from the re-materialized baseline
|
|
204
|
+
* tree, but only when they faithfully match what install produced: the
|
|
205
|
+
* re-materialized digest must equal the digest `recorded` at install. A
|
|
206
|
+
* mismatch (or a file the re-materialization did not produce) means the
|
|
207
|
+
* install-time content cannot be reconstructed — typically because the curated
|
|
208
|
+
* adapter overlay the file was built from changed since install — so `null` is
|
|
209
|
+
* returned and the caller flags the file instead of diffing fabricated bytes.
|
|
210
|
+
*/
|
|
211
|
+
function baselineContent(
|
|
212
|
+
path: string,
|
|
213
|
+
baselineRoot: string,
|
|
214
|
+
recorded: Fingerprint,
|
|
215
|
+
materialized: Fingerprint,
|
|
216
|
+
): FileContent | null {
|
|
217
|
+
if (materialized.files[path] !== recorded.files[path]) return null;
|
|
218
|
+
return readContent(join(baselineRoot, path));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Build a per-file unified diff for the on-disk install, classifying drift
|
|
223
|
+
* against the fingerprint `recorded` at install — the same baseline `inspect`
|
|
224
|
+
* uses, so the two surfaces always agree on which files changed and an adapter
|
|
225
|
+
* overlay that moved since install never reads as drift. The re-materialized
|
|
226
|
+
* tree supplies the baseline *bytes* for the diff, verified per file against
|
|
227
|
+
* `recorded`. The provenance sidecar is excluded on both sides — it never
|
|
228
|
+
* exists in the baseline and must not read as a local addition.
|
|
229
|
+
*/
|
|
230
|
+
function buildFileDiffs(
|
|
231
|
+
baselineRoot: string,
|
|
232
|
+
target: string,
|
|
233
|
+
recorded: Fingerprint,
|
|
234
|
+
): PluginFileDiff[] {
|
|
235
|
+
const comparison = compareFingerprint(target, recorded, [
|
|
236
|
+
INSTALL_META_FILENAME,
|
|
237
|
+
]);
|
|
238
|
+
const materialized = computeFingerprint(baselineRoot, [
|
|
239
|
+
INSTALL_META_FILENAME,
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
const files: PluginFileDiff[] = [];
|
|
243
|
+
for (const path of comparison.modified) {
|
|
244
|
+
const before = baselineContent(path, baselineRoot, recorded, materialized);
|
|
245
|
+
files.push(
|
|
246
|
+
makeDiff(
|
|
247
|
+
path,
|
|
248
|
+
"modified",
|
|
249
|
+
before,
|
|
250
|
+
readContent(join(target, path)),
|
|
251
|
+
before !== null,
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
for (const path of comparison.added) {
|
|
256
|
+
files.push(
|
|
257
|
+
makeDiff(path, "added", null, readContent(join(target, path)), true),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
for (const path of comparison.removed) {
|
|
261
|
+
const before = baselineContent(path, baselineRoot, recorded, materialized);
|
|
262
|
+
files.push(makeDiff(path, "removed", before, null, before !== null));
|
|
263
|
+
}
|
|
264
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
265
|
+
return files;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Resolve the unified diff of an installed plugin against its install-time
|
|
270
|
+
* baseline.
|
|
271
|
+
*
|
|
272
|
+
* Throws {@link PluginNotInstalledError} when no copy is installed,
|
|
273
|
+
* {@link PluginDiffUnavailableError} when the install recorded no commit to
|
|
274
|
+
* re-materialize, {@link PluginNotFoundError} when the recorded commit can no
|
|
275
|
+
* longer be fetched (e.g. the source repo or commit was removed), and
|
|
276
|
+
* propagates {@link materializePluginTree}'s errors (e.g. source unavailable)
|
|
277
|
+
* when the baseline clone itself fails.
|
|
278
|
+
*/
|
|
279
|
+
export async function diffPlugin(
|
|
280
|
+
opts: DiffPluginOptions,
|
|
281
|
+
deps: DiffPluginDeps,
|
|
282
|
+
): Promise<PluginDiffResult> {
|
|
283
|
+
const name = sanitizePluginName(opts.name);
|
|
284
|
+
const dir = deps.workspacePluginsDir ?? getWorkspacePluginsDir();
|
|
285
|
+
const target = join(dir, name);
|
|
286
|
+
|
|
287
|
+
if (!readInstalledPlugin(name, { workspacePluginsDir: dir })) {
|
|
288
|
+
throw new PluginNotInstalledError(name, target);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const meta = readInstallMeta(target);
|
|
292
|
+
const commit = meta?.commit ?? null;
|
|
293
|
+
if (!meta || !commit) {
|
|
294
|
+
throw new PluginDiffUnavailableError(
|
|
295
|
+
name,
|
|
296
|
+
`no install commit was recorded (an older or manually-copied install); reinstall with 'assistant plugins install ${name} --force' to record provenance`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
// Drift is classified against the install-time fingerprint (as `inspect`
|
|
300
|
+
// does), so without it there is no trustworthy baseline to diff against.
|
|
301
|
+
const recorded = meta.fingerprint;
|
|
302
|
+
if (!recorded) {
|
|
303
|
+
throw new PluginDiffUnavailableError(
|
|
304
|
+
name,
|
|
305
|
+
`no install-time fingerprint was recorded (an older or manually-copied install); reinstall with 'assistant plugins install ${name} --force' to record provenance`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const source: PluginFetchSource = {
|
|
310
|
+
owner: meta.source.owner,
|
|
311
|
+
repo: meta.source.repo,
|
|
312
|
+
rootPath: meta.source.path ?? "",
|
|
313
|
+
ref: commit,
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const baselineRoot = mkdtempSync(join(tmpdir(), `plugin-diff-${name}-`));
|
|
317
|
+
try {
|
|
318
|
+
const materialized = await materializePluginTree(
|
|
319
|
+
{ source, name, stubRef: DEFAULT_PLUGIN_REF, destDir: baselineRoot },
|
|
320
|
+
{
|
|
321
|
+
fetch: deps.fetch,
|
|
322
|
+
runGit: deps.runGit,
|
|
323
|
+
runPostinstall: deps.runPostinstall,
|
|
324
|
+
},
|
|
325
|
+
);
|
|
326
|
+
if (materialized.fileCount === 0) {
|
|
327
|
+
throw new PluginNotFoundError(
|
|
328
|
+
name,
|
|
329
|
+
commit,
|
|
330
|
+
`${source.owner}/${source.repo}`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const files = buildFileDiffs(baselineRoot, target, recorded);
|
|
335
|
+
return {
|
|
336
|
+
name,
|
|
337
|
+
target,
|
|
338
|
+
commit,
|
|
339
|
+
committedAt: meta.committedAt ?? materialized.committedAt ?? null,
|
|
340
|
+
clean: files.length === 0,
|
|
341
|
+
files,
|
|
342
|
+
};
|
|
343
|
+
} finally {
|
|
344
|
+
rmSync(baselineRoot, { recursive: true, force: true });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
@@ -221,7 +221,7 @@ function isTransientUpstreamStatus(res: Response): boolean {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
/** Resolved GitHub coordinates a plugin name is fetched from. */
|
|
224
|
-
interface PluginFetchSource {
|
|
224
|
+
export interface PluginFetchSource {
|
|
225
225
|
readonly owner: string;
|
|
226
226
|
readonly repo: string;
|
|
227
227
|
/** Repo-relative directory holding the plugin root; `""` = repo root. */
|
|
@@ -376,22 +376,13 @@ export async function installPlugin(
|
|
|
376
376
|
let commit: string | null = null;
|
|
377
377
|
let committedAt: string | null = null;
|
|
378
378
|
try {
|
|
379
|
-
const
|
|
380
|
-
source,
|
|
381
|
-
|
|
382
|
-
deps.runGit ?? defaultGitRunner,
|
|
379
|
+
const materialized = await materializePluginTree(
|
|
380
|
+
{ source, name, stubRef: marketplaceRef, destDir: stagingDir },
|
|
381
|
+
deps,
|
|
383
382
|
);
|
|
384
|
-
fileCount =
|
|
385
|
-
commit =
|
|
386
|
-
committedAt =
|
|
387
|
-
// An external clone is often a foreign-ecosystem plugin (e.g. a Claude
|
|
388
|
-
// Code plugin) that the Vellum loader can't run as-is. When we curate an
|
|
389
|
-
// adapter stub for it, overlay the stub and run its transform so the
|
|
390
|
-
// materialized tree is a valid Vellum plugin. Raw clones (no stub) are
|
|
391
|
-
// left untouched.
|
|
392
|
-
if (fileCount > 0) {
|
|
393
|
-
await applyAdapterStub(name, marketplaceRef, stagingDir, deps);
|
|
394
|
-
}
|
|
383
|
+
fileCount = materialized.fileCount;
|
|
384
|
+
commit = materialized.commit;
|
|
385
|
+
committedAt = materialized.committedAt;
|
|
395
386
|
} catch (err) {
|
|
396
387
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
397
388
|
throw err;
|
|
@@ -402,6 +393,52 @@ export async function installPlugin(
|
|
|
402
393
|
throw new PluginNotFoundError(name, ref, sourceLabel(source));
|
|
403
394
|
}
|
|
404
395
|
|
|
396
|
+
finalizeStagedInstall(stagingDir, {
|
|
397
|
+
name,
|
|
398
|
+
source,
|
|
399
|
+
ref,
|
|
400
|
+
commit,
|
|
401
|
+
committedAt,
|
|
402
|
+
pluginsDir,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
return { name, target, fileCount, ref, commit, committedAt };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Inputs for {@link finalizeStagedInstall}. */
|
|
409
|
+
export interface FinalizeStagedInstallParams {
|
|
410
|
+
readonly name: string;
|
|
411
|
+
/** Source coordinates recorded in the provenance sidecar. */
|
|
412
|
+
readonly source: PluginFetchSource;
|
|
413
|
+
/** Ref recorded in the sidecar (the resolved commit SHA for marketplace installs). */
|
|
414
|
+
readonly ref: string;
|
|
415
|
+
readonly commit: string | null;
|
|
416
|
+
/** ISO-8601 committer timestamp of {@link FinalizeStagedInstallParams.commit} (UTC); null when unknown. */
|
|
417
|
+
readonly committedAt: string | null;
|
|
418
|
+
/** Served plugins directory; the staging dir is swapped into `<pluginsDir>/<name>`. */
|
|
419
|
+
readonly pluginsDir: string;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Fingerprint a fully-populated `stagingDir`, write its provenance sidecar, and
|
|
424
|
+
* atomically swap it into `<pluginsDir>/<name>`. Returns the final install path
|
|
425
|
+
* and the fingerprint that was recorded.
|
|
426
|
+
*
|
|
427
|
+
* Shared by {@link installPlugin} (fresh materialization) and the merge-based
|
|
428
|
+
* `plugins upgrade --strategy` path so both record identical provenance and use
|
|
429
|
+
* the same atomic rm+rename swap.
|
|
430
|
+
*/
|
|
431
|
+
export function finalizeStagedInstall(
|
|
432
|
+
stagingDir: string,
|
|
433
|
+
{
|
|
434
|
+
name,
|
|
435
|
+
source,
|
|
436
|
+
ref,
|
|
437
|
+
commit,
|
|
438
|
+
committedAt,
|
|
439
|
+
pluginsDir,
|
|
440
|
+
}: FinalizeStagedInstallParams,
|
|
441
|
+
): { target: string; fingerprint: Fingerprint } {
|
|
405
442
|
// Hash the materialized tree before the sidecar is written (so the sidecar
|
|
406
443
|
// never hashes itself) — the baseline `plugins inspect` uses to detect later
|
|
407
444
|
// local edits. The per-file fingerprint answers "which files changed"; the
|
|
@@ -428,13 +465,14 @@ export async function installPlugin(
|
|
|
428
465
|
// rm and the rename — and at that point the staging dir is fully populated.
|
|
429
466
|
// Ensure the served `plugins/` directory exists: staging now lives outside
|
|
430
467
|
// it, so the target's parent is no longer created as a side effect.
|
|
468
|
+
const target = join(pluginsDir, name);
|
|
431
469
|
mkdirSync(pluginsDir, { recursive: true });
|
|
432
470
|
if (existsSync(target)) {
|
|
433
471
|
rmSync(target, { recursive: true, force: true });
|
|
434
472
|
}
|
|
435
473
|
renameSync(stagingDir, target);
|
|
436
474
|
|
|
437
|
-
return {
|
|
475
|
+
return { target, fingerprint };
|
|
438
476
|
}
|
|
439
477
|
|
|
440
478
|
/** Cap on any single git invocation; a shallow fetch is well under this. */
|
|
@@ -523,6 +561,56 @@ export interface InstallMeta {
|
|
|
523
561
|
readonly fingerprint: Fingerprint | null;
|
|
524
562
|
}
|
|
525
563
|
|
|
564
|
+
/** Outcome of materializing an external plugin tree into a directory. */
|
|
565
|
+
export interface MaterializedTree {
|
|
566
|
+
/** Number of regular files written into the destination. */
|
|
567
|
+
readonly fileCount: number;
|
|
568
|
+
/** Commit SHA the source was cloned at; `null` when it could not be read. */
|
|
569
|
+
readonly commit: string | null;
|
|
570
|
+
/** ISO-8601 committer timestamp of {@link MaterializedTree.commit}; `null` when unread. */
|
|
571
|
+
readonly committedAt: string | null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Produce the exact plugin tree an install stages, into `destDir`: clone the
|
|
576
|
+
* source at `source.ref`, then overlay the curated adapter stub (when one
|
|
577
|
+
* exists) so a foreign-ecosystem clone is translated into Vellum shape.
|
|
578
|
+
*
|
|
579
|
+
* `installPlugin` calls this and then fingerprints, records provenance, and
|
|
580
|
+
* swaps the result into the workspace. `diffPlugin` (see {@link ./diff-plugin})
|
|
581
|
+
* calls it with the *recorded install commit* to reconstruct the install-time
|
|
582
|
+
* baseline. Routing both through one path guarantees a re-materialized commit
|
|
583
|
+
* is byte-identical to what the original install produced, so an install-time
|
|
584
|
+
* adapter transform never reads as local drift when diffing.
|
|
585
|
+
*/
|
|
586
|
+
export async function materializePluginTree(
|
|
587
|
+
opts: {
|
|
588
|
+
/** Source coordinates; `source.ref` selects the commit to clone. */
|
|
589
|
+
readonly source: PluginFetchSource;
|
|
590
|
+
/** Install name, used to locate the curated adapter stub. */
|
|
591
|
+
readonly name: string;
|
|
592
|
+
/** Ref the curated adapter stub is fetched at (the canonical repo ref). */
|
|
593
|
+
readonly stubRef: string;
|
|
594
|
+
/** Directory the tree is written into. */
|
|
595
|
+
readonly destDir: string;
|
|
596
|
+
},
|
|
597
|
+
deps: InstallPluginDeps,
|
|
598
|
+
): Promise<MaterializedTree> {
|
|
599
|
+
const cloned = await copyExternalViaGit(
|
|
600
|
+
opts.source,
|
|
601
|
+
opts.destDir,
|
|
602
|
+
deps.runGit ?? defaultGitRunner,
|
|
603
|
+
);
|
|
604
|
+
// An external clone is often a foreign-ecosystem plugin (e.g. a Claude Code
|
|
605
|
+
// plugin) that the Vellum loader can't run as-is. When we curate an adapter
|
|
606
|
+
// stub for it, overlay the stub and run its transform so the materialized
|
|
607
|
+
// tree is a valid Vellum plugin. Raw clones (no stub) are left untouched.
|
|
608
|
+
if (cloned.fileCount > 0) {
|
|
609
|
+
await applyAdapterStub(opts.name, opts.stubRef, opts.destDir, deps);
|
|
610
|
+
}
|
|
611
|
+
return cloned;
|
|
612
|
+
}
|
|
613
|
+
|
|
526
614
|
/**
|
|
527
615
|
* Materialize an external plugin by shallow-cloning its repo at the pinned ref.
|
|
528
616
|
*
|