@vellumai/assistant 0.8.11 → 0.8.12-staging.2

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.
Files changed (244) hide show
  1. package/ARCHITECTURE.md +15 -17
  2. package/README.md +0 -6
  3. package/bun.lock +6 -122
  4. package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
  5. package/node_modules/@vellumai/gateway-client/package.json +3 -1
  6. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
  7. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
  9. package/openapi.yaml +633 -4
  10. package/package.json +1 -3
  11. package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
  12. package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
  13. package/src/__tests__/anthropic-provider.test.ts +129 -0
  14. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
  15. package/src/__tests__/btw-routes.test.ts +7 -34
  16. package/src/__tests__/checker.test.ts +6 -12
  17. package/src/__tests__/config-loader-backfill.test.ts +4 -2
  18. package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
  19. package/src/__tests__/config-watcher.test.ts +2 -2
  20. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  21. package/src/__tests__/conversation-error.test.ts +2 -6
  22. package/src/__tests__/conversation-history-web-search.test.ts +8 -0
  23. package/src/__tests__/conversation-title-service.test.ts +2 -1
  24. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  25. package/src/__tests__/disk-pressure-tools.test.ts +1 -1
  26. package/src/__tests__/exploration-drift-hook.test.ts +692 -0
  27. package/src/__tests__/filing-service.test.ts +8 -3
  28. package/src/__tests__/guardian-action-store.test.ts +0 -167
  29. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
  30. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
  31. package/src/__tests__/heartbeat-service.test.ts +5 -2
  32. package/src/__tests__/identity-intro-cache.test.ts +12 -5
  33. package/src/__tests__/identity-routes.test.ts +16 -57
  34. package/src/__tests__/injector-chain.test.ts +8 -3
  35. package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
  36. package/src/__tests__/llm-catalog-parity.test.ts +16 -0
  37. package/src/__tests__/llm-usage-store.test.ts +11 -0
  38. package/src/__tests__/log-export-workspace.test.ts +468 -3
  39. package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
  40. package/src/__tests__/model-intents.test.ts +1 -1
  41. package/src/__tests__/oauth-cli.test.ts +19 -8
  42. package/src/__tests__/openai-provider.test.ts +34 -0
  43. package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
  44. package/src/__tests__/recurrence-engine.test.ts +45 -0
  45. package/src/__tests__/schedule-routes.test.ts +34 -0
  46. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
  47. package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
  48. package/src/__tests__/secret-fixtures.ts +20 -0
  49. package/src/__tests__/skill-tool-factory.test.ts +49 -0
  50. package/src/__tests__/subagent-role-registry.test.ts +24 -1
  51. package/src/__tests__/subagent-tools.test.ts +1 -0
  52. package/src/__tests__/system-prompt.test.ts +109 -11
  53. package/src/__tests__/tool-approval-handler.test.ts +85 -0
  54. package/src/__tests__/tool-audit-listener.test.ts +86 -0
  55. package/src/__tests__/tool-error-hook.test.ts +1 -0
  56. package/src/__tests__/tool-result-spool.test.ts +337 -0
  57. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  58. package/src/__tests__/validate-input.test.ts +95 -1
  59. package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
  60. package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
  61. package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
  62. package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
  63. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
  64. package/src/acp/__tests__/agent-process.test.ts +315 -2
  65. package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
  66. package/src/acp/agent-process.ts +163 -34
  67. package/src/acp/prepare-agent-env.ts +55 -15
  68. package/src/agent/loop.ts +81 -24
  69. package/src/api/events/usage-progress.ts +28 -0
  70. package/src/api/index.ts +6 -0
  71. package/src/background-wake/wake-intent-hooks.test.ts +2 -0
  72. package/src/bundler/app-bundler.ts +25 -42
  73. package/src/bundler/app-compiler.ts +8 -0
  74. package/src/calls/call-controller.ts +1 -1
  75. package/src/cli/commands/plugins.ts +248 -15
  76. package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
  77. package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
  78. package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
  79. package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
  80. package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
  81. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +307 -0
  82. package/src/cli/lib/inspect-plugin.ts +252 -0
  83. package/src/cli/lib/install-from-github.ts +214 -21
  84. package/src/cli/lib/list-installed-plugins.ts +17 -6
  85. package/src/cli/lib/plugin-artifact.ts +103 -0
  86. package/src/cli/lib/plugin-details.ts +18 -1
  87. package/src/cli/lib/plugin-fingerprint.ts +197 -0
  88. package/src/cli/lib/upgrade-plugin.ts +225 -0
  89. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  90. package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
  91. package/src/config/call-site-defaults.ts +13 -2
  92. package/src/config/feature-flag-registry.json +8 -16
  93. package/src/config/loader.ts +52 -59
  94. package/src/config/schema.ts +0 -2
  95. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  96. package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
  97. package/src/config/schemas/llm.ts +10 -0
  98. package/src/config/schemas/memory-v2.ts +13 -0
  99. package/src/config/schemas/memory-v3.ts +92 -0
  100. package/src/config/seed-inference-profiles.ts +4 -8
  101. package/src/context/post-turn-tool-result-truncation.ts +32 -18
  102. package/src/context/tool-result-spool.ts +104 -0
  103. package/src/credential-execution/feature-gates.ts +0 -1
  104. package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
  105. package/src/daemon/conversation-error.ts +6 -15
  106. package/src/daemon/conversation.ts +9 -0
  107. package/src/daemon/disk-pressure-policy.ts +0 -1
  108. package/src/daemon/lifecycle.ts +1 -20
  109. package/src/daemon/message-types/conversations.ts +2 -15
  110. package/src/daemon/trust-context.ts +1 -1
  111. package/src/events/tool-audit-listener.ts +40 -9
  112. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  113. package/src/home/__tests__/home-content-refresh.test.ts +114 -0
  114. package/src/home/__tests__/suggested-prompts.test.ts +86 -5
  115. package/src/home/home-content-refresh.ts +43 -31
  116. package/src/home/home-greeting-cache.ts +8 -1
  117. package/src/home/home-greeting.ts +13 -9
  118. package/src/home/suggested-prompts.ts +77 -24
  119. package/src/ipc/routes/trust-rules.test.ts +66 -72
  120. package/src/media/image-credentials.ts +2 -2
  121. package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
  122. package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
  123. package/src/memory/conversation-attention-store.ts +1 -0
  124. package/src/memory/conversation-bootstrap.ts +18 -9
  125. package/src/memory/conversation-crud.ts +12 -2
  126. package/src/memory/conversation-title-service.ts +53 -9
  127. package/src/memory/delivery-channels.ts +0 -69
  128. package/src/memory/graph/extraction-job.ts +0 -15
  129. package/src/memory/guardian-action-store.ts +1 -376
  130. package/src/memory/llm-usage-store.ts +5 -1
  131. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
  132. package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
  133. package/src/memory/v2/__tests__/injection.test.ts +70 -0
  134. package/src/memory/v2/__tests__/static-context.test.ts +12 -0
  135. package/src/memory/v2/consolidation-job.ts +93 -9
  136. package/src/memory/v2/injection.ts +53 -0
  137. package/src/memory/v2/prompts/consolidation.ts +1 -0
  138. package/src/memory/v2/static-context.ts +13 -1
  139. package/src/memory/v2/sweep-job.ts +1 -1
  140. package/src/memory/v2/types.ts +5 -0
  141. package/src/plugin-api/types.ts +7 -0
  142. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
  143. package/src/plugins/defaults/exploration-drift/package.json +15 -0
  144. package/src/plugins/defaults/index.ts +25 -0
  145. package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
  146. package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
  147. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
  148. package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
  149. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  150. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
  151. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
  152. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
  153. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
  154. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
  155. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
  156. package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
  157. package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
  158. package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
  159. package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
  160. package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
  161. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
  162. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
  163. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
  164. package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
  165. package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
  166. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
  167. package/src/prompts/cache-boundary.ts +17 -0
  168. package/src/prompts/sections.ts +50 -17
  169. package/src/prompts/system-prompt.ts +12 -4
  170. package/src/prompts/templates/system-sections.ts +22 -0
  171. package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
  172. package/src/providers/anthropic/client.ts +74 -28
  173. package/src/providers/gemini/client.ts +5 -1
  174. package/src/providers/minimax/client.ts +9 -0
  175. package/src/providers/model-catalog.ts +28 -0
  176. package/src/providers/model-intents.ts +3 -3
  177. package/src/providers/openai/chat-completions-provider.ts +4 -2
  178. package/src/providers/openai/responses-provider.ts +7 -2
  179. package/src/providers/retry.ts +8 -0
  180. package/src/providers/types.ts +11 -0
  181. package/src/providers/unparseable-tool-args.ts +56 -0
  182. package/src/runtime/AGENTS.md +6 -0
  183. package/src/runtime/__tests__/agent-wake.test.ts +2 -2
  184. package/src/runtime/agent-wake.ts +5 -5
  185. package/src/runtime/background-job-runner.ts +2 -2
  186. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
  187. package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
  188. package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
  189. package/src/runtime/migrations/vbundle-importer.ts +9 -4
  190. package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
  191. package/src/runtime/pre-first-message-gate.ts +1 -1
  192. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
  193. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
  194. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
  195. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
  196. package/src/runtime/routes/__tests__/plugins-routes.test.ts +348 -0
  197. package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
  198. package/src/runtime/routes/btw-routes.ts +0 -14
  199. package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
  200. package/src/runtime/routes/conversation-list-routes.ts +77 -5
  201. package/src/runtime/routes/conversation-management-routes.ts +54 -0
  202. package/src/runtime/routes/conversation-query-routes.ts +79 -4
  203. package/src/runtime/routes/gateway-log-routes.ts +14 -64
  204. package/src/runtime/routes/home-feed-routes.ts +10 -0
  205. package/src/runtime/routes/identity-intro-cache.ts +1 -1
  206. package/src/runtime/routes/identity-routes.ts +76 -20
  207. package/src/runtime/routes/inbound-message-handler.ts +0 -36
  208. package/src/runtime/routes/log-export-routes.ts +143 -96
  209. package/src/runtime/routes/plugins-routes.ts +380 -0
  210. package/src/runtime/routes/redact-staged-export.ts +259 -0
  211. package/src/runtime/routes/schedule-routes.ts +19 -2
  212. package/src/runtime/routes/trust-rules-routes.ts +14 -67
  213. package/src/schedule/recurrence-engine.ts +34 -0
  214. package/src/schedule/scheduler.ts +1 -0
  215. package/src/security/redact-json.ts +61 -0
  216. package/src/skills/validate-input.ts +41 -1
  217. package/src/subagent/types.ts +26 -1
  218. package/src/telemetry/types.ts +15 -1
  219. package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
  220. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  221. package/src/tools/apps/executors.ts +1 -1
  222. package/src/tools/skills/skill-tool-factory.ts +19 -8
  223. package/src/tools/tool-approval-handler.ts +31 -0
  224. package/src/usage/types.ts +8 -1
  225. package/src/util/platform.ts +16 -0
  226. package/src/watcher/engine.ts +1 -0
  227. package/src/workspace/adaptive-thinking-repair.ts +113 -0
  228. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
  229. package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
  230. package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
  231. package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
  232. package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
  233. package/src/workspace/migrations/registry.ts +8 -0
  234. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
  235. package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
  236. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
  237. package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
  238. package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
  239. package/src/__tests__/update-bulletin-job.test.ts +0 -292
  240. package/src/config/schemas/updates.ts +0 -14
  241. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
  242. package/src/memory/conversation-starters-cadence.ts +0 -78
  243. package/src/prompts/update-bulletin-job.ts +0 -180
  244. package/src/runtime/guardian-action-followup-executor.ts +0 -306
