@vellumai/assistant 0.12.2-staging.5 → 0.12.2-staging.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +7 -7
- package/docs/architecture/memory.md +11 -2
- package/docs/desktop-browser-cli.md +4 -2
- package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
- package/node_modules/@vellumai/environments/src/shell.ts +24 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
- package/openapi.yaml +157 -55
- package/package.json +5 -4
- package/scripts/bundled-plugin-packages.ts +154 -0
- package/scripts/generate-bundled-plugin-packages.ts +29 -0
- package/scripts/postinstall.ts +33 -0
- package/scripts/smoke-desktop-browser-cli.ts +1 -0
- package/scripts/test.ts +15 -13
- package/src/__tests__/agent-loop.test.ts +124 -0
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/channel-approval.test.ts +9 -14
- package/src/__tests__/conversation-agent-loop.test.ts +25 -0
- package/src/__tests__/db-conversation-tool-surface.test.ts +144 -0
- package/src/__tests__/managed-store.test.ts +121 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +88 -0
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/subagent-tool-gate-mode.test.ts +169 -0
- package/src/__tests__/terminal-tools.test.ts +8 -0
- package/src/__tests__/unicode.test.ts +36 -0
- package/src/agent/loop.ts +19 -0
- package/src/api/events/desktop-activity-changed.ts +10 -0
- package/src/api/index.ts +6 -0
- package/src/approvals/approval-primitive.ts +5 -2
- package/src/approvals/scoped-approval-grants.ts +6 -2
- package/src/calls/__tests__/voice-control-protocol.test.ts +24 -2
- package/src/calls/__tests__/voice-session-bridge.test.ts +22 -2
- package/src/calls/voice-control-protocol.ts +13 -4
- package/src/calls/voice-session-bridge.ts +27 -6
- package/src/cli/commands/__tests__/plugins.test.ts +66 -0
- package/src/cli/commands/plugins.ts +50 -18
- package/src/cli/lib/__tests__/install-from-github.test.ts +67 -0
- package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +169 -0
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +57 -0
- package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +14 -0
- package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +27 -1
- package/src/cli/lib/__tests__/plugin-details.test.ts +9 -2
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +20 -0
- package/src/cli/lib/__tests__/plugins-install-offline.test.ts +43 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +31 -0
- package/src/cli/lib/bundled-plugin-packages.json +4 -0
- package/src/cli/lib/bundled-plugin-packages.ts +87 -0
- package/src/cli/lib/diff-plugin.ts +1 -1
- package/src/cli/lib/inspect-plugin.ts +80 -12
- package/src/cli/lib/install-from-github.ts +133 -76
- package/src/cli/lib/plugin-catalog-cache.ts +22 -3
- package/src/cli/lib/plugin-catalog-local.ts +13 -3
- package/src/cli/lib/plugin-catalog-platform.ts +6 -1
- package/src/cli/lib/plugin-catalog-resolve.ts +20 -0
- package/src/cli/lib/plugin-details.ts +12 -0
- package/src/cli/lib/plugin-marketplace.ts +121 -21
- package/src/cli/lib/plugin-pin-history.ts +5 -2
- package/src/cli/lib/search-plugins.ts +58 -16
- package/src/cli/lib/upgrade-plugin.ts +48 -3
- package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
- package/src/config/bundled-skills/skill-management/TOOLS.json +6 -6
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
- package/src/daemon/conversation-agent-loop.ts +2 -0
- package/src/daemon/conversation-tool-setup.ts +61 -23
- package/src/daemon/conversation.ts +17 -0
- package/src/daemon/daemon-control.ts +2 -6
- package/src/daemon/orphan-reaper.ts +4 -3
- package/src/daemon/tool-setup-types.ts +6 -0
- package/src/daemon/wake-conversation-ops.ts +38 -15
- package/src/desktop/desktop-automation-lease.test.ts +143 -0
- package/src/desktop/desktop-automation-lease.ts +39 -3
- package/src/live-voice/__tests__/live-voice-vad.test.ts +624 -3
- package/src/live-voice/__tests__/session-controls.test.ts +18 -0
- package/src/live-voice/live-voice-session.ts +575 -53
- package/src/live-voice/session-controls.ts +7 -3
- package/src/messaging/provider-message-metadata.ts +3 -3
- package/src/monitoring/plugin-auto-update.ts +6 -0
- package/src/notifications/__tests__/copy-composer.test.ts +70 -0
- package/src/notifications/copy-composer.ts +11 -3
- package/src/persistence/conversation-plugin-facade.ts +13 -0
- package/src/persistence/conversation-tool-surface.ts +86 -0
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
- package/src/persistence/schema/conversation-tool-surfaces.ts +22 -0
- package/src/persistence/schema/index.ts +1 -0
- package/src/persistence/steps.ts +2 -0
- package/src/plugin-api/conversation-turn.ts +31 -7
- package/src/plugin-api/index.ts +9 -1
- package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
- package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
- package/src/plugins/defaults/memory/AGENTS.md +14 -2
- package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +320 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
- package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +46 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
- package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
- package/src/plugins/defaults/memory/buffer-file.ts +354 -0
- package/src/plugins/defaults/memory/buffer-format.ts +40 -0
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
- package/src/plugins/defaults/memory/context-search/format.ts +2 -1
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
- package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
- package/src/plugins/defaults/memory/host-utils.ts +0 -10
- package/src/plugins/defaults/memory/injectors.ts +4 -3
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +55 -181
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
- package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
- package/src/plugins/defaults/memory/tools.ts +1 -1
- package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +2 -1
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
- package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
- package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
- package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +21 -0
- package/src/plugins/defaults/memory/v3/card.ts +2 -1
- package/src/plugins/defaults/memory/v3/injector.ts +212 -178
- package/src/plugins/defaults/memory/v3/orchestrate.ts +86 -22
- package/src/plugins/defaults/memory/v3/pool-select.ts +10 -7
- package/src/plugins/defaults/memory/v3/sections.ts +2 -1
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +10 -1
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
- package/src/runtime/AGENTS.md +1 -1
- package/src/runtime/__tests__/agent-wake.test.ts +86 -1
- package/src/runtime/agent-wake.ts +20 -4
- package/src/runtime/guardian-action-service.ts +2 -17
- package/src/runtime/guardian-reply-router.ts +1 -8
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +102 -0
- package/src/runtime/routes/channel-route-shared.ts +1 -9
- package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
- package/src/runtime/routes/desktop-setup-routes.ts +7 -3
- package/src/runtime/routes/guardian-approval-interception.ts +24 -0
- package/src/runtime/routes/inbound-message-handler.ts +2 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
- package/src/runtime/routes/plugins-routes.ts +69 -49
- package/src/schedule/run-script.ts +2 -2
- package/src/skills/managed-store.ts +98 -32
- package/src/tools/host-terminal/host-shell.ts +12 -6
- package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
- package/src/tools/shared/shell-output.test.ts +10 -0
- package/src/tools/shared/shell-output.ts +14 -2
- package/src/tools/skills/find-similar-skills.test.ts +3 -0
- package/src/tools/skills/resolve-execute-invocation.ts +24 -0
- package/src/tools/skills/sandbox-runner.ts +13 -2
- package/src/tools/skills/scaffold-managed.ts +18 -14
- package/src/tools/terminal/__tests__/safe-env.test.ts +29 -0
- package/src/tools/terminal/safe-env.ts +30 -1
- package/src/tools/terminal/sanitized-bash.ts +15 -2
- package/src/tools/terminal/shell-launch.test.ts +162 -0
- package/src/tools/terminal/shell.test.ts +29 -0
- package/src/tools/terminal/shell.ts +13 -7
- package/src/util/host-process.test.ts +17 -1
- package/src/util/host-process.ts +24 -0
- package/src/util/unicode.ts +29 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Route handlers for the assistant plugins surface.
|
|
3
3
|
*
|
|
4
4
|
* GET /v1/plugins — list installed plugins under `<workspaceDir>/plugins/`.
|
|
5
|
-
* GET /v1/plugins/search
|
|
5
|
+
* GET /v1/plugins/search - search the reviewed catalog of installable plugins.
|
|
6
6
|
* GET /v1/plugins/:name — resolve a single plugin's detail view (metadata + README).
|
|
7
7
|
* POST /v1/plugins/install — install a plugin by name from the canonical source.
|
|
8
8
|
* DELETE /v1/plugins/:name — uninstall a plugin from `<workspaceDir>/plugins/<name>/`.
|
|
@@ -192,30 +192,47 @@ const pluginsListResponseSchema = z.object({
|
|
|
192
192
|
});
|
|
193
193
|
|
|
194
194
|
const pluginMatchSourceSchema = z
|
|
195
|
-
.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
195
|
+
.discriminatedUnion("kind", [
|
|
196
|
+
z.object({
|
|
197
|
+
kind: z.literal("github"),
|
|
198
|
+
repo: z
|
|
199
|
+
.string()
|
|
200
|
+
.describe("`owner/repo` of the external plugin repository."),
|
|
201
|
+
path: z
|
|
202
|
+
.string()
|
|
203
|
+
.optional()
|
|
204
|
+
.describe(
|
|
205
|
+
"Directory within the repo, when the plugin is not at the root.",
|
|
206
|
+
),
|
|
207
|
+
ref: z.string().describe("Pinned git ref the plugin is fetched from."),
|
|
208
|
+
}),
|
|
209
|
+
z.object({
|
|
210
|
+
kind: z.literal("local"),
|
|
211
|
+
path: z.string().describe("Package key in the assistant bundle."),
|
|
212
|
+
version: z.string().describe("Bundled package version."),
|
|
213
|
+
}),
|
|
214
|
+
])
|
|
215
|
+
.describe("Reviewed source of the marketplace entry.");
|
|
216
|
+
|
|
217
|
+
const mcpPluginIntegrationSchema = z.object({
|
|
218
|
+
kind: z.literal("mcp"),
|
|
219
|
+
displayName: z.string(),
|
|
220
|
+
documentationUrl: z.string(),
|
|
221
|
+
verifiedAt: z.string(),
|
|
222
|
+
verification: z.literal("documentation-only"),
|
|
223
|
+
setup: z.object({
|
|
224
|
+
mode: z.enum(["oauth", "manual"]),
|
|
225
|
+
instructions: z.string(),
|
|
226
|
+
}),
|
|
227
|
+
logo: z.string(),
|
|
228
|
+
oauthProvider: z.string().optional(),
|
|
229
|
+
});
|
|
209
230
|
|
|
210
231
|
const pluginSearchMatchSchema = z.object({
|
|
211
232
|
name: z
|
|
212
233
|
.string()
|
|
213
234
|
.describe("Install name. Matches `assistant plugins install <name>`."),
|
|
214
|
-
path: z
|
|
215
|
-
.string()
|
|
216
|
-
.describe(
|
|
217
|
-
"Human-readable origin: a `github:owner/repo@ref` locator for the external plugin.",
|
|
218
|
-
),
|
|
235
|
+
path: z.string().describe("Human-readable catalog origin locator."),
|
|
219
236
|
description: z
|
|
220
237
|
.string()
|
|
221
238
|
.optional()
|
|
@@ -232,6 +249,7 @@ const pluginSearchMatchSchema = z.object({
|
|
|
232
249
|
.describe(
|
|
233
250
|
"Marketplace category slug (Skills taxonomy); null when the entry declares none.",
|
|
234
251
|
),
|
|
252
|
+
integration: mcpPluginIntegrationSchema.optional(),
|
|
235
253
|
source: pluginMatchSourceSchema,
|
|
236
254
|
});
|
|
237
255
|
|
|
@@ -418,22 +436,20 @@ const fingerprintComparisonSchema = z
|
|
|
418
436
|
);
|
|
419
437
|
|
|
420
438
|
const installMetaSourceSchema = z
|
|
421
|
-
.
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
.string()
|
|
427
|
-
.
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
),
|
|
431
|
-
|
|
432
|
-
.string()
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
),
|
|
436
|
-
})
|
|
439
|
+
.discriminatedUnion("kind", [
|
|
440
|
+
z.object({
|
|
441
|
+
kind: z.literal("github"),
|
|
442
|
+
owner: z.string(),
|
|
443
|
+
repo: z.string(),
|
|
444
|
+
path: z.string().optional(),
|
|
445
|
+
ref: z.string(),
|
|
446
|
+
}),
|
|
447
|
+
z.object({
|
|
448
|
+
kind: z.literal("local"),
|
|
449
|
+
path: z.string(),
|
|
450
|
+
version: z.string(),
|
|
451
|
+
}),
|
|
452
|
+
])
|
|
437
453
|
.describe(
|
|
438
454
|
"Source coordinates recorded in the install-time provenance sidecar.",
|
|
439
455
|
);
|
|
@@ -489,6 +505,7 @@ const pluginLocalInfoSchema = z
|
|
|
489
505
|
|
|
490
506
|
const pluginRemoteInfoSchema = z
|
|
491
507
|
.object({
|
|
508
|
+
kind: z.literal("local").optional(),
|
|
492
509
|
repo: z
|
|
493
510
|
.string()
|
|
494
511
|
.describe("`owner/repo` of the external plugin repository."),
|
|
@@ -517,6 +534,7 @@ const pluginRemoteInfoSchema = z
|
|
|
517
534
|
.describe(
|
|
518
535
|
"Ref of the canonical repo the marketplace manifest was read from.",
|
|
519
536
|
),
|
|
537
|
+
version: z.string().optional().describe("Bundled package version."),
|
|
520
538
|
})
|
|
521
539
|
.describe("The marketplace's current pin and advertised metadata.");
|
|
522
540
|
|
|
@@ -870,7 +888,8 @@ interface PluginMatchView {
|
|
|
870
888
|
description?: string;
|
|
871
889
|
icon?: string;
|
|
872
890
|
category: string | null;
|
|
873
|
-
|
|
891
|
+
integration?: PluginSearchMatch["integration"];
|
|
892
|
+
source: PluginSearchMatch["source"];
|
|
874
893
|
}
|
|
875
894
|
|
|
876
895
|
/**
|
|
@@ -886,12 +905,7 @@ function projectMatch(
|
|
|
886
905
|
name: m.name,
|
|
887
906
|
path: m.path,
|
|
888
907
|
category: normalizeMarketplaceCategory(m.category, validSlugs),
|
|
889
|
-
source: {
|
|
890
|
-
kind: "github",
|
|
891
|
-
repo: m.source.repo,
|
|
892
|
-
ref: m.source.ref,
|
|
893
|
-
...(m.source.path !== undefined ? { path: m.source.path } : {}),
|
|
894
|
-
},
|
|
908
|
+
source: { ...m.source },
|
|
895
909
|
};
|
|
896
910
|
if (m.description !== undefined) {
|
|
897
911
|
view.description = m.description;
|
|
@@ -899,6 +913,9 @@ function projectMatch(
|
|
|
899
913
|
if (m.icon !== undefined) {
|
|
900
914
|
view.icon = m.icon;
|
|
901
915
|
}
|
|
916
|
+
if (m.integration !== undefined) {
|
|
917
|
+
view.integration = m.integration;
|
|
918
|
+
}
|
|
902
919
|
return view;
|
|
903
920
|
}
|
|
904
921
|
|
|
@@ -1276,12 +1293,15 @@ async function handleInstallPlugin({ body = {}, headers }: RouteHandlerArgs) {
|
|
|
1276
1293
|
{
|
|
1277
1294
|
name,
|
|
1278
1295
|
force,
|
|
1279
|
-
trustedSource:
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1296
|
+
trustedSource:
|
|
1297
|
+
source.kind === "local"
|
|
1298
|
+
? source
|
|
1299
|
+
: {
|
|
1300
|
+
owner: source.owner,
|
|
1301
|
+
repo: source.repo,
|
|
1302
|
+
rootPath: source.path,
|
|
1303
|
+
ref: source.ref,
|
|
1304
|
+
},
|
|
1285
1305
|
},
|
|
1286
1306
|
{ fetch: globalThis.fetch.bind(globalThis) },
|
|
1287
1307
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildSanitizedEnv } from "../tools/terminal/safe-env.js";
|
|
2
2
|
import {
|
|
3
3
|
buildShellInvocation,
|
|
4
|
+
buildShellSpawnFlags,
|
|
4
5
|
terminateProcessTree,
|
|
5
6
|
} from "../util/host-process.js";
|
|
6
7
|
import { getLogger } from "../util/logger.js";
|
|
@@ -64,10 +65,9 @@ export async function runScript(
|
|
|
64
65
|
const shell = buildShellInvocation(command);
|
|
65
66
|
const proc = Bun.spawn([shell.command, ...shell.args], {
|
|
66
67
|
cwd,
|
|
67
|
-
|
|
68
|
+
...buildShellSpawnFlags(),
|
|
68
69
|
stdout: "pipe",
|
|
69
70
|
stderr: "pipe",
|
|
70
|
-
windowsHide: true,
|
|
71
71
|
env: {
|
|
72
72
|
...buildSanitizedEnv(),
|
|
73
73
|
// __SCHEDULE_ID lets a saved command find its own dir; __SCHEDULE_RUN_ID
|
|
@@ -18,6 +18,7 @@ import { parseFrontmatter } from "../config/skills.js";
|
|
|
18
18
|
import { deleteSkillCapabilityNode } from "../plugins/defaults/memory/graph/capability-seed.js";
|
|
19
19
|
import { isDeniedBasename } from "../tools/shared/filesystem/path-policy.js";
|
|
20
20
|
import { getLogger } from "../util/logger.js";
|
|
21
|
+
import { isPlainObject } from "../util/object.js";
|
|
21
22
|
import { getWorkspaceDir, getWorkspaceSkillsDir } from "../util/platform.js";
|
|
22
23
|
import { parseFrontmatterFields } from "./frontmatter.js";
|
|
23
24
|
import { writeInstallMeta } from "./install-meta.js";
|
|
@@ -212,11 +213,48 @@ interface BuildSkillMarkdownInput {
|
|
|
212
213
|
name: string;
|
|
213
214
|
description: string;
|
|
214
215
|
bodyMarkdown: string;
|
|
216
|
+
/**
|
|
217
|
+
* The five fields the scaffold tool owns. `undefined` leaves whatever
|
|
218
|
+
* `preserve` carries for that field (nothing, on a create); an empty value
|
|
219
|
+
* clears it; a value sets it.
|
|
220
|
+
*/
|
|
215
221
|
emoji?: string;
|
|
216
222
|
includes?: string[];
|
|
217
223
|
activationHints?: string[];
|
|
218
224
|
avoidWhen?: string[];
|
|
219
225
|
category?: string;
|
|
226
|
+
/**
|
|
227
|
+
* The skill's existing frontmatter, as parsed from disk, for an overwrite.
|
|
228
|
+
* Every key survives except `name`, `description`, and the five fields
|
|
229
|
+
* above, so a `platforms` gate, a `display-name`, or custom metadata a
|
|
230
|
+
* person added by hand is not lost to a call that never mentions it.
|
|
231
|
+
*/
|
|
232
|
+
preserve?: Record<string, unknown>;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Apply one tool-owned field to the vellum block: an input left `undefined`
|
|
237
|
+
* keeps the preserved value, an input given but empty (`value` undefined)
|
|
238
|
+
* clears it, and a value sets it.
|
|
239
|
+
*/
|
|
240
|
+
function setOrClear(
|
|
241
|
+
target: Record<string, unknown>,
|
|
242
|
+
key: string,
|
|
243
|
+
value: unknown,
|
|
244
|
+
input: unknown,
|
|
245
|
+
): void {
|
|
246
|
+
if (input === undefined) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (value === undefined) {
|
|
250
|
+
delete target[key];
|
|
251
|
+
} else {
|
|
252
|
+
target[key] = value;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function nonEmptyList(list: string[] | undefined): string[] | undefined {
|
|
257
|
+
return list && list.length > 0 ? list : undefined;
|
|
220
258
|
}
|
|
221
259
|
|
|
222
260
|
export function buildSkillMarkdown(input: BuildSkillMarkdownInput): string {
|
|
@@ -230,40 +268,55 @@ export function buildSkillMarkdown(input: BuildSkillMarkdownInput): string {
|
|
|
230
268
|
lines.push(`name: "${esc(input.name)}"`);
|
|
231
269
|
lines.push(`description: "${esc(input.description)}"`);
|
|
232
270
|
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
const vellum: Record<string, unknown> = {};
|
|
238
|
-
if (input.emoji) {
|
|
239
|
-
vellum.emoji = input.emoji;
|
|
240
|
-
}
|
|
241
|
-
if (input.includes && input.includes.length > 0) {
|
|
242
|
-
vellum.includes = input.includes;
|
|
243
|
-
}
|
|
244
|
-
// Kebab-case keys match what parseFrontmatter reads back
|
|
245
|
-
// (config/skills.ts: vellum["activation-hints"] / vellum["avoid-when"]).
|
|
246
|
-
// These flow through stringifyYaml below, which escapes/quotes values, so no
|
|
271
|
+
// Everything but name and description is emitted from one object: the
|
|
272
|
+
// preserved frontmatter with the tool-owned fields applied over it, under
|
|
273
|
+
// `metadata.vellum` where parseFrontmatter reads them back (kebab-case for
|
|
274
|
+
// the two list fields). stringifyYaml quotes and escapes values, so no
|
|
247
275
|
// manual sanitization is needed here.
|
|
248
|
-
|
|
249
|
-
|
|
276
|
+
const rest: Record<string, unknown> = structuredClone(input.preserve ?? {});
|
|
277
|
+
delete rest.name;
|
|
278
|
+
delete rest.description;
|
|
279
|
+
const metadata = isPlainObject(rest.metadata) ? rest.metadata : {};
|
|
280
|
+
const vellum = isPlainObject(metadata.vellum) ? metadata.vellum : {};
|
|
281
|
+
setOrClear(vellum, "emoji", input.emoji?.trim() || undefined, input.emoji);
|
|
282
|
+
// An emoji at the legacy `metadata.emoji` location wins over an absent
|
|
283
|
+
// vellum one on read, so a call that states the emoji retires it.
|
|
284
|
+
if (input.emoji !== undefined) {
|
|
285
|
+
delete metadata.emoji;
|
|
250
286
|
}
|
|
251
|
-
|
|
252
|
-
|
|
287
|
+
setOrClear(vellum, "includes", nonEmptyList(input.includes), input.includes);
|
|
288
|
+
setOrClear(
|
|
289
|
+
vellum,
|
|
290
|
+
"activation-hints",
|
|
291
|
+
nonEmptyList(input.activationHints),
|
|
292
|
+
input.activationHints,
|
|
293
|
+
);
|
|
294
|
+
setOrClear(
|
|
295
|
+
vellum,
|
|
296
|
+
"avoid-when",
|
|
297
|
+
nonEmptyList(input.avoidWhen),
|
|
298
|
+
input.avoidWhen,
|
|
299
|
+
);
|
|
300
|
+
// The web Skills UI buckets skills by this value; a blank one is a clear,
|
|
301
|
+
// never an empty bucket in the file.
|
|
302
|
+
setOrClear(
|
|
303
|
+
vellum,
|
|
304
|
+
"category",
|
|
305
|
+
input.category?.trim() || undefined,
|
|
306
|
+
input.category,
|
|
307
|
+
);
|
|
308
|
+
if (Object.keys(vellum).length > 0) {
|
|
309
|
+
metadata.vellum = vellum;
|
|
310
|
+
} else {
|
|
311
|
+
delete metadata.vellum;
|
|
253
312
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
313
|
+
if (Object.keys(metadata).length > 0) {
|
|
314
|
+
rest.metadata = metadata;
|
|
315
|
+
} else {
|
|
316
|
+
delete rest.metadata;
|
|
258
317
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const metadata = { vellum };
|
|
262
|
-
const yamlBlock = stringifyYaml(metadata, { indent: 2 });
|
|
263
|
-
lines.push("metadata:");
|
|
264
|
-
for (const yamlLine of yamlBlock.trimEnd().split("\n")) {
|
|
265
|
-
lines.push(` ${yamlLine}`);
|
|
266
|
-
}
|
|
318
|
+
if (Object.keys(rest).length > 0) {
|
|
319
|
+
lines.push(stringifyYaml(rest, { indent: 2 }).trimEnd());
|
|
267
320
|
}
|
|
268
321
|
|
|
269
322
|
lines.push("---");
|
|
@@ -350,7 +403,8 @@ export function createManagedSkill(
|
|
|
350
403
|
const skillDir = getManagedSkillDir(params.id);
|
|
351
404
|
const skillFilePath = join(skillDir, "SKILL.md");
|
|
352
405
|
|
|
353
|
-
|
|
406
|
+
const skillExists = existsSync(skillFilePath);
|
|
407
|
+
if (skillExists && !params.overwrite) {
|
|
354
408
|
return {
|
|
355
409
|
created: false,
|
|
356
410
|
path: skillFilePath,
|
|
@@ -358,6 +412,14 @@ export function createManagedSkill(
|
|
|
358
412
|
};
|
|
359
413
|
}
|
|
360
414
|
|
|
415
|
+
// An overwrite replaces the body and patches the frontmatter: a field the
|
|
416
|
+
// call leaves undefined keeps its current value, an explicit empty value
|
|
417
|
+
// clears it, and frontmatter the tool does not own passes through. Callers
|
|
418
|
+
// rarely hold every field (the retrospective sees a skill through a
|
|
419
|
+
// similarity hit; a user edit is "change step 3"), so a field they do not
|
|
420
|
+
// pass is kept rather than dropped.
|
|
421
|
+
const existing = skillExists ? readStoredManagedSkill(params.id) : null;
|
|
422
|
+
|
|
361
423
|
// Resolve and validate every companion path before any write so an invalid
|
|
362
424
|
// path leaves no partial files behind.
|
|
363
425
|
const companionWrites: Array<{ resolvedPath: string; content: string }> = [];
|
|
@@ -413,6 +475,7 @@ export function createManagedSkill(
|
|
|
413
475
|
activationHints: params.activationHints,
|
|
414
476
|
avoidWhen: params.avoidWhen,
|
|
415
477
|
category: params.category,
|
|
478
|
+
preserve: existing?.frontmatter,
|
|
416
479
|
});
|
|
417
480
|
|
|
418
481
|
mkdirSync(skillDir, { recursive: true });
|
|
@@ -461,11 +524,13 @@ export function createManagedSkill(
|
|
|
461
524
|
* `{workspaceDir}` and strips feature-gated sections, and a caller that wrote
|
|
462
525
|
* that output back would bake absolute paths into the skill; and a first line
|
|
463
526
|
* that opens an indented code block must keep its indentation or a copy turns
|
|
464
|
-
* it into prose.
|
|
527
|
+
* it into prose. `frontmatter` is the whole block as written, for an
|
|
528
|
+
* overwrite to carry keys through that the typed fields do not cover.
|
|
465
529
|
*/
|
|
466
530
|
export interface StoredManagedSkill {
|
|
467
531
|
name: string;
|
|
468
532
|
description: string;
|
|
533
|
+
frontmatter: Record<string, unknown>;
|
|
469
534
|
emoji?: string;
|
|
470
535
|
includes?: string[];
|
|
471
536
|
activationHints?: string[];
|
|
@@ -493,6 +558,7 @@ export function readStoredManagedSkill(
|
|
|
493
558
|
return {
|
|
494
559
|
name: parsed.name,
|
|
495
560
|
description: parsed.description,
|
|
561
|
+
frontmatter: raw.fields,
|
|
496
562
|
emoji: parsed.emoji,
|
|
497
563
|
includes: parsed.includes,
|
|
498
564
|
activationHints: parsed.activationHints,
|
|
@@ -23,8 +23,10 @@ import { conversationRevealNonce } from "../../runtime/reveal-nonce.js";
|
|
|
23
23
|
import { redactSecrets } from "../../security/secret-scanner.js";
|
|
24
24
|
import {
|
|
25
25
|
buildShellInvocation,
|
|
26
|
+
buildShellSpawnFlags,
|
|
26
27
|
prependUniquePathEntries,
|
|
27
28
|
terminateProcessTree,
|
|
29
|
+
watchShellProcessStart,
|
|
28
30
|
} from "../../util/host-process.js";
|
|
29
31
|
import { getLogger } from "../../util/logger.js";
|
|
30
32
|
import type { CompletedBackgroundTool } from "../background-tool-registry.js";
|
|
@@ -455,9 +457,9 @@ export const hostShellTool = {
|
|
|
455
457
|
cwd: workingDir,
|
|
456
458
|
env: hostEnv,
|
|
457
459
|
stdio: ["ignore", "pipe", "pipe"],
|
|
458
|
-
|
|
459
|
-
windowsHide: true,
|
|
460
|
+
...buildShellSpawnFlags(),
|
|
460
461
|
});
|
|
462
|
+
const launch = watchShellProcessStart(child);
|
|
461
463
|
|
|
462
464
|
const collector = attachBoundedStdio(child);
|
|
463
465
|
let timedOut = false;
|
|
@@ -483,7 +485,9 @@ export const hostShellTool = {
|
|
|
483
485
|
}
|
|
484
486
|
completed = true;
|
|
485
487
|
clearTimeout(timer);
|
|
486
|
-
const result = collector.format(code, timedOut, timeoutSec
|
|
488
|
+
const result = collector.format(code, timedOut, timeoutSec, {
|
|
489
|
+
started: launch.didStart(),
|
|
490
|
+
});
|
|
487
491
|
// Cancel takes precedence over the SIGKILL-induced error result.
|
|
488
492
|
const status = aborted
|
|
489
493
|
? "cancelled"
|
|
@@ -629,9 +633,9 @@ export const hostShellTool = {
|
|
|
629
633
|
cwd: workingDir,
|
|
630
634
|
env: hostEnv,
|
|
631
635
|
stdio: ["ignore", "pipe", "pipe"],
|
|
632
|
-
|
|
633
|
-
windowsHide: true,
|
|
636
|
+
...buildShellSpawnFlags(),
|
|
634
637
|
});
|
|
638
|
+
const launch = watchShellProcessStart(child);
|
|
635
639
|
const collector = attachBoundedStdio(child, {
|
|
636
640
|
onOutput: context.onOutput,
|
|
637
641
|
});
|
|
@@ -657,7 +661,9 @@ export const hostShellTool = {
|
|
|
657
661
|
clearTimeout(timer);
|
|
658
662
|
context.signal?.removeEventListener("abort", onAbort);
|
|
659
663
|
|
|
660
|
-
const result = collector.format(code, timedOut, timeoutSec
|
|
664
|
+
const result = collector.format(code, timedOut, timeoutSec, {
|
|
665
|
+
started: launch.didStart(),
|
|
666
|
+
});
|
|
661
667
|
|
|
662
668
|
resolve({
|
|
663
669
|
content: result.content,
|
|
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { minimatch } from "minimatch";
|
|
5
5
|
|
|
6
6
|
import { ensureDir, pathExists } from "../../../util/fs.js";
|
|
7
|
+
import { surrogateSafeWindow } from "../../../util/unicode.js";
|
|
7
8
|
import { isAbortLikeError } from "../abort.js";
|
|
8
9
|
import { applyEdit } from "./edit-engine.js";
|
|
9
10
|
import * as Err from "./errors.js";
|
|
@@ -102,37 +103,6 @@ function truncationNotice(
|
|
|
102
103
|
return `\n\n[Truncated: characters ${start}-${end} of ${totalChars}. Read on with start_index=${end}.]`;
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
const isHighSurrogate = (code: number): boolean =>
|
|
106
|
-
code >= 0xd800 && code <= 0xdbff;
|
|
107
|
-
const isLowSurrogate = (code: number): boolean =>
|
|
108
|
-
code >= 0xdc00 && code <= 0xdfff;
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Character window that never splits a surrogate pair. A split leaves a lone
|
|
112
|
-
* half at each edge, and each encodes to U+FFFD, so the character is lost from
|
|
113
|
-
* both this window and the next one paged in after it.
|
|
114
|
-
*/
|
|
115
|
-
export function surrogateSafeWindow(
|
|
116
|
-
total: number,
|
|
117
|
-
charCodeAt: (index: number) => number,
|
|
118
|
-
requestedStart: number,
|
|
119
|
-
maxChars: number,
|
|
120
|
-
): { start: number; end: number } {
|
|
121
|
-
let start = Math.max(0, Math.min(requestedStart, total));
|
|
122
|
-
if (start > 0 && start < total && isLowSurrogate(charCodeAt(start))) {
|
|
123
|
-
start -= 1;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
let end = Math.min(total, start + maxChars);
|
|
127
|
-
if (end > start && end < total && isHighSurrogate(charCodeAt(end - 1))) {
|
|
128
|
-
// Backing off would empty a one-character window, which stalls paging on
|
|
129
|
-
// the same offset, so take the whole pair instead.
|
|
130
|
-
end = end - 1 > start ? end - 1 : Math.min(total, end + 1);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return { start, end };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
106
|
export class FileSystemOps {
|
|
137
107
|
private policy: PathPolicy;
|
|
138
108
|
private sizeLimit: number | undefined;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
formatShellOutput,
|
|
8
8
|
MAX_OUTPUT_LENGTH,
|
|
9
9
|
OUTPUT_TRUNCATED_TAG,
|
|
10
|
+
SHELL_DID_NOT_START_MESSAGE,
|
|
10
11
|
} from "./shell-output.js";
|
|
11
12
|
|
|
12
13
|
describe("BoundedStdioCollector", () => {
|
|
@@ -85,6 +86,15 @@ describe("attachBoundedStdio", () => {
|
|
|
85
86
|
});
|
|
86
87
|
});
|
|
87
88
|
|
|
89
|
+
describe("formatShellOutput launch start", () => {
|
|
90
|
+
test("does not report command_completed when the process never started", () => {
|
|
91
|
+
const result = formatShellOutput("", "", 0, false, 120, { started: false });
|
|
92
|
+
expect(result.isError).toBe(true);
|
|
93
|
+
expect(result.content).toBe(SHELL_DID_NOT_START_MESSAGE);
|
|
94
|
+
expect(result.content).not.toContain("<command_completed />");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
88
98
|
describe("formatShellOutput truncation", () => {
|
|
89
99
|
test("truncates an already-materialized oversized string without a file path", () => {
|
|
90
100
|
const longOutput = "x".repeat(30_000);
|
|
@@ -4,6 +4,9 @@ export const MAX_OUTPUT_LENGTH = 20_000;
|
|
|
4
4
|
|
|
5
5
|
export const OUTPUT_TRUNCATED_TAG = `<output_truncated limit="20K" />`;
|
|
6
6
|
|
|
7
|
+
export const SHELL_DID_NOT_START_MESSAGE =
|
|
8
|
+
"Error: the shell command did not start. No process was created, so the command did not run.";
|
|
9
|
+
|
|
7
10
|
export interface ShellOutputResult {
|
|
8
11
|
content: string;
|
|
9
12
|
status: string | undefined;
|
|
@@ -31,8 +34,16 @@ export function formatShellOutput(
|
|
|
31
34
|
code: number | null,
|
|
32
35
|
timedOut: boolean,
|
|
33
36
|
timeoutSec: number,
|
|
34
|
-
options?: { truncated?: boolean },
|
|
37
|
+
options?: { truncated?: boolean; started?: boolean },
|
|
35
38
|
): ShellOutputResult {
|
|
39
|
+
if (options?.started === false) {
|
|
40
|
+
return {
|
|
41
|
+
content: SHELL_DID_NOT_START_MESSAGE,
|
|
42
|
+
status: undefined,
|
|
43
|
+
isError: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
36
47
|
let output = stdout;
|
|
37
48
|
if (stderr) {
|
|
38
49
|
output += (output ? "\n" : "") + stderr;
|
|
@@ -137,6 +148,7 @@ export class BoundedStdioCollector {
|
|
|
137
148
|
code: number | null,
|
|
138
149
|
timedOut: boolean,
|
|
139
150
|
timeoutSec: number,
|
|
151
|
+
options?: { started?: boolean },
|
|
140
152
|
): ShellOutputResult {
|
|
141
153
|
return formatShellOutput(
|
|
142
154
|
Buffer.concat(this.stdoutParts).toString(),
|
|
@@ -144,7 +156,7 @@ export class BoundedStdioCollector {
|
|
|
144
156
|
code,
|
|
145
157
|
timedOut,
|
|
146
158
|
timeoutSec,
|
|
147
|
-
{ truncated: this.truncated },
|
|
159
|
+
{ truncated: this.truncated, started: options?.started },
|
|
148
160
|
);
|
|
149
161
|
}
|
|
150
162
|
}
|
|
@@ -257,6 +257,7 @@ describe("find_similar_skills: current skill for a refinable hit", () => {
|
|
|
257
257
|
"weekly-export": {
|
|
258
258
|
name: "Weekly Report Export",
|
|
259
259
|
description: "Export the weekly usage report",
|
|
260
|
+
frontmatter: {},
|
|
260
261
|
emoji: "📊",
|
|
261
262
|
category: "productivity",
|
|
262
263
|
includes: ["csv-basics"],
|
|
@@ -267,11 +268,13 @@ describe("find_similar_skills: current skill for a refinable hit", () => {
|
|
|
267
268
|
"user-skill": {
|
|
268
269
|
name: "User Skill",
|
|
269
270
|
description: "A person wrote this",
|
|
271
|
+
frontmatter: {},
|
|
270
272
|
body: "Do the user's thing.",
|
|
271
273
|
},
|
|
272
274
|
"clean-disk": {
|
|
273
275
|
name: "Clean Disk",
|
|
274
276
|
description: "Free up disk space",
|
|
277
|
+
frontmatter: {},
|
|
275
278
|
body: "Delete caches.",
|
|
276
279
|
},
|
|
277
280
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getTool } from "../registry.js";
|
|
2
|
+
import { resolveToolInvocationAlias } from "../tool-name-aliases.js";
|
|
3
|
+
import {
|
|
4
|
+
recoverSkillExecuteEnvelope,
|
|
5
|
+
resolveSkillExecuteInput,
|
|
6
|
+
} from "./execute.js";
|
|
7
|
+
|
|
8
|
+
export function resolveSkillExecuteInvocation(
|
|
9
|
+
input: Record<string, unknown>,
|
|
10
|
+
allowedToolNames?: ReadonlySet<string>,
|
|
11
|
+
): { name: string; input: Record<string, unknown> } {
|
|
12
|
+
const envelope = recoverSkillExecuteEnvelope(input);
|
|
13
|
+
const rawToolName = typeof envelope.tool === "string" ? envelope.tool : "";
|
|
14
|
+
const innerSchema = rawToolName
|
|
15
|
+
? getTool(rawToolName)?.input_schema
|
|
16
|
+
: undefined;
|
|
17
|
+
const rawToolInput = resolveSkillExecuteInput(envelope, innerSchema);
|
|
18
|
+
|
|
19
|
+
return resolveToolInvocationAlias(
|
|
20
|
+
rawToolName,
|
|
21
|
+
{ ...rawToolInput },
|
|
22
|
+
allowedToolNames,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -7,9 +7,12 @@ import { conversationRevealNonce } from "../../runtime/reveal-nonce.js";
|
|
|
7
7
|
import { computeSkillVersionHash } from "../../skills/version-hash.js";
|
|
8
8
|
import {
|
|
9
9
|
buildShellInvocation,
|
|
10
|
+
buildShellSpawnFlags,
|
|
10
11
|
terminateProcessTree,
|
|
12
|
+
watchShellProcessStart,
|
|
11
13
|
} from "../../util/host-process.js";
|
|
12
14
|
import { safeStringSlice } from "../../util/unicode.js";
|
|
15
|
+
import { SHELL_DID_NOT_START_MESSAGE } from "../shared/shell-output.js";
|
|
13
16
|
import { buildSanitizedEnv } from "../terminal/safe-env.js";
|
|
14
17
|
import type { ToolContext, ToolExecutionResult } from "../types.js";
|
|
15
18
|
|
|
@@ -160,9 +163,9 @@ function spawnRunner(
|
|
|
160
163
|
cwd: runDir,
|
|
161
164
|
env,
|
|
162
165
|
stdio: ["ignore", "pipe", "pipe"],
|
|
163
|
-
|
|
164
|
-
windowsHide: true,
|
|
166
|
+
...buildShellSpawnFlags(),
|
|
165
167
|
});
|
|
168
|
+
const launch = watchShellProcessStart(child);
|
|
166
169
|
|
|
167
170
|
const timer = setTimeout(() => {
|
|
168
171
|
timedOut = true;
|
|
@@ -188,6 +191,14 @@ function spawnRunner(
|
|
|
188
191
|
clearTimeout(timer);
|
|
189
192
|
context.signal?.removeEventListener("abort", onAbort);
|
|
190
193
|
|
|
194
|
+
if (!launch.didStart()) {
|
|
195
|
+
resolve({
|
|
196
|
+
content: `Failed to spawn skill tool script "${executorPath}": ${SHELL_DID_NOT_START_MESSAGE}`,
|
|
197
|
+
isError: true,
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
191
202
|
if (timedOut) {
|
|
192
203
|
resolve({
|
|
193
204
|
content: `Skill tool script "${executorPath}" timed out after ${timeoutMs}ms`,
|