wave-agent-sdk 1.1.5 → 1.3.0
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/dist/agent.d.ts +128 -28
- package/dist/agent.js +201 -49
- package/dist/builtin/index.js +2 -0
- package/dist/builtin/plugins.js +11 -20
- package/dist/builtin/skills/settings.js +7 -20
- package/dist/builtin/skills/wave-daemon.d.ts +1 -0
- package/dist/builtin/skills/wave-daemon.js +194 -0
- package/dist/constants/images.d.ts +26 -0
- package/dist/constants/images.js +26 -0
- package/dist/constants/index.d.ts +16 -0
- package/dist/constants/index.js +16 -0
- package/dist/constants/memory.d.ts +26 -0
- package/dist/constants/memory.js +34 -0
- package/dist/constants/messages.d.ts +11 -0
- package/dist/constants/messages.js +11 -0
- package/dist/constants/plugins.d.ts +8 -0
- package/dist/constants/plugins.js +8 -0
- package/dist/constants/tools.d.ts +1 -0
- package/dist/constants/tools.js +1 -0
- package/dist/core/plugin.d.ts +54 -10
- package/dist/core/plugin.js +137 -23
- package/dist/core/session.d.ts +1 -1
- package/dist/core/session.js +1 -1
- package/dist/exec/catalog.d.ts +140 -0
- package/dist/exec/catalog.js +470 -0
- package/dist/exec/catalogAnnouncement.d.ts +89 -0
- package/dist/exec/catalogAnnouncement.js +293 -0
- package/dist/exec/constants.d.ts +51 -0
- package/dist/exec/constants.js +51 -0
- package/dist/exec/execRuntime.d.ts +55 -0
- package/dist/exec/execRuntime.js +217 -0
- package/dist/exec/workerSource.d.ts +28 -0
- package/dist/exec/workerSource.js +299 -0
- package/dist/host/index.d.ts +23 -0
- package/dist/host/index.js +23 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +8 -1
- package/dist/managers/MemoryRuleManager.d.ts +6 -0
- package/dist/managers/MemoryRuleManager.js +12 -0
- package/dist/managers/aiManager.d.ts +35 -25
- package/dist/managers/aiManager.js +204 -202
- package/dist/managers/backgroundTaskManager.js +14 -0
- package/dist/managers/bashModeManager.d.ts +33 -0
- package/dist/managers/bashModeManager.js +110 -0
- package/dist/managers/hookManager.d.ts +18 -0
- package/dist/managers/hookManager.js +37 -3
- package/dist/managers/liveConfigManager.d.ts +33 -0
- package/dist/managers/liveConfigManager.js +106 -11
- package/dist/managers/lspManager.d.ts +9 -0
- package/dist/managers/lspManager.js +47 -18
- package/dist/managers/mcpManager.d.ts +68 -10
- package/dist/managers/mcpManager.js +265 -15
- package/dist/managers/messageManager.d.ts +60 -18
- package/dist/managers/messageManager.js +170 -81
- package/dist/managers/permissionManager.d.ts +69 -0
- package/dist/managers/permissionManager.js +221 -78
- package/dist/managers/planManager.d.ts +9 -0
- package/dist/managers/planManager.js +19 -1
- package/dist/managers/pluginManager.d.ts +46 -2
- package/dist/managers/pluginManager.js +117 -11
- package/dist/managers/pluginScopeManager.d.ts +15 -2
- package/dist/managers/pluginScopeManager.js +20 -1
- package/dist/managers/skillManager.d.ts +50 -0
- package/dist/managers/skillManager.js +166 -12
- package/dist/managers/slashCommandManager.d.ts +10 -0
- package/dist/managers/slashCommandManager.js +44 -31
- package/dist/managers/subagentManager.d.ts +15 -0
- package/dist/managers/subagentManager.js +81 -9
- package/dist/managers/toolManager.d.ts +29 -3
- package/dist/managers/toolManager.js +87 -13
- package/dist/managers/workflowManager.js +6 -0
- package/dist/prompts/autoMemory.d.ts +9 -0
- package/dist/prompts/autoMemory.js +30 -31
- package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
- package/dist/prompts/autoMemoryExtraction.js +8 -111
- package/dist/prompts/index.d.ts +0 -1
- package/dist/prompts/index.js +0 -4
- package/dist/prompts/memoryTypes.d.ts +63 -0
- package/dist/prompts/memoryTypes.js +191 -0
- package/dist/services/GitService.d.ts +7 -0
- package/dist/services/GitService.js +23 -0
- package/dist/services/MarketplaceService.d.ts +101 -17
- package/dist/services/MarketplaceService.js +323 -102
- package/dist/services/artifactContent.d.ts +84 -0
- package/dist/services/artifactContent.js +204 -0
- package/dist/services/artifactSession.d.ts +6 -0
- package/dist/services/artifactSession.js +17 -0
- package/dist/services/autoMemoryService.js +5 -13
- package/dist/services/configurationService.d.ts +92 -9
- package/dist/services/configurationService.js +246 -64
- package/dist/services/contentSummarizer.d.ts +15 -0
- package/dist/services/contentSummarizer.js +45 -0
- package/dist/services/execAvailability.d.ts +9 -0
- package/dist/services/execAvailability.js +32 -0
- package/dist/services/fileWatcher.js +61 -6
- package/dist/services/initializationService.js +21 -17
- package/dist/services/interactionService.d.ts +9 -1
- package/dist/services/interactionService.js +28 -8
- package/dist/services/jsonlHandler.d.ts +98 -0
- package/dist/services/jsonlHandler.js +250 -12
- package/dist/services/memory.d.ts +17 -1
- package/dist/services/memory.js +44 -7
- package/dist/services/officialMarketplaceMirror.d.ts +85 -0
- package/dist/services/officialMarketplaceMirror.js +290 -0
- package/dist/services/pluginLoader.d.ts +12 -4
- package/dist/services/pluginLoader.js +38 -7
- package/dist/services/remoteSettingsService.js +20 -6
- package/dist/services/session.d.ts +74 -0
- package/dist/services/session.js +174 -16
- package/dist/services/sessionEntries.d.ts +2 -0
- package/dist/services/sessionEntries.js +20 -0
- package/dist/services/worktreeHooks.js +6 -1
- package/dist/stdio/index.d.ts +12 -0
- package/dist/stdio/index.js +12 -0
- package/dist/stdio/notificationRouter.d.ts +38 -0
- package/dist/stdio/notificationRouter.js +97 -0
- package/dist/stdio/rpcClient.d.ts +18 -0
- package/dist/stdio/rpcClient.js +10 -0
- package/dist/stdio/stdioAgent.d.ts +229 -0
- package/dist/stdio/stdioAgent.js +360 -0
- package/dist/tools/artifactTool.js +406 -273
- package/dist/tools/bashTool.js +10 -6
- package/dist/tools/editTool.js +6 -3
- package/dist/tools/execTool.d.ts +2 -0
- package/dist/tools/execTool.js +165 -0
- package/dist/tools/exitPlanMode.js +10 -2
- package/dist/tools/grepTool.js +7 -1
- package/dist/tools/readTool.js +30 -2
- package/dist/tools/types.d.ts +34 -8
- package/dist/tools/webFetchTool.js +15 -166
- package/dist/tools/workflowTool.js +40 -8
- package/dist/tools/writeTool.js +6 -3
- package/dist/types/agent.d.ts +24 -1
- package/dist/types/commands.d.ts +7 -0
- package/dist/types/configuration.d.ts +45 -2
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/hooks.js +19 -0
- package/dist/types/marketplace.d.ts +40 -2
- package/dist/types/mcp.d.ts +42 -0
- package/dist/types/messaging.d.ts +1 -8
- package/dist/types/permissions.d.ts +22 -0
- package/dist/types/permissions.js +17 -0
- package/dist/types/plugins.d.ts +26 -2
- package/dist/types/skills.d.ts +26 -0
- package/dist/utils/bashParser.d.ts +17 -0
- package/dist/utils/bashParser.js +72 -0
- package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
- package/dist/utils/bashStructure/bashLexer.js +676 -0
- package/dist/utils/bashStructure/bashParser.d.ts +144 -0
- package/dist/utils/bashStructure/bashParser.js +606 -0
- package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
- package/dist/utils/bashStructure/bashSemantics.js +477 -0
- package/dist/utils/bashStructure/index.d.ts +26 -0
- package/dist/utils/bashStructure/index.js +27 -0
- package/dist/utils/bashStructure/types.d.ts +62 -0
- package/dist/utils/bashStructure/types.js +47 -0
- package/dist/utils/constants.d.ts +10 -0
- package/dist/utils/constants.js +10 -0
- package/dist/utils/containerSetup.js +48 -6
- package/dist/utils/convertMessagesForAPI.d.ts +7 -1
- package/dist/utils/convertMessagesForAPI.js +64 -14
- package/dist/utils/fileChangeReminder.d.ts +20 -0
- package/dist/utils/fileChangeReminder.js +153 -0
- package/dist/utils/fileSearch.js +4 -3
- package/dist/utils/fileUtils.d.ts +44 -0
- package/dist/utils/fileUtils.js +118 -0
- package/dist/utils/frontmatterYaml.d.ts +33 -0
- package/dist/utils/frontmatterYaml.js +192 -0
- package/dist/utils/imageBudget.d.ts +85 -0
- package/dist/utils/imageBudget.js +109 -0
- package/dist/utils/imageDimensions.d.ts +83 -0
- package/dist/utils/imageDimensions.js +232 -0
- package/dist/utils/imageProcessor.d.ts +66 -0
- package/dist/utils/imageProcessor.js +84 -0
- package/dist/utils/imageRewrite.d.ts +29 -0
- package/dist/utils/imageRewrite.js +251 -0
- package/dist/utils/markdownParser.d.ts +5 -1
- package/dist/utils/markdownParser.js +9 -51
- package/dist/utils/mcpInstructions.d.ts +61 -0
- package/dist/utils/mcpInstructions.js +126 -0
- package/dist/utils/mcpUtils.d.ts +7 -0
- package/dist/utils/mcpUtils.js +11 -2
- package/dist/utils/memoryAge.d.ts +32 -0
- package/dist/utils/memoryAge.js +47 -0
- package/dist/utils/memoryEntrypoint.d.ts +20 -0
- package/dist/utils/memoryEntrypoint.js +49 -0
- package/dist/utils/memoryIndex.d.ts +30 -0
- package/dist/utils/memoryIndex.js +76 -0
- package/dist/utils/messageOperations.d.ts +6 -20
- package/dist/utils/messageOperations.js +40 -91
- package/dist/utils/nestedMemory.d.ts +22 -0
- package/dist/utils/nestedMemory.js +61 -0
- package/dist/utils/npmTarball.d.ts +19 -0
- package/dist/utils/npmTarball.js +92 -0
- package/dist/utils/pluginSource.d.ts +37 -0
- package/dist/utils/pluginSource.js +73 -0
- package/dist/utils/ripgrep.d.ts +18 -4
- package/dist/utils/ripgrep.js +56 -4
- package/dist/utils/runtimeDeps.d.ts +35 -0
- package/dist/utils/runtimeDeps.js +426 -0
- package/dist/utils/skillParser.js +22 -52
- package/dist/utils/subagentParser.js +48 -45
- package/dist/utils/tokenCalculation.js +0 -8
- package/dist/utils/userSettings.d.ts +90 -0
- package/dist/utils/userSettings.js +291 -0
- package/dist/utils/worktreeUtils.d.ts +2 -1
- package/dist/utils/worktreeUtils.js +64 -34
- package/package.json +12 -4
- package/dist/managers/bangManager.d.ts +0 -26
- package/dist/managers/bangManager.js +0 -78
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builtin official marketplace zip-snapshot mirror fetch.
|
|
3
|
+
*
|
|
4
|
+
* The official marketplace (wave-plugins-official, BUILTIN in MarketplaceService)
|
|
5
|
+
* is hosted on GitHub, which is poorly reachable from CN networks. Instead of a
|
|
6
|
+
* dumb-HTTP git endpoint we mirror the "content-addressed zip + latest pointer +
|
|
7
|
+
* local sentinel" model Claude Code uses for its official marketplace
|
|
8
|
+
* (officialMarketplaceGcs.ts, inc-5046):
|
|
9
|
+
*
|
|
10
|
+
* 1. GET `{base}/latest` (10s timeout) → content-addressed sha
|
|
11
|
+
* 2. Compare against the local sentinel `.wave-market-sha` at the market root
|
|
12
|
+
* — equal ⇒ no-op (idempotent, one ~40B request per run)
|
|
13
|
+
* 3. GET `{base}/{sha}.zip` (60s timeout) → extract into a `.staging` dir
|
|
14
|
+
* (path-traversal/absolute-path rejection, size limits, exec-bit restore
|
|
15
|
+
* from zip external attrs)
|
|
16
|
+
* 4. Atomic swap: rm old market dir + rename staging → market dir, write the
|
|
17
|
+
* sentinel back
|
|
18
|
+
*
|
|
19
|
+
* Any failure returns null; the caller decides whether to fall through to the
|
|
20
|
+
* existing git path (guarded by a kill switch, mirroring Claude's
|
|
21
|
+
* tengu_plugin_official_mkt_git_fallback flag).
|
|
22
|
+
*/
|
|
23
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
24
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
25
|
+
import { unzipSync } from "fflate";
|
|
26
|
+
import { logWarn } from "../utils/globalLogger.js";
|
|
27
|
+
/** Sentinel file name stored at the market root holding the last fetched sha. */
|
|
28
|
+
export const OFFICIAL_MARKET_SENTINEL_FILE = ".wave-market-sha";
|
|
29
|
+
/** Suffix of the staging dir used for atomic swap into the market root. */
|
|
30
|
+
const STAGING_SUFFIX = ".staging";
|
|
31
|
+
/**
|
|
32
|
+
* Default mirror base URL (prod contract) — files hang directly off this base:
|
|
33
|
+
* `{base}/latest` (pure-text sha) and `{base}/{sha}.zip` (whole-tree snapshot).
|
|
34
|
+
* Trailing slash is normalized away when composing URLs.
|
|
35
|
+
*
|
|
36
|
+
* - PROD: `https://codechat.codewave.163.com/wave-plugins-official/`
|
|
37
|
+
* - TEST(验证用): `https://codechat.codewave-test.163yun.com/wave-plugins-official/`
|
|
38
|
+
*
|
|
39
|
+
* Override with env var `WAVE_OFFICIAL_MARKET_MIRROR_BASE_URL` (highest
|
|
40
|
+
* precedence). 两套环境的镜像通道均已上线且有内容(2026-09-23 实测:prod 与 test 的
|
|
41
|
+
* `/latest` 均返回 sha、`{sha}.zip` 均可下载)。镜像请求若失败(网络/超时/404/内容异常),
|
|
42
|
+
* 仍按既有设计回退 git 兜底(ALLOW_OFFICIAL_MARKET_GIT_FALLBACK),语义不变。
|
|
43
|
+
*/
|
|
44
|
+
const DEFAULT_OFFICIAL_MARKET_MIRROR_BASE_URL = "https://codechat.codewave.163.com/wave-plugins-official/";
|
|
45
|
+
/** Env var override for the mirror base URL (highest precedence). */
|
|
46
|
+
export const OFFICIAL_MARKET_MIRROR_BASE_URL_ENV = "WAVE_OFFICIAL_MARKET_MIRROR_BASE_URL";
|
|
47
|
+
/**
|
|
48
|
+
* Kill switch for the git fallback after a mirror failure (default: allowed).
|
|
49
|
+
* Mirrors Claude Code's `tengu_plugin_official_mkt_git_fallback` flag. When
|
|
50
|
+
* false and the mirror fails, the builtin marketplace update is skipped
|
|
51
|
+
* (retried on next run) instead of hitting GitHub.
|
|
52
|
+
*/
|
|
53
|
+
export const ALLOW_OFFICIAL_MARKET_GIT_FALLBACK = true;
|
|
54
|
+
const LATEST_TIMEOUT_MS = 10000;
|
|
55
|
+
const ZIP_TIMEOUT_MS = 60000;
|
|
56
|
+
// Sanity limits against corrupt/malicious archives (zip bombs). The publisher
|
|
57
|
+
// is first-party, so these are corruption guards rather than a trust boundary;
|
|
58
|
+
// values follow the Claude Code reference (utils/dxt/zip.ts).
|
|
59
|
+
const ZIP_LIMITS = {
|
|
60
|
+
MAX_SINGLE_FILE_SIZE: 512 * 1024 * 1024, // per-file uncompressed
|
|
61
|
+
MAX_TOTAL_SIZE: 1024 * 1024 * 1024, // total uncompressed
|
|
62
|
+
MAX_FILE_COUNT: 100000,
|
|
63
|
+
MAX_COMPRESSION_RATIO: 50, // uncompressed : compressed
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Resolves the mirror base URL: env var wins, otherwise the built-in constant.
|
|
67
|
+
* Returns null when neither is configured (mirror channel disabled).
|
|
68
|
+
*/
|
|
69
|
+
export function resolveOfficialMarketplaceMirrorBaseUrl() {
|
|
70
|
+
const envUrl = process.env[OFFICIAL_MARKET_MIRROR_BASE_URL_ENV];
|
|
71
|
+
const candidate = (envUrl && envUrl.trim()) || DEFAULT_OFFICIAL_MARKET_MIRROR_BASE_URL;
|
|
72
|
+
if (!candidate)
|
|
73
|
+
return null;
|
|
74
|
+
return candidate.replace(/\/+$/, ""); // normalize trailing slashes
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Rejects zip entry paths that would escape the extraction root: absolute
|
|
78
|
+
* paths (POSIX or Windows drive/UNC), `..` segments, and null bytes. Paths
|
|
79
|
+
* use either `/` or `\` as separator. Returns the normalized relative path on
|
|
80
|
+
* success or null for entries that must be skipped (empty/directory-only is
|
|
81
|
+
* handled by the caller via the trailing `/`).
|
|
82
|
+
*/
|
|
83
|
+
export function sanitizeZipEntryPath(entryPath) {
|
|
84
|
+
if (!entryPath || entryPath.includes("\0"))
|
|
85
|
+
return null;
|
|
86
|
+
if (entryPath.startsWith("/") || entryPath.startsWith("\\"))
|
|
87
|
+
return null;
|
|
88
|
+
const segments = entryPath.split(/[\\/]/);
|
|
89
|
+
// A drive-letter prefix (Windows "C:/…") must not slip through as relative.
|
|
90
|
+
if (segments[0] && /^[A-Za-z]:$/.test(segments[0]))
|
|
91
|
+
return null;
|
|
92
|
+
for (const seg of segments) {
|
|
93
|
+
if (seg === "..")
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return entryPath;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Fetches the official marketplace from the zip-snapshot mirror and extracts
|
|
100
|
+
* it to installLocation. Idempotent — compares the local `.wave-market-sha`
|
|
101
|
+
* sentinel before downloading.
|
|
102
|
+
*
|
|
103
|
+
* @param installLocation where to extract (must be inside marketplacesCacheDir)
|
|
104
|
+
* @param marketplacesCacheDir the marketplace cache root (defense-in-depth:
|
|
105
|
+
* this function `rm`s installLocation during the atomic swap)
|
|
106
|
+
* @param baseUrl optional explicit base URL (tests); defaults to env/constant
|
|
107
|
+
* @returns the fetched sha on success (including no-op), null on any failure
|
|
108
|
+
* (unconfigured mirror, network, 404, corrupt zip, unsafe entries). Caller
|
|
109
|
+
* decides whether to fall through to git.
|
|
110
|
+
*/
|
|
111
|
+
export async function fetchOfficialMarketplaceFromMirror(installLocation, marketplacesCacheDir, baseUrl) {
|
|
112
|
+
// Defense in depth: this function does `rm(installLocation, {recursive})`
|
|
113
|
+
// during the atomic swap. A corrupted known_marketplaces.json could point
|
|
114
|
+
// at the user's project. Refuse any path outside the marketplace cache dir.
|
|
115
|
+
// Same guard as Claude's officialMarketplaceGcs.ts:57-65.
|
|
116
|
+
const cacheDir = resolve(marketplacesCacheDir);
|
|
117
|
+
const resolvedLoc = resolve(installLocation);
|
|
118
|
+
if (resolvedLoc !== cacheDir && !resolvedLoc.startsWith(cacheDir + sep)) {
|
|
119
|
+
logWarn(`Official marketplace mirror: refusing install location outside marketplaces cache dir: ${installLocation}`);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const effectiveBase = baseUrl ?? resolveOfficialMarketplaceMirrorBaseUrl();
|
|
123
|
+
if (!effectiveBase)
|
|
124
|
+
return null; // mirror not configured
|
|
125
|
+
try {
|
|
126
|
+
// 1. Latest pointer — tiny request, hit on every startup.
|
|
127
|
+
const latestText = await httpGetText(`${effectiveBase}/latest`, LATEST_TIMEOUT_MS);
|
|
128
|
+
const sha = latestText.trim();
|
|
129
|
+
if (!sha) {
|
|
130
|
+
// Empty /latest body — mirror misconfigured. Bail (null), don't lock
|
|
131
|
+
// into a permanently-broken empty-sentinel state.
|
|
132
|
+
throw new Error("Official marketplace mirror: latest pointer returned empty body");
|
|
133
|
+
}
|
|
134
|
+
// sha is interpolated into a URL — keep it a sane opaque token.
|
|
135
|
+
if (!/^[^\s/\\]+$/.test(sha)) {
|
|
136
|
+
throw new Error(`Official marketplace mirror: invalid sha from latest pointer`);
|
|
137
|
+
}
|
|
138
|
+
// 2. Sentinel check.
|
|
139
|
+
const currentSha = await readFile(join(installLocation, OFFICIAL_MARKET_SENTINEL_FILE), "utf8")
|
|
140
|
+
.then((s) => s.trim())
|
|
141
|
+
.catch(() => null); // ENOENT — first fetch, proceed to download
|
|
142
|
+
if (currentSha === sha)
|
|
143
|
+
return sha;
|
|
144
|
+
// 3. Download zip and extract to a staging dir, then atomic-swap into
|
|
145
|
+
// place. A crash mid-extract leaves a .staging dir (rm'd on the next
|
|
146
|
+
// run) rather than a half-written installLocation.
|
|
147
|
+
const zipBuf = await httpGetBuffer(`${effectiveBase}/${sha}.zip`, ZIP_TIMEOUT_MS);
|
|
148
|
+
const zipState = {
|
|
149
|
+
fileCount: 0,
|
|
150
|
+
totalUncompressed: 0,
|
|
151
|
+
compressedSize: zipBuf.length,
|
|
152
|
+
};
|
|
153
|
+
const files = unzipSync(new Uint8Array(zipBuf), {
|
|
154
|
+
filter: (file) => {
|
|
155
|
+
validateZipEntry(file.name, file.originalSize, zipState);
|
|
156
|
+
return true;
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
// fflate doesn't surface external_attr, so parse the central directory
|
|
160
|
+
// ourselves to recover exec bits. Without this, hooks/scripts extract as
|
|
161
|
+
// 0644 and fail to execute — git clone preserves +x natively, so the zip
|
|
162
|
+
// path must too.
|
|
163
|
+
const modes = parseZipModes(zipBuf);
|
|
164
|
+
const staging = `${installLocation}${STAGING_SUFFIX}`;
|
|
165
|
+
await rm(staging, { recursive: true, force: true });
|
|
166
|
+
await mkdir(staging, { recursive: true });
|
|
167
|
+
for (const [entryName, data] of Object.entries(files)) {
|
|
168
|
+
if (!entryName || entryName.endsWith("/"))
|
|
169
|
+
continue; // dir entries
|
|
170
|
+
const rel = sanitizeZipEntryPath(entryName);
|
|
171
|
+
if (!rel)
|
|
172
|
+
continue; // validated in filter; skip defensively
|
|
173
|
+
const dest = join(staging, rel);
|
|
174
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
175
|
+
await writeFile(dest, data);
|
|
176
|
+
const mode = modes[entryName];
|
|
177
|
+
if (mode && mode & 0o111) {
|
|
178
|
+
// Only chmod when an exec bit is set — skip plain files to save
|
|
179
|
+
// syscalls. Swallow EPERM/ENOTSUP (read-only mounts) — losing +x is
|
|
180
|
+
// better than aborting mid-extraction.
|
|
181
|
+
await chmod(dest, mode & 0o777).catch(() => { });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
await writeFile(join(staging, OFFICIAL_MARKET_SENTINEL_FILE), sha);
|
|
185
|
+
// 4. Atomic swap: rm old, rename staging. Brief window where
|
|
186
|
+
// installLocation doesn't exist — acceptable for a background refresh
|
|
187
|
+
// (next run re-downloads if it crashes here).
|
|
188
|
+
await rm(installLocation, { recursive: true, force: true });
|
|
189
|
+
await rename(staging, installLocation);
|
|
190
|
+
return sha;
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
logWarn(`Official marketplace mirror fetch failed (falling back per caller policy): ${error instanceof Error ? error.message : String(error)}`);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Validates one zip entry (called from the fflate filter). Throws to abort the
|
|
199
|
+
* whole extraction when an entry is unsafe or the archive exceeds limits.
|
|
200
|
+
*/
|
|
201
|
+
export function validateZipEntry(name, originalSize, state) {
|
|
202
|
+
state.fileCount++;
|
|
203
|
+
if (state.fileCount > ZIP_LIMITS.MAX_FILE_COUNT) {
|
|
204
|
+
throw new Error(`Official marketplace zip contains too many files (${state.fileCount}, max ${ZIP_LIMITS.MAX_FILE_COUNT})`);
|
|
205
|
+
}
|
|
206
|
+
const clean = sanitizeZipEntryPath(name);
|
|
207
|
+
if (!clean) {
|
|
208
|
+
// Reject unsafe entries (path traversal, absolute paths, null bytes).
|
|
209
|
+
// Directory entries are validated too — a `../` dir entry aborts.
|
|
210
|
+
throw new Error(`Official marketplace zip contains unsafe path: "${name}"`);
|
|
211
|
+
}
|
|
212
|
+
if (originalSize > ZIP_LIMITS.MAX_SINGLE_FILE_SIZE) {
|
|
213
|
+
throw new Error(`Official marketplace zip entry "${name}" too large (${originalSize} bytes, max ${ZIP_LIMITS.MAX_SINGLE_FILE_SIZE})`);
|
|
214
|
+
}
|
|
215
|
+
state.totalUncompressed += originalSize;
|
|
216
|
+
if (state.totalUncompressed > ZIP_LIMITS.MAX_TOTAL_SIZE) {
|
|
217
|
+
throw new Error(`Official marketplace zip total size exceeds ${ZIP_LIMITS.MAX_TOTAL_SIZE} bytes`);
|
|
218
|
+
}
|
|
219
|
+
if (state.compressedSize > 0 &&
|
|
220
|
+
state.totalUncompressed / state.compressedSize >
|
|
221
|
+
ZIP_LIMITS.MAX_COMPRESSION_RATIO) {
|
|
222
|
+
throw new Error(`Official marketplace zip suspicious compression ratio (>${ZIP_LIMITS.MAX_COMPRESSION_RATIO}:1) — possible zip bomb`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function httpGetText(url, timeoutMs) {
|
|
226
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
throw new Error(`HTTP ${response.status} fetching ${url}`);
|
|
229
|
+
}
|
|
230
|
+
return response.text();
|
|
231
|
+
}
|
|
232
|
+
async function httpGetBuffer(url, timeoutMs) {
|
|
233
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
234
|
+
if (!response.ok) {
|
|
235
|
+
throw new Error(`HTTP ${response.status} fetching ${url}`);
|
|
236
|
+
}
|
|
237
|
+
return Buffer.from(await response.arrayBuffer());
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Parse Unix file modes from a zip's central directory.
|
|
241
|
+
*
|
|
242
|
+
* fflate's `unzipSync` returns only `Record<string, Uint8Array>` — it does not
|
|
243
|
+
* surface the external file attributes stored in the central directory, so
|
|
244
|
+
* executable bits would be lost (everything extracts as 0644). Returns
|
|
245
|
+
* `name → mode` for entries created on a Unix host (`versionMadeBy` high byte
|
|
246
|
+
* === 3); other entries are omitted and extract with default mode.
|
|
247
|
+
*
|
|
248
|
+
* Format per PKZIP APPNOTE.TXT §4.3.12 (central directory) and §4.3.16 (EOCD).
|
|
249
|
+
* ZIP64 is not handled — returns `{}` on archives >4GB or >65535 entries,
|
|
250
|
+
* which is fine for marketplace zips.
|
|
251
|
+
*/
|
|
252
|
+
export function parseZipModes(data) {
|
|
253
|
+
const buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
254
|
+
const modes = {};
|
|
255
|
+
// 1. Find the End of Central Directory record (sig 0x06054b50). Scan
|
|
256
|
+
// backwards from the trailing 22 + max-comment bytes.
|
|
257
|
+
const minEocd = Math.max(0, buf.length - 22 - 0xffff);
|
|
258
|
+
let eocd = -1;
|
|
259
|
+
for (let i = buf.length - 22; i >= minEocd; i--) {
|
|
260
|
+
if (buf.readUInt32LE(i) === 0x06054b50) {
|
|
261
|
+
eocd = i;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (eocd < 0)
|
|
266
|
+
return modes; // malformed — the unzip error surfaces separately
|
|
267
|
+
const entryCount = buf.readUInt16LE(eocd + 10);
|
|
268
|
+
let off = buf.readUInt32LE(eocd + 16);
|
|
269
|
+
// 2. Walk central directory entries (sig 0x02014b50). Each entry has a
|
|
270
|
+
// 46-byte fixed header followed by variable-length name/extra/comment.
|
|
271
|
+
for (let i = 0; i < entryCount; i++) {
|
|
272
|
+
if (off + 46 > buf.length || buf.readUInt32LE(off) !== 0x02014b50)
|
|
273
|
+
break;
|
|
274
|
+
const versionMadeBy = buf.readUInt16LE(off + 4);
|
|
275
|
+
const nameLen = buf.readUInt16LE(off + 28);
|
|
276
|
+
const extraLen = buf.readUInt16LE(off + 30);
|
|
277
|
+
const commentLen = buf.readUInt16LE(off + 32);
|
|
278
|
+
const externalAttr = buf.readUInt32LE(off + 38);
|
|
279
|
+
const name = buf.toString("utf8", off + 46, off + 46 + nameLen);
|
|
280
|
+
// versionMadeBy high byte = host OS. 3 = Unix. For Unix zips the high 16
|
|
281
|
+
// bits of externalAttr hold st_mode (file type + permission bits).
|
|
282
|
+
if (versionMadeBy >> 8 === 3) {
|
|
283
|
+
const mode = (externalAttr >>> 16) & 0xffff;
|
|
284
|
+
if (mode)
|
|
285
|
+
modes[name] = mode;
|
|
286
|
+
}
|
|
287
|
+
off += 46 + nameLen + extraLen + commentLen;
|
|
288
|
+
}
|
|
289
|
+
return modes;
|
|
290
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PluginManifest, CustomSlashCommand, Skill, LspConfig, McpConfig, PartialHookConfiguration } from "../types/index.js";
|
|
1
|
+
import { PluginManifest, CustomSlashCommand, Skill, LspConfig, McpConfig, McpServerConfig, PartialHookConfiguration } from "../types/index.js";
|
|
2
2
|
import { type SubagentConfiguration } from "../utils/subagentParser.js";
|
|
3
3
|
export declare class PluginLoader {
|
|
4
4
|
/**
|
|
@@ -21,9 +21,17 @@ export declare class PluginLoader {
|
|
|
21
21
|
*/
|
|
22
22
|
static loadLspConfig(pluginPath: string): Promise<LspConfig | undefined>;
|
|
23
23
|
/**
|
|
24
|
-
* Load MCP configuration from a plugin
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
* Load MCP configuration from a plugin.
|
|
25
|
+
*
|
|
26
|
+
* Accepts both declaration shapes found in the wild (spec plugin A-023): the
|
|
27
|
+
* wrapped `{"mcpServers": {…}}` and the flat `{"<serverName>": {…}}`, matching
|
|
28
|
+
* Claude Code's `parsed.mcpServers || parsed`. A plugin manifest that declares
|
|
29
|
+
* `mcpServers` inline is equivalent to a sibling `.mcp.json`; when both exist
|
|
30
|
+
* they are merged by server name, the file winning.
|
|
31
|
+
*
|
|
32
|
+
* @param manifestMcpServers Inline declarations from the plugin manifest
|
|
33
|
+
*/
|
|
34
|
+
static loadMcpConfig(pluginPath: string, manifestMcpServers?: Record<string, McpServerConfig>): Promise<McpConfig | undefined>;
|
|
27
35
|
/**
|
|
28
36
|
* Load hooks configuration from a plugin
|
|
29
37
|
*/
|
|
@@ -128,18 +128,33 @@ export class PluginLoader {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
/**
|
|
131
|
-
* Load MCP configuration from a plugin
|
|
131
|
+
* Load MCP configuration from a plugin.
|
|
132
|
+
*
|
|
133
|
+
* Accepts both declaration shapes found in the wild (spec plugin A-023): the
|
|
134
|
+
* wrapped `{"mcpServers": {…}}` and the flat `{"<serverName>": {…}}`, matching
|
|
135
|
+
* Claude Code's `parsed.mcpServers || parsed`. A plugin manifest that declares
|
|
136
|
+
* `mcpServers` inline is equivalent to a sibling `.mcp.json`; when both exist
|
|
137
|
+
* they are merged by server name, the file winning.
|
|
138
|
+
*
|
|
139
|
+
* @param manifestMcpServers Inline declarations from the plugin manifest
|
|
132
140
|
*/
|
|
133
|
-
static async loadMcpConfig(pluginPath) {
|
|
141
|
+
static async loadMcpConfig(pluginPath, manifestMcpServers) {
|
|
142
|
+
const servers = {
|
|
143
|
+
...(manifestMcpServers ?? {}),
|
|
144
|
+
};
|
|
134
145
|
const mcpPath = path.join(pluginPath, ".mcp.json");
|
|
135
146
|
try {
|
|
136
147
|
const content = await fs.readFile(mcpPath, "utf-8");
|
|
148
|
+
const parsed = JSON.parse(content);
|
|
137
149
|
// Return raw config — let McpManager resolve templates and capture originalUrl
|
|
138
|
-
|
|
150
|
+
Object.assign(servers, extractMcpServers(parsed));
|
|
139
151
|
}
|
|
140
152
|
catch {
|
|
141
|
-
|
|
153
|
+
// no .mcp.json — inline declarations (if any) stand alone
|
|
142
154
|
}
|
|
155
|
+
if (Object.keys(servers).length === 0)
|
|
156
|
+
return undefined;
|
|
157
|
+
return { mcpServers: servers };
|
|
143
158
|
}
|
|
144
159
|
/**
|
|
145
160
|
* Load hooks configuration from a plugin
|
|
@@ -197,11 +212,27 @@ export class PluginLoader {
|
|
|
197
212
|
if (!manifest.description) {
|
|
198
213
|
throw new Error("Plugin manifest missing 'description'");
|
|
199
214
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
215
|
+
// `version` is optional: the Claude Code ecosystem omits it freely and the
|
|
216
|
+
// install path already defaults it (spec plugin A-022).
|
|
203
217
|
if (!/^[a-z0-9-]+$/.test(manifest.name)) {
|
|
204
218
|
throw new Error(`Invalid plugin name: ${manifest.name}. Only lowercase letters, numbers, and hyphens are allowed.`);
|
|
205
219
|
}
|
|
206
220
|
}
|
|
207
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Normalizes an MCP declaration to a server map, accepting both the wrapped and
|
|
224
|
+
* the flat shape (spec plugin A-023). Anything that is not an object of server
|
|
225
|
+
* configs yields an empty map rather than throwing, so one malformed declaration
|
|
226
|
+
* cannot fail the whole plugin.
|
|
227
|
+
*/
|
|
228
|
+
function extractMcpServers(parsed) {
|
|
229
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
230
|
+
return {};
|
|
231
|
+
}
|
|
232
|
+
const record = parsed;
|
|
233
|
+
const wrapped = record.mcpServers;
|
|
234
|
+
const servers = wrapped && typeof wrapped === "object" && !Array.isArray(wrapped)
|
|
235
|
+
? wrapped
|
|
236
|
+
: record;
|
|
237
|
+
return servers;
|
|
238
|
+
}
|
|
@@ -268,8 +268,8 @@ export function mergeRemoteSettings(localMerged, remote) {
|
|
|
268
268
|
deny: lp.deny || rp.deny
|
|
269
269
|
? dedupe([...(lp.deny ?? []), ...(rp.deny ?? [])])
|
|
270
270
|
: undefined,
|
|
271
|
-
//
|
|
272
|
-
|
|
271
|
+
// defaultMode: remote wins (scalar)
|
|
272
|
+
defaultMode: rp.defaultMode ?? lp.defaultMode,
|
|
273
273
|
// additionalDirectories: concatenate + dedupe
|
|
274
274
|
additionalDirectories: lp.additionalDirectories || rp.additionalDirectories
|
|
275
275
|
? dedupe([
|
|
@@ -283,8 +283,8 @@ export function mergeRemoteSettings(localMerged, remote) {
|
|
|
283
283
|
delete result.permissions.allow;
|
|
284
284
|
if (!result.permissions.deny)
|
|
285
285
|
delete result.permissions.deny;
|
|
286
|
-
if (!result.permissions.
|
|
287
|
-
delete result.permissions.
|
|
286
|
+
if (!result.permissions.defaultMode)
|
|
287
|
+
delete result.permissions.defaultMode;
|
|
288
288
|
if (!result.permissions.additionalDirectories)
|
|
289
289
|
delete result.permissions.additionalDirectories;
|
|
290
290
|
}
|
|
@@ -303,12 +303,26 @@ export function mergeRemoteSettings(localMerged, remote) {
|
|
|
303
303
|
result.worktree = remote.worktree;
|
|
304
304
|
if (remote.models !== undefined)
|
|
305
305
|
result.models = remote.models;
|
|
306
|
+
// Plugin / marketplace maps merge per key (remote wins per key), like `env`
|
|
307
|
+
// above: the managed layer's keys override same-named local ones while every
|
|
308
|
+
// key the admin did not mention stays as the user configured it. A wholesale
|
|
309
|
+
// replace would silently drop all of a user's own plugins the moment an admin
|
|
310
|
+
// pushes a single managed one (spec enterprise server-managed-config
|
|
311
|
+
// 「托管配置下发插件市场与启用列表」场景 5, matching Claude Code's policy layer).
|
|
306
312
|
if (remote.marketplaces !== undefined)
|
|
307
|
-
result.marketplaces =
|
|
313
|
+
result.marketplaces = {
|
|
314
|
+
...localMerged.marketplaces,
|
|
315
|
+
...remote.marketplaces,
|
|
316
|
+
};
|
|
308
317
|
if (remote.enabledPlugins !== undefined)
|
|
309
|
-
result.enabledPlugins =
|
|
318
|
+
result.enabledPlugins = {
|
|
319
|
+
...localMerged.enabledPlugins,
|
|
320
|
+
...remote.enabledPlugins,
|
|
321
|
+
};
|
|
310
322
|
if (remote.enableArtifact !== undefined)
|
|
311
323
|
result.enableArtifact = remote.enableArtifact;
|
|
324
|
+
if (remote.enableExec !== undefined)
|
|
325
|
+
result.enableExec = remote.enableExec;
|
|
312
326
|
return result;
|
|
313
327
|
}
|
|
314
328
|
/**
|
|
@@ -22,6 +22,13 @@ export interface SessionData {
|
|
|
22
22
|
workdir: string;
|
|
23
23
|
lastActiveAt: string;
|
|
24
24
|
latestTotalTokens: number;
|
|
25
|
+
/**
|
|
26
|
+
* User-set title recovered from the whole-file scan (see
|
|
27
|
+
* `readMessagesAndCustomTitle`). Held in memory by the host so a later
|
|
28
|
+
* `reAppendSessionMetadata` can write it back at EOF even after later
|
|
29
|
+
* messages pushed it out of the tail window.
|
|
30
|
+
*/
|
|
31
|
+
customTitle?: string;
|
|
25
32
|
};
|
|
26
33
|
}
|
|
27
34
|
export interface SessionMetadata {
|
|
@@ -33,6 +40,11 @@ export interface SessionMetadata {
|
|
|
33
40
|
lastActiveAt: Date;
|
|
34
41
|
latestTotalTokens: number;
|
|
35
42
|
firstMessage?: string;
|
|
43
|
+
/**
|
|
44
|
+
* User-set conversation title (from a `custom-title` entry in the session
|
|
45
|
+
* file). Takes precedence over `firstMessage` when rendering a label.
|
|
46
|
+
*/
|
|
47
|
+
customTitle?: string;
|
|
36
48
|
/** Git branch at session creation time (from the metadata header). */
|
|
37
49
|
branch?: string;
|
|
38
50
|
}
|
|
@@ -75,6 +87,21 @@ export declare function getSessionFilePath(sessionId: string, workdir: string, s
|
|
|
75
87
|
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
76
88
|
*/
|
|
77
89
|
export declare function createSession(sessionId: string, workdir: string, sessionType?: "main" | "subagent"): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Rewrite a session file down to its first `keepMessageCount` messages.
|
|
92
|
+
*
|
|
93
|
+
* The only caller is `/rewind`. Unlike `appendMessages` this removes history
|
|
94
|
+
* from the file, but it keeps every file-level reserved entry (metadata header,
|
|
95
|
+
* `custom-title`) byte for byte — dropping them would silently turn the session
|
|
96
|
+
* into the "legacy file without a header" shape, losing its creation time,
|
|
97
|
+
* git branch and user-set title.
|
|
98
|
+
*
|
|
99
|
+
* @param sessionId - UUID session identifier
|
|
100
|
+
* @param keepMessageCount - Number of leading messages to keep
|
|
101
|
+
* @param workdir - Working directory for the session
|
|
102
|
+
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
103
|
+
*/
|
|
104
|
+
export declare function truncateSession(sessionId: string, keepMessageCount: number, workdir: string, sessionType?: "main" | "subagent"): Promise<void>;
|
|
78
105
|
/**
|
|
79
106
|
* Append messages to session using JSONL format (new approach)
|
|
80
107
|
*
|
|
@@ -84,6 +111,53 @@ export declare function createSession(sessionId: string, workdir: string, sessio
|
|
|
84
111
|
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
85
112
|
*/
|
|
86
113
|
export declare function appendMessages(sessionId: string, newMessages: Message[], workdir: string, sessionType?: "main" | "subagent"): Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* Set (or replace) a session's user-set title.
|
|
116
|
+
*
|
|
117
|
+
* The title is persisted as a `custom-title` entry appended to the session's
|
|
118
|
+
* own JSONL file — never as a rewrite of the metadata header, and never in a
|
|
119
|
+
* sidecar index — so every host that reads session files sees it without any
|
|
120
|
+
* extra sync channel.
|
|
121
|
+
*
|
|
122
|
+
* A session whose transcript has not been materialized yet (only meta messages
|
|
123
|
+
* so far) is created here: renaming is an explicit user action, so it is not
|
|
124
|
+
* subject to the "meta-only sessions never hit disk" rule.
|
|
125
|
+
*
|
|
126
|
+
* An empty/blank title is a no-op — it is not a signal to clear the title.
|
|
127
|
+
*
|
|
128
|
+
* @param sessionId - UUID session identifier
|
|
129
|
+
* @param workdir - Working directory the session belongs to
|
|
130
|
+
* @param title - New title
|
|
131
|
+
* @returns The trimmed title that was written, or undefined for a blank no-op
|
|
132
|
+
*/
|
|
133
|
+
export declare function setSessionCustomTitle(sessionId: string, workdir: string, title: string): Promise<string | undefined>;
|
|
134
|
+
/**
|
|
135
|
+
* Re-append the session's reserved metadata entries at EOF.
|
|
136
|
+
*
|
|
137
|
+
* A listing reads only the tail window (64 KB) of each session file, so a
|
|
138
|
+
* `custom-title` entry that later messages pushed past that window becomes
|
|
139
|
+
* invisible — the session would flip back to showing its first message. This
|
|
140
|
+
* runs at the session's sparse flush points (spec `session-management.md`
|
|
141
|
+
* 验收场景 5): graceful exit, compaction, taking over a resumed session,
|
|
142
|
+
* switching away from one, and after `/rewind` rewrites the file. Claude Code
|
|
143
|
+
* solves the same problem the same way (`reAppendSessionMetadata`,
|
|
144
|
+
* `sessionStorage.ts:458/983/1533`, `compact.ts:711/1057`).
|
|
145
|
+
*
|
|
146
|
+
* The entry is refreshed from the tail first: another process (the CLI session
|
|
147
|
+
* picker, a second host) may have renamed the session while we held it, in
|
|
148
|
+
* which case its value — not our stale in-memory one — is the truth. When the
|
|
149
|
+
* tail holds nothing (already evicted, or never written by anyone else), the
|
|
150
|
+
* caller's in-memory value stands.
|
|
151
|
+
*
|
|
152
|
+
* A missing transcript file is a no-op: re-appending must never materialize a
|
|
153
|
+
* session that does not exist.
|
|
154
|
+
*
|
|
155
|
+
* @param sessionId - UUID session identifier
|
|
156
|
+
* @param workdir - Working directory the session belongs to
|
|
157
|
+
* @param customTitle - Title held in memory by the caller (may be undefined)
|
|
158
|
+
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
159
|
+
*/
|
|
160
|
+
export declare function reAppendSessionMetadata(sessionId: string, workdir: string, customTitle: string | undefined, sessionType?: "main" | "subagent"): Promise<void>;
|
|
87
161
|
/**
|
|
88
162
|
* Load session data from JSONL file (new approach)
|
|
89
163
|
*
|