@@ -6,12 +6,11 @@
6
6
  */
7
7
 
8
8
  import { createHash } from "node:crypto";
9
- import { createWriteStream, existsSync, readFileSync } from "node:fs";
9
+ import { existsSync, readFileSync } from "node:fs";
10
10
  import { readFile, stat, writeFile } from "node:fs/promises";
11
11
  import { tmpdir } from "node:os";
12
12
  import { join } from "node:path";
13
13
 
14
- import archiver from "archiver";
15
14
  import JSZip from "jszip";
16
15
 
17
16
  import { getApp, getAppDirPath, isMultifileApp } from "../memory/app-store.js";
@@ -37,6 +36,14 @@ export interface BundleResult {
37
36
  iconImageBase64?: string;
38
37
  }
39
38
 
39
+ function generateZipBuffer(zip: JSZip): Promise<Buffer> {
40
+ return zip.generateAsync({
41
+ type: "nodebuffer",
42
+ compression: "DEFLATE",
43
+ compressionOptions: { level: 9 },
44
+ });
45
+ }
46
+
40
47
  function isDefaultMainScaffold(source: string): boolean {
41
48
  const normalized = source.replace(/\s+/g, " ").trim();
42
49
  return (
@@ -177,54 +184,31 @@ export async function packageApp(
177
184
  const bundleFilename = `${safeName}-${uniqueSuffix}.vellum`;
178
185
  const bundlePath = join(tmpdir(), bundleFilename);
179
186
 
180
- await new Promise<void>((resolve, reject) => {
181
- const output = createWriteStream(bundlePath);
182
- const archive = archiver("zip", { zlib: { level: 9 } });
183
-
184
- output.on("close", () => resolve());
185
- output.on("error", (err: Error) => reject(err));
186
- archive.on("error", (err: Error) => reject(err));
187
- archive.on("warning", (err: Error) => {
188
- // Only reject on fatal warnings
189
- if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
190
- reject(err);
191
- }
192
- });
193
-
194
- archive.pipe(output);
187
+ const zip = new JSZip();
195
188
 
196
- // Add manifest.json at root level
197
- archive.append(serializeManifest(manifest), { name: "manifest.json" });
189
+ // Add manifest.json at root level
190
+ zip.file("manifest.json", serializeManifest(manifest));
198
191
 
199
- // Add compiled dist/ files
200
- for (const file of compiledFiles) {
201
- archive.append(file.data, { name: file.name });
202
- }
192
+ // Add compiled dist/ files
193
+ for (const file of compiledFiles) {
194
+ zip.file(file.name, file.data);
195
+ }
203
196
 
204
- // Include app icon if one was generated
205
- const iconPath = join(getAppDirPath(appId), "icon.png");
206
- if (existsSync(iconPath)) {
207
- archive.append(readFileSync(iconPath), { name: "icon.png" });
208
- }
197
+ // Include app icon if one was generated
198
+ const iconPath = join(appDir, "icon.png");
199
+ if (existsSync(iconPath)) {
200
+ zip.file("icon.png", readFileSync(iconPath));
201
+ }
209
202
 
210
- archive.finalize();
211
- });
203
+ await writeFile(bundlePath, await generateZipBuffer(zip));
212
204
 
213
205
  // Sign the bundle if a signing callback is provided
214
206
  if (requestSignature) {
215
207
  try {
216
208
  const signatureJson = await signBundle(bundlePath, requestSignature);
217
209
 
218
- // Re-open the zip and add signature.json
219
- const zipBuffer = await readFile(bundlePath);
220
- const zip = await JSZip.loadAsync(zipBuffer);
221
210
  zip.file("signature.json", JSON.stringify(signatureJson, null, 2));
222
- const signedBuffer = await zip.generateAsync({
223
- type: "nodebuffer",
224
- compression: "DEFLATE",
225
- compressionOptions: { level: 9 },
226
- });
227
- await writeFile(bundlePath, signedBuffer);
211
+ await writeFile(bundlePath, await generateZipBuffer(zip));
228
212
 
229
213
  bundlerLog.info({ appId }, "Bundle signed successfully");
230
214
  } catch (err) {
@@ -250,9 +234,8 @@ export async function packageApp(
250
234
 
251
235
  // Read icon for inclusion in the response
252
236
  let iconImageBase64: string | undefined;
253
- const iconFilePath = join(getAppDirPath(appId), "icon.png");
254
- if (existsSync(iconFilePath)) {
255
- iconImageBase64 = readFileSync(iconFilePath).toString("base64");
237
+ if (existsSync(iconPath)) {
238
+ iconImageBase64 = readFileSync(iconPath).toString("base64");
256
239
  }
257
240
 
258
241
  return { bundlePath, manifest, iconImageBase64 };
@@ -454,6 +454,14 @@ async function runCompile(appDir: string): Promise<CompileResult> {
454
454
  if (existsSync(htmlSrc)) {
455
455
  let html = await readFile(htmlSrc, "utf-8");
456
456
 
457
+ // Strip source-file script tags (e.g. <script src="/src/main.tsx">) that
458
+ // models often write in Vite style; browsers cannot load raw TSX/TS/JSX.
459
+ // The compiled main.js tag is injected below instead.
460
+ html = html.replace(
461
+ /<script\b[^>]*\bsrc=["'][^"']*\.(?:tsx|ts|jsx)["'][^>]*>\s*<\/script>\s*/gi,
462
+ "",
463
+ );
464
+
457
465
  // Check if CSS output was produced
458
466
  const distFiles = await readdir(distDir);
459
467
  const hasCss = distFiles.some((f) => f.endsWith(".css"));
@@ -1312,7 +1312,7 @@ export class CallController {
1312
1312
 
1313
1313
  // Mark the linked guardian action request as timed out and
1314
1314
  // send expiry notices to guardian destinations. Deliveries
1315
- // must be captured before markTimedOutWithReason changes
1315
+ // must be captured before expiring the request changes
1316
1316
  // their status.
1317
1317
  const pendingActionRequest = getPendingCanonicalRequestByCallSessionId(
1318
1318
  this.callSessionId,
@@ -10,6 +10,11 @@
10
10
  import type { Command } from "commander";
11
11
 
12
12
  import { confirmPrompt } from "../lib/confirm-prompt.js";
13
+ import {
14
+ inspectPlugin,
15
+ type PluginInspection,
16
+ PluginInspectNotFoundError,
17
+ } from "../lib/inspect-plugin.js";
13
18
  import {
14
19
  DEFAULT_PLUGIN_REF,
15
20
  installPlugin,
@@ -18,6 +23,7 @@ import {
18
23
  PluginNotFoundError,
19
24
  } from "../lib/install-from-github.js";
20
25
  import { listInstalledPlugins } from "../lib/list-installed-plugins.js";
26
+ import type { FingerprintComparison } from "../lib/plugin-fingerprint.js";
21
27
  import { registerCommand } from "../lib/register-command.js";
22
28
  import {
23
29
  InvalidSearchPatternError,
@@ -27,6 +33,11 @@ import {
27
33
  PluginNotInstalledError,
28
34
  uninstallPlugin,
29
35
  } from "../lib/uninstall-plugin.js";
36
+ import {
37
+ PluginNotUpgradableError,
38
+ type PluginUpgradeResult,
39
+ upgradePlugin,
40
+ } from "../lib/upgrade-plugin.js";
30
41
  import { getCliLogger } from "../logger.js";
31
42
 
32
43
  const log = getCliLogger("plugins");
@@ -41,15 +52,19 @@ export function registerPluginsCommand(program: Command): void {
41
52
  "after",
42
53
  `
43
54
  Examples:
44
- $ assistant plugins install simple-memory
45
- $ assistant plugins install simple-memory --force
46
- $ assistant plugins install simple-memory --ref my-feature-branch
55
+ $ assistant plugins install example
56
+ $ assistant plugins install example --force
57
+ $ assistant plugins install example --ref my-feature-branch
47
58
  $ assistant plugins list
48
59
  $ assistant plugins list --json
49
- $ assistant plugins search memory
50
- $ assistant plugins search "^simple"
51
- $ assistant plugins search memory --json
52
- $ assistant plugins uninstall simple-memory`,
60
+ $ assistant plugins inspect example
61
+ $ assistant plugins inspect example --json
62
+ $ assistant plugins upgrade example
63
+ $ assistant plugins upgrade example --dry-run
64
+ $ assistant plugins search example
65
+ $ assistant plugins search "^example"
66
+ $ assistant plugins search example --json
67
+ $ assistant plugins uninstall example`,
53
68
  );
54
69
 
55
70
  plugins
@@ -147,6 +162,55 @@ Examples:
147
162
  );
148
163
  });
149
164
 
165
+ plugins
166
+ .command("inspect <name>")
167
+ .description(
168
+ "Show a plugin's local install metadata and the marketplace pin, and whether an update is available",
169
+ )
170
+ .option("--json", "Emit machine-readable JSON instead of a summary")
171
+ .action(async (name: string, opts: { json?: boolean }) => {
172
+ try {
173
+ const inspection = await inspectPlugin(
174
+ { name },
175
+ { fetch: globalThis.fetch.bind(globalThis) },
176
+ );
177
+
178
+ if (opts.json) {
179
+ process.stdout.write(JSON.stringify(inspection, null, 2) + "\n");
180
+ return;
181
+ }
182
+
183
+ // Logged after the JSON early-return: the CLI logger writes
184
+ // info to stdout, which would otherwise corrupt --json output.
185
+ log.info(
186
+ {
187
+ name: inspection.name,
188
+ installed: inspection.installed,
189
+ status: inspection.status,
190
+ },
191
+ "plugin inspect",
192
+ );
193
+
194
+ for (const line of formatInspection(inspection)) {
195
+ console.log(line);
196
+ }
197
+ } catch (err) {
198
+ if (err instanceof PluginInspectNotFoundError) {
199
+ console.error(err.message);
200
+ process.exitCode = 1;
201
+ return;
202
+ }
203
+ if (err instanceof InvalidPluginNameError) {
204
+ console.error(err.message);
205
+ process.exitCode = 1;
206
+ return;
207
+ }
208
+ const message = err instanceof Error ? err.message : String(err);
209
+ console.error(`Plugin inspect failed: ${message}`);
210
+ process.exitCode = 1;
211
+ }
212
+ });
213
+
150
214
  plugins
151
215
  .command("search <query>")
152
216
  .description(
@@ -160,9 +224,13 @@ Examples:
160
224
  { fetch: globalThis.fetch.bind(globalThis) },
161
225
  );
162
226
 
163
- // Log on every success path — JSON output, empty results, and
164
- // populated tables alike — so observability doesn't depend on
165
- // which formatting branch the caller landed in.
227
+ if (opts.json) {
228
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
229
+ return;
230
+ }
231
+
232
+ // Logged after the JSON early-return: the CLI logger writes info
233
+ // to stdout, which would otherwise corrupt --json output.
166
234
  log.info(
167
235
  {
168
236
  query: result.query,
@@ -172,11 +240,6 @@ Examples:
172
240
  "external plugin search",
173
241
  );
174
242
 
175
- if (opts.json) {
176
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
177
- return;
178
- }
179
-
180
243
  if (result.matches.length === 0) {
181
244
  console.log(`No plugins matched "${result.query}".`);
182
245
  return;
@@ -253,6 +316,176 @@ Examples:
253
316
  process.exitCode = 1;
254
317
  }
255
318
  });
319
+
320
+ plugins
321
+ .command("upgrade <name>")
322
+ .description(
323
+ "Upgrade an installed plugin to the marketplace's current pin",
324
+ )
325
+ .option(
326
+ "--dry-run",
327
+ "Show what would change without modifying the install",
328
+ )
329
+ .option("--json", "Emit machine-readable JSON instead of a summary")
330
+ .action(
331
+ async (name: string, opts: { dryRun?: boolean; json?: boolean }) => {
332
+ try {
333
+ const result = await upgradePlugin(
334
+ { name, dryRun: opts.dryRun },
335
+ { fetch: globalThis.fetch.bind(globalThis) },
336
+ );
337
+
338
+ if (opts.json) {
339
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
340
+ return;
341
+ }
342
+
343
+ // Logged after the JSON early-return: the CLI logger writes
344
+ // info to stdout, which would otherwise corrupt --json output.
345
+ log.info(
346
+ {
347
+ name: result.name,
348
+ outcome: result.outcome,
349
+ from: result.fromCommit,
350
+ to: result.toCommit,
351
+ },
352
+ "plugin upgrade",
353
+ );
354
+
355
+ for (const line of formatUpgrade(result)) {
356
+ console.log(line);
357
+ }
358
+ } catch (err) {
359
+ if (
360
+ err instanceof PluginNotInstalledError ||
361
+ err instanceof PluginNotUpgradableError ||
362
+ err instanceof InvalidPluginNameError
363
+ ) {
364
+ console.error(err.message);
365
+ process.exitCode = 1;
366
+ return;
367
+ }
368
+ const message = err instanceof Error ? err.message : String(err);
369
+ console.error(`Plugin upgrade failed: ${message}`);
370
+ process.exitCode = 1;
371
+ }
372
+ },
373
+ );
256
374
  },
257
375
  });
258
376
  }
377
+
378
+ /** Abbreviate a commit SHA for display; passes through non-SHA / null values. */
379
+ function shortSha(sha: string | null): string {
380
+ return sha ? sha.slice(0, 7) : "—";
381
+ }
382
+
383
+ /** Human-readable status line for an inspection result. */
384
+ function statusLine(inspection: PluginInspection): string {
385
+ const { status, local, remote } = inspection;
386
+ switch (status) {
387
+ case "up-to-date":
388
+ return "up to date";
389
+ case "update-available":
390
+ return `update available (${shortSha(local?.commit ?? null)} → ${shortSha(
391
+ remote?.commit ?? null,
392
+ )})`;
393
+ case "not-installed":
394
+ return "not installed";
395
+ case "not-in-marketplace":
396
+ return "installed (not in marketplace)";
397
+ case "unknown-provenance":
398
+ return "unknown — reinstall to record provenance";
399
+ case "remote-unavailable":
400
+ return "installed (marketplace unavailable)";
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Summarize local edits relative to the install-time fingerprint. Reports
406
+ * "unknown" when no baseline was recorded (an older or manually-copied
407
+ * install), "none" when the on-disk tree matches, or a per-category count.
408
+ */
409
+ function localEditsLine(changes: FingerprintComparison | null): string {
410
+ if (!changes)
411
+ return "unknown (no recorded baseline; reinstall to record one)";
412
+ if (changes.clean) return "none";
413
+ const parts = [
414
+ `${changes.modified.length} modified`,
415
+ `${changes.added.length} added`,
416
+ `${changes.removed.length} removed`,
417
+ ];
418
+ return parts.join(", ");
419
+ }
420
+
421
+ /** Render an inspection as aligned, human-readable summary lines. */
422
+ function formatInspection(inspection: PluginInspection): string[] {
423
+ const { name, local, remote, remoteError } = inspection;
424
+ const lines: string[] = [`${name} plugin`];
425
+ const row = (label: string, value: string) =>
426
+ lines.push(` ${label.padEnd(11)} ${value}`);
427
+
428
+ row("status", statusLine(inspection));
429
+
430
+ if (local) {
431
+ const installedAt = local.installedAt
432
+ ? ` (${local.installedAt.slice(0, 10)})`
433
+ : "";
434
+ row("installed", `${shortSha(local.commit)}${installedAt}`);
435
+ row("local edits", localEditsLine(local.localChanges));
436
+ }
437
+
438
+ if (remote) {
439
+ const where = remote.path ? `${remote.repo}/${remote.path}` : remote.repo;
440
+ row(
441
+ "remote pin",
442
+ `${shortSha(remote.commit)} (${where}, ${remote.marketplaceRef})`,
443
+ );
444
+ } else if (remoteError) {
445
+ row("remote", `unavailable — ${remoteError}`);
446
+ }
447
+
448
+ const version = local?.version ?? null;
449
+ if (version) row("version", version);
450
+
451
+ const license = remote?.license ?? null;
452
+ const homepage = remote?.homepage ?? null;
453
+ if (license && homepage) row("license", `${license} homepage ${homepage}`);
454
+ else if (license) row("license", license);
455
+ else if (homepage) row("homepage", homepage);
456
+
457
+ const description = remote?.description ?? local?.description ?? null;
458
+ if (description) row("description", description);
459
+
460
+ for (const issue of local?.issues ?? []) row("issue", issue);
461
+
462
+ return lines;
463
+ }
464
+
465
+ /** Render an upgrade result as human-readable summary lines. */
466
+ function formatUpgrade(result: PluginUpgradeResult): string[] {
467
+ const { name, fromCommit, toCommit } = result;
468
+ const move = `${shortSha(fromCommit)} → ${shortSha(toCommit)}`;
469
+ const provenanceNote = result.provenanceWasUnknown
470
+ ? " (previous install had no recorded provenance)"
471
+ : "";
472
+
473
+ switch (result.outcome) {
474
+ case "already-up-to-date":
475
+ return [`"${name}" is already up to date (${shortSha(toCommit)}).`];
476
+ case "would-upgrade":
477
+ return [
478
+ `"${name}" would upgrade ${move}${provenanceNote} (dry run; no changes made).`,
479
+ ];
480
+ case "upgraded": {
481
+ const count =
482
+ result.fileCount === null
483
+ ? ""
484
+ : ` (${result.fileCount} file${result.fileCount === 1 ? "" : "s"})`;
485
+ return [
486
+ `Upgraded "${name}" ${move}${count}${provenanceNote} → ${result.target}`,
487
+ "Restart the assistant to pick up the upgrade.",
488
+ ];
489
+ }
490
+ }
491
+ }