@cjhyy/code-shell-core 0.8.9 → 0.8.11

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 (76) hide show
  1. package/dist/automation/scheduler.js +49 -0
  2. package/dist/automation/store.d.ts +1 -1
  3. package/dist/automation/store.js +184 -10
  4. package/dist/cli/agent-server-stdio.js +7 -0
  5. package/dist/credentials/store.d.ts +14 -0
  6. package/dist/credentials/store.js +245 -42
  7. package/dist/engine/engine.js +45 -6
  8. package/dist/engine/file-history-hook.js +24 -5
  9. package/dist/engine/run-types.d.ts +9 -0
  10. package/dist/engine/turn-loop.js +9 -8
  11. package/dist/goal/lifecycle.d.ts +2 -0
  12. package/dist/goal/lifecycle.js +56 -33
  13. package/dist/index.d.ts +2 -3
  14. package/dist/index.internal.d.ts +1 -0
  15. package/dist/index.internal.js +1 -0
  16. package/dist/index.js +2 -2
  17. package/dist/links/cli.d.ts +2 -0
  18. package/dist/links/cli.js +11 -4
  19. package/dist/model-catalog/index.js +19 -4
  20. package/dist/model-catalog/save-entry.js +122 -61
  21. package/dist/model-catalog/types.js +27 -23
  22. package/dist/panel-apps/installer.js +27 -14
  23. package/dist/panel-apps/registry.js +60 -12
  24. package/dist/plugins/installedPlugins.d.ts +4 -0
  25. package/dist/plugins/installedPlugins.js +70 -30
  26. package/dist/plugins/installer/types.d.ts +12 -12
  27. package/dist/plugins/installer/update.js +37 -38
  28. package/dist/plugins/knownMarketplaces.d.ts +7 -3
  29. package/dist/plugins/knownMarketplaces.js +127 -23
  30. package/dist/plugins/pluginCatalog.js +18 -4
  31. package/dist/plugins/pluginHookApproval.js +56 -60
  32. package/dist/plugins/pluginMcpApproval.js +50 -52
  33. package/dist/profile/catalog-store.js +39 -4
  34. package/dist/profile/catalog.js +55 -15
  35. package/dist/profile/store.js +51 -21
  36. package/dist/protocol/chat-session-manager.d.ts +9 -0
  37. package/dist/protocol/chat-session-manager.js +13 -0
  38. package/dist/protocol/chat-session.d.ts +5 -0
  39. package/dist/protocol/chat-session.js +1 -0
  40. package/dist/protocol/server.d.ts +2 -0
  41. package/dist/protocol/server.js +75 -29
  42. package/dist/protocol/types.d.ts +8 -0
  43. package/dist/run/FileRunStore.d.ts +2 -0
  44. package/dist/run/FileRunStore.js +153 -18
  45. package/dist/run/Heartbeat.js +63 -4
  46. package/dist/services/auto-dream.js +39 -17
  47. package/dist/services/session-memory.js +107 -8
  48. package/dist/session/file-history.d.ts +63 -2
  49. package/dist/session/file-history.js +593 -86
  50. package/dist/session/session-manager.d.ts +1 -0
  51. package/dist/session/session-manager.js +52 -21
  52. package/dist/session/transcript.js +33 -3
  53. package/dist/session/undo-target.d.ts +15 -6
  54. package/dist/session/undo-target.js +26 -9
  55. package/dist/settings/manager.d.ts +22 -3
  56. package/dist/settings/manager.js +185 -50
  57. package/dist/settings/schema.d.ts +3 -3
  58. package/dist/sources/adapters/local-files.js +49 -4
  59. package/dist/sources/catalog.js +64 -18
  60. package/dist/sources/types.d.ts +3 -3
  61. package/dist/sources/types.js +7 -4
  62. package/dist/themes/installer.js +192 -28
  63. package/dist/tool-system/builtin/add-marketplace.js +21 -1
  64. package/dist/tool-system/builtin/cron.d.ts +2 -1
  65. package/dist/tool-system/builtin/cron.js +20 -6
  66. package/dist/tool-system/builtin/index.js +44 -0
  67. package/dist/tool-system/builtin/install-capability.d.ts +52 -0
  68. package/dist/tool-system/builtin/install-capability.js +1057 -0
  69. package/dist/tool-system/builtin/skill.js +3 -1
  70. package/dist/tool-system/executor.js +1 -0
  71. package/dist/tool-system/registry.js +5 -0
  72. package/dist/tool-system/sandbox/index.d.ts +1 -0
  73. package/dist/tool-system/sandbox/index.js +4 -1
  74. package/dist/utils/file-mutex.d.ts +2 -0
  75. package/dist/utils/file-mutex.js +29 -4
  76. package/package.json +2 -1
@@ -12,6 +12,10 @@
12
12
  * ("openai"/"google"/"fal"). `shape` is documentation/future only.
13
13
  */
14
14
  import { z } from "zod";
15
+ const id = z.string().min(1).max(128);
16
+ const shortText = z.string().max(256);
17
+ const longText = z.string().max(32_768);
18
+ const boundedNumber = z.number().finite().min(0).max(1_000_000_000);
15
19
  /**
16
20
  * How the param maps onto the outgoing request body. `field` is the request
17
21
  * field this param lands on — same param name can land differently per
@@ -20,7 +24,7 @@ import { z } from "zod";
20
24
  * in `if (kind === ...)` branches in the engine. Minimal v1: field only.
21
25
  */
22
26
  export const wireSpecSchema = z.object({
23
- field: z.string(),
27
+ field: id,
24
28
  });
25
29
  /**
26
30
  * One **generic** declarative param — no special-casing per param. reasoning
@@ -32,18 +36,18 @@ export const wireSpecSchema = z.object({
32
36
  */
33
37
  export const paramSpecSchema = z.object({
34
38
  /** Logical name, e.g. "reasoning" / "size" / "quality" / "temperature". */
35
- name: z.string(),
39
+ name: id,
36
40
  /** UI control label (falls back to `name`). */
37
- label: z.string().optional(),
41
+ label: shortText.optional(),
38
42
  control: z.enum(["enum", "number", "toggle", "text"]),
39
43
  /** control=enum allowed values, e.g. ["low","medium","high","xhigh"]. */
40
- options: z.array(z.string()).optional(),
44
+ options: z.array(shortText).max(64).optional(),
41
45
  /** control=number bounds. */
42
- min: z.number().optional(),
43
- max: z.number().optional(),
44
- default: z.union([z.string(), z.number(), z.boolean()]).optional(),
46
+ min: boundedNumber.optional(),
47
+ max: boundedNumber.optional(),
48
+ default: z.union([shortText, boundedNumber, z.boolean()]).optional(),
45
49
  /** Natural-language usage note → injected into the tool description. */
46
- doc: z.string().optional(),
50
+ doc: z.string().max(4_096).optional(),
47
51
  /** How this param lands on the request body. */
48
52
  wire: wireSpecSchema.optional(),
49
53
  });
@@ -53,17 +57,17 @@ export const paramSpecSchema = z.object({
53
57
  * can expose different params, since gateways normalize differently).
54
58
  */
55
59
  export const modelPresetSchema = z.object({
56
- value: z.string(),
57
- label: z.string().optional(),
58
- maxContextTokens: z.number().optional(),
59
- maxOutputTokens: z.number().optional(),
60
+ value: id,
61
+ label: shortText.optional(),
62
+ maxContextTokens: boundedNumber.optional(),
63
+ maxOutputTokens: boundedNumber.optional(),
60
64
  supportsVision: z.boolean().optional(),
61
65
  /** Params this model supports; absent → no adjustable knobs. */
62
- params: z.array(paramSpecSchema).optional(),
66
+ params: z.array(paramSpecSchema).max(64).optional(),
63
67
  });
64
68
  export const catalogEntrySchema = z.object({
65
69
  /** Template id, e.g. "openai" / "openai-images" / "fal-video". */
66
- id: z.string(),
70
+ id,
67
71
  /** Which 连接 page group this lands in. (audio = speech-to-text / voice input.) */
68
72
  tag: z.enum(["text", "image", "video", "audio"]),
69
73
  /**
@@ -71,15 +75,15 @@ export const catalogEntrySchema = z.object({
71
75
  * runtime adapter for image/video. OpenRouter is `openrouter` even though its
72
76
  * text protocol is `openai-compat`.
73
77
  */
74
- adapterKind: z.string(),
78
+ adapterKind: id,
75
79
  /** LLM client protocol (text entries). */
76
80
  protocol: z.enum(["openai-compat", "anthropic-style"]).optional(),
77
81
  /** HTTP shape — documentation/future only; runtime dispatches on adapterKind. */
78
82
  shape: z.enum(["generic-sync", "fal-queue"]).optional(),
79
- displayName: z.string(),
80
- description: z.string(),
81
- defaultBaseUrl: z.string(),
82
- defaultModel: z.string().optional(),
83
+ displayName: z.string().min(1).max(256),
84
+ description: z.string().max(4_096),
85
+ defaultBaseUrl: z.string().max(4_096),
86
+ defaultModel: shortText.optional(),
83
87
  /** Whether this provider needs an API key (ollama/local = false). */
84
88
  needsKey: z.boolean().optional(),
85
89
  /**
@@ -89,8 +93,8 @@ export const catalogEntrySchema = z.object({
89
93
  * `value`, so future built-in models remain visible.
90
94
  */
91
95
  modelPresetsMode: z.enum(["replace", "merge"]).optional(),
92
- modelPresets: z.array(modelPresetSchema).optional(),
93
- signupUrl: z.string().optional(),
96
+ modelPresets: z.array(modelPresetSchema).max(512).optional(),
97
+ signupUrl: z.string().max(4_096).optional(),
94
98
  /** Whether the 连接 card offers a "测试" button (image=true, video=false). */
95
99
  test: z.boolean().optional(),
96
100
  /**
@@ -98,7 +102,7 @@ export const catalogEntrySchema = z.object({
98
102
  * the agent via the dynamic GenerateImage/GenerateVideo tool description so it
99
103
  * knows what a configured model accepts (different models differ).
100
104
  */
101
- paramsDoc: z.string().optional(),
105
+ paramsDoc: longText.optional(),
102
106
  });
103
107
  /** A user-catalog file is just an array of entries (zod-validated on load). */
104
- export const userCatalogFileSchema = z.array(catalogEntrySchema);
108
+ export const userCatalogFileSchema = z.array(catalogEntrySchema).max(256);
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { existsSync } from "node:fs";
3
- import { cp, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
2
+ import { constants, existsSync } from "node:fs";
3
+ import { cp, lstat, mkdir, mkdtemp, open, readdir, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { basename, extname, join, posix, relative, resolve, sep } from "node:path";
6
6
  import { extractZip, extractZipSubdirectory } from "../plugins/installer/unzip.js";
@@ -279,22 +279,34 @@ async function walkBoundedTree(root, directory, depth, budget, files, directorie
279
279
  }
280
280
  }
281
281
  async function readManifest(sourceRoot) {
282
- const file = join(sourceRoot, PANEL_APP_MANIFEST_FILE);
283
- let info;
284
282
  try {
285
- info = await stat(file);
283
+ const raw = await readBoundedPackageFile(sourceRoot, PANEL_APP_MANIFEST_FILE, MAX_MANIFEST_BYTES);
284
+ return PanelAppManifest.parse(JSON.parse(raw.toString("utf8")));
286
285
  }
287
- catch {
288
- throw new PanelAppInstallError(`missing ${PANEL_APP_MANIFEST_FILE}`);
286
+ catch (error) {
287
+ throw new PanelAppInstallError(`invalid Panel App manifest: ${error instanceof Error ? error.message : String(error)}`);
288
+ }
289
+ }
290
+ async function readBoundedPackageFile(root, relativePath, maxBytes) {
291
+ const candidate = join(root, ...relativePath.split("/"));
292
+ const metadata = await lstat(candidate);
293
+ if (metadata.isSymbolicLink() || !metadata.isFile() || metadata.size > maxBytes) {
294
+ throw new PanelAppInstallError(`Panel App file is not a bounded regular file: ${relativePath}`);
289
295
  }
290
- if (!info.isFile() || info.size > MAX_MANIFEST_BYTES) {
291
- throw new PanelAppInstallError(`${PANEL_APP_MANIFEST_FILE} must be a regular file no larger than 1 MiB`);
296
+ const physical = await realpath(candidate);
297
+ if (physical !== root && !physical.startsWith(`${root}${sep}`)) {
298
+ throw new PanelAppInstallError(`Panel App file escapes its package: ${relativePath}`);
292
299
  }
300
+ const handle = await open(candidate, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
293
301
  try {
294
- return PanelAppManifest.parse(JSON.parse(await readFile(file, "utf-8")));
302
+ const opened = await handle.stat();
303
+ if (!opened.isFile() || opened.size > maxBytes) {
304
+ throw new PanelAppInstallError(`Panel App file is not a bounded regular file: ${relativePath}`);
305
+ }
306
+ return await handle.readFile();
295
307
  }
296
- catch (error) {
297
- throw new PanelAppInstallError(`invalid Panel App manifest: ${error instanceof Error ? error.message : String(error)}`);
308
+ finally {
309
+ await handle.close();
298
310
  }
299
311
  }
300
312
  async function inspectPanelAppSource(sourceRoot) {
@@ -323,10 +335,11 @@ async function inspectPanelAppSource(sourceRoot) {
323
335
  if (!files.includes(skillEntry)) {
324
336
  throw new PanelAppInstallError(`declared Panel App skill is missing: ${skillEntry}`);
325
337
  }
326
- const skillInfo = await stat(join(root, ...skillEntry.split("/")));
338
+ const skillInfo = await lstat(join(root, ...skillEntry.split("/")));
327
339
  if (!skillInfo.isFile() || skillInfo.size > MAX_AGENT_SKILL_BYTES) {
328
340
  throw new PanelAppInstallError(`declared Panel App skill must be a file no larger than 256 KiB: ${skillEntry}`);
329
341
  }
342
+ await readBoundedPackageFile(root, skillEntry, MAX_AGENT_SKILL_BYTES);
330
343
  }
331
344
  for (const file of files) {
332
345
  if (file === PANEL_APP_MANIFEST_FILE || file === PANEL_APP_META_FILE)
@@ -352,7 +365,7 @@ async function inspectPanelAppSource(sourceRoot) {
352
365
  hash
353
366
  .update(file)
354
367
  .update("\0")
355
- .update(await readFile(join(root, ...file.split("/"))));
368
+ .update(await readBoundedPackageFile(root, file, file === PANEL_APP_MANIFEST_FILE ? MAX_MANIFEST_BYTES : MAX_FILE_BYTES));
356
369
  }
357
370
  return { manifest, files, digest: hash.digest("hex") };
358
371
  }
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { existsSync } from "node:fs";
3
- import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { chmod, lstat, mkdir, open, rename, rm, writeFile } from "node:fs/promises";
4
4
  import { dirname } from "node:path";
5
5
  import { z } from "zod";
6
6
  import { panelAppsRegistryPath } from "./paths.js";
@@ -28,47 +28,95 @@ const Registry = z
28
28
  apps: z.array(RegistryEntry).max(1_024),
29
29
  })
30
30
  .strict();
31
- async function readRegistryUnlocked() {
31
+ const MAX_PANEL_APP_REGISTRY_BYTES = 4 * 1024 * 1024;
32
+ async function registryEntry(path) {
33
+ try {
34
+ return await lstat(path);
35
+ }
36
+ catch (error) {
37
+ if (error.code === "ENOENT")
38
+ return null;
39
+ throw error;
40
+ }
41
+ }
42
+ async function checkedRegistryDirectory() {
43
+ const directory = dirname(panelAppsRegistryPath());
44
+ await mkdir(directory, { recursive: true, mode: 0o700 });
45
+ const metadata = await lstat(directory);
46
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
47
+ throw new Error("Panel App registry directory must be a real directory");
48
+ }
49
+ return directory;
50
+ }
51
+ async function readRegistryUnlocked(strict = false) {
32
52
  const path = panelAppsRegistryPath();
33
- if (!existsSync(path))
34
- return [];
53
+ let handle;
35
54
  try {
36
- return Registry.parse(JSON.parse(await readFile(path, "utf-8"))).apps;
55
+ const metadata = await registryEntry(path);
56
+ if (!metadata)
57
+ return [];
58
+ if (metadata.isSymbolicLink() ||
59
+ !metadata.isFile() ||
60
+ metadata.size > MAX_PANEL_APP_REGISTRY_BYTES) {
61
+ throw new Error("Panel App registry must be a bounded regular file");
62
+ }
63
+ handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
64
+ const opened = await handle.stat();
65
+ if (!opened.isFile() || opened.size > MAX_PANEL_APP_REGISTRY_BYTES) {
66
+ throw new Error("Panel App registry must be a bounded regular file");
67
+ }
68
+ return Registry.parse(JSON.parse(await handle.readFile("utf8"))).apps;
37
69
  }
38
- catch {
70
+ catch (error) {
71
+ if (strict) {
72
+ const detail = error instanceof Error ? `: ${error.message}` : "";
73
+ throw new Error(`Panel App registry is corrupt${detail}`, { cause: error });
74
+ }
39
75
  return [];
40
76
  }
77
+ finally {
78
+ await handle?.close().catch(() => undefined);
79
+ }
41
80
  }
42
81
  async function writeRegistryUnlocked(apps) {
43
82
  const path = panelAppsRegistryPath();
44
- await mkdir(dirname(path), { recursive: true, mode: 0o700 });
83
+ await checkedRegistryDirectory();
84
+ const target = await registryEntry(path);
85
+ if (target && (target.isSymbolicLink() || !target.isFile())) {
86
+ throw new Error("Panel App registry target must be a regular file");
87
+ }
45
88
  const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
46
89
  try {
47
90
  const registry = Registry.parse({
48
91
  version: 1,
49
92
  apps: [...apps].sort((left, right) => left.id.localeCompare(right.id)),
50
93
  });
51
- await writeFile(tmp, `${JSON.stringify(registry, null, 2)}\n`, {
94
+ const serialized = `${JSON.stringify(registry, null, 2)}\n`;
95
+ if (Buffer.byteLength(serialized, "utf8") > MAX_PANEL_APP_REGISTRY_BYTES) {
96
+ throw new Error("Panel App registry is too large");
97
+ }
98
+ await writeFile(tmp, serialized, {
52
99
  encoding: "utf-8",
53
100
  flag: "wx",
54
101
  mode: 0o600,
55
102
  });
56
103
  await rename(tmp, path);
104
+ if (process.platform !== "win32")
105
+ await chmod(path, 0o600);
57
106
  }
58
107
  finally {
59
108
  await rm(tmp, { force: true });
60
109
  }
61
110
  }
62
111
  async function mutateRegistry(mutation) {
63
- const directory = dirname(panelAppsRegistryPath());
64
- await mkdir(directory, { recursive: true, mode: 0o700 });
112
+ const directory = await checkedRegistryDirectory();
65
113
  const release = await lock(directory, {
66
114
  realpath: true,
67
115
  stale: 10_000,
68
116
  retries: { retries: 8, minTimeout: 10, maxTimeout: 120, factor: 1.5 },
69
117
  });
70
118
  try {
71
- const next = mutation(await readRegistryUnlocked());
119
+ const next = mutation(await readRegistryUnlocked(true));
72
120
  await writeRegistryUnlocked(next.apps);
73
121
  return next.result;
74
122
  }
@@ -7,6 +7,10 @@ import type { InstalledPluginsV2, PluginInstallEntry } from "./types.js";
7
7
  export declare function installedPluginsPath(): string;
8
8
  export declare function readInstalledPlugins(): InstalledPluginsV2;
9
9
  export declare function writeInstalledPlugins(data: InstalledPluginsV2): void;
10
+ export declare function mutateInstalledPlugins<R>(mutation: (current: InstalledPluginsV2) => {
11
+ value?: InstalledPluginsV2;
12
+ result?: R;
13
+ }): R | undefined;
10
14
  /**
11
15
  * Append an install entry for `<plugin>@<marketplace>`. Multiple entries
12
16
  * for the same key are allowed (different scopes); MVP only writes scope:"user".
@@ -3,10 +3,10 @@
3
3
  * Reads the Claude Code-compatible V2 shape plus optional CodeShell integrity
4
4
  * fields on each install entry.
5
5
  */
6
- import { randomUUID } from "node:crypto";
7
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
8
- import { dirname, join } from "node:path";
6
+ import { chmodSync, closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, } from "node:fs";
7
+ import { join } from "node:path";
9
8
  import { homedir } from "node:os";
9
+ import { mutateJsonFile } from "../utils/file-mutex.js";
10
10
  const MAX_PLUGIN_KEYS = 2_048;
11
11
  const MAX_INSTALLS_PER_KEY = 16;
12
12
  const MAX_KEY_LENGTH = 256;
@@ -16,6 +16,7 @@ const DIGEST_RE = /^[a-f0-9]{64}$/;
16
16
  const MAX_HOOK_REVIEW_ITEMS = 256;
17
17
  const MAX_HOOK_REVIEW_COMMAND_LENGTH = 4_096;
18
18
  const MAX_HOOK_REVIEW_MATCHER_LENGTH = 4_096;
19
+ const MAX_INSTALLED_PLUGINS_FILE_BYTES = 16 * 1024 * 1024;
19
20
  function userHome() {
20
21
  return process.env.HOME ?? homedir();
21
22
  }
@@ -142,52 +143,91 @@ function registryOf(value) {
142
143
  }
143
144
  export function readInstalledPlugins() {
144
145
  const path = installedPluginsPath();
145
- if (!existsSync(path))
146
- return { version: 2, plugins: {} };
146
+ let descriptor;
147
147
  try {
148
- const parsed = registryOf(JSON.parse(readFileSync(path, "utf-8")));
148
+ const metadata = lstatSync(path);
149
+ if (metadata.isSymbolicLink() ||
150
+ !metadata.isFile() ||
151
+ metadata.size > MAX_INSTALLED_PLUGINS_FILE_BYTES) {
152
+ return { version: 2, plugins: {} };
153
+ }
154
+ descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
155
+ const opened = fstatSync(descriptor);
156
+ if (!opened.isFile() || opened.size > MAX_INSTALLED_PLUGINS_FILE_BYTES) {
157
+ return { version: 2, plugins: {} };
158
+ }
159
+ const parsed = registryOf(JSON.parse(readFileSync(descriptor, "utf-8")));
149
160
  if (parsed)
150
161
  return parsed;
151
162
  }
152
163
  catch {
153
164
  // Corrupt — treat as empty so the user can re-install.
154
165
  }
166
+ finally {
167
+ if (descriptor !== undefined)
168
+ closeSync(descriptor);
169
+ }
155
170
  return { version: 2, plugins: {} };
156
171
  }
157
172
  export function writeInstalledPlugins(data) {
158
173
  const path = installedPluginsPath();
159
- mkdirSync(dirname(path), { recursive: true });
160
- const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
161
- try {
162
- writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", {
163
- encoding: "utf-8",
164
- flag: "wx",
165
- mode: 0o600,
166
- });
167
- renameSync(tmp, path);
168
- }
169
- finally {
170
- rmSync(tmp, { force: true });
171
- }
174
+ const normalized = registryOf(data);
175
+ if (!normalized)
176
+ throw new Error("invalid installed plugins registry");
177
+ mutateJsonFile(path, {
178
+ parse: parseInstalledPlugins,
179
+ serialize: (value) => `${JSON.stringify(value, null, 2)}\n`,
180
+ mutation: () => ({ value: normalized }),
181
+ mode: 0o600,
182
+ maxBytes: MAX_INSTALLED_PLUGINS_FILE_BYTES,
183
+ });
184
+ if (process.platform !== "win32")
185
+ chmodSync(path, 0o600);
186
+ }
187
+ function parseInstalledPlugins(raw) {
188
+ if (raw === undefined)
189
+ return { version: 2, plugins: {} };
190
+ const parsed = registryOf(JSON.parse(raw));
191
+ if (!parsed)
192
+ throw new Error("installed plugins registry is corrupt");
193
+ return parsed;
194
+ }
195
+ export function mutateInstalledPlugins(mutation) {
196
+ return mutateJsonFile(installedPluginsPath(), {
197
+ parse: parseInstalledPlugins,
198
+ serialize: (value) => `${JSON.stringify(value, null, 2)}\n`,
199
+ mutation,
200
+ mode: 0o600,
201
+ maxBytes: MAX_INSTALLED_PLUGINS_FILE_BYTES,
202
+ });
172
203
  }
173
204
  /**
174
205
  * Append an install entry for `<plugin>@<marketplace>`. Multiple entries
175
206
  * for the same key are allowed (different scopes); MVP only writes scope:"user".
176
207
  */
177
208
  export function appendInstallEntry(key, entry) {
178
- const data = readInstalledPlugins();
179
- const list = data.plugins[key] ?? [];
180
- list.push(entry);
181
- data.plugins[key] = list;
182
- writeInstalledPlugins(data);
209
+ if (key.length === 0 || key.length > MAX_KEY_LENGTH || key.includes("\0")) {
210
+ throw new Error("invalid plugin install key");
211
+ }
212
+ const normalized = installEntryOf(entry);
213
+ if (!normalized)
214
+ throw new Error("invalid plugin install entry");
215
+ mutateInstalledPlugins((data) => {
216
+ const list = [...(data.plugins[key] ?? [])];
217
+ if (list.length >= MAX_INSTALLS_PER_KEY)
218
+ throw new Error("too many plugin installs for key");
219
+ list.push(normalized);
220
+ data.plugins[key] = list;
221
+ return { value: data };
222
+ });
183
223
  }
184
224
  export function removeInstallEntries(key) {
185
- const data = readInstalledPlugins();
186
- if (!(key in data.plugins))
187
- return false;
188
- delete data.plugins[key];
189
- writeInstalledPlugins(data);
190
- return true;
225
+ return (mutateInstalledPlugins((data) => {
226
+ if (!Object.prototype.hasOwnProperty.call(data.plugins, key))
227
+ return { result: false };
228
+ delete data.plugins[key];
229
+ return { value: data, result: true };
230
+ }) ?? false);
191
231
  }
192
232
  export function pluginInstallKey(plugin, marketplace) {
193
233
  return `${plugin}@${marketplace}`;
@@ -29,8 +29,8 @@ export declare const PluginAutomationTemplate: z.ZodObject<{
29
29
  en?: string | undefined;
30
30
  "zh-CN"?: string | undefined;
31
31
  };
32
- permissionLevel: "full" | "read-only" | "workspace-write";
33
32
  schedule: string;
33
+ permissionLevel: "full" | "read-only" | "workspace-write";
34
34
  description?: string | undefined;
35
35
  timezone?: string | undefined;
36
36
  }, {
@@ -79,8 +79,8 @@ export declare const PluginAutomationsManifest: z.ZodEffects<z.ZodObject<{
79
79
  en?: string | undefined;
80
80
  "zh-CN"?: string | undefined;
81
81
  };
82
- permissionLevel: "full" | "read-only" | "workspace-write";
83
82
  schedule: string;
83
+ permissionLevel: "full" | "read-only" | "workspace-write";
84
84
  description?: string | undefined;
85
85
  timezone?: string | undefined;
86
86
  }, {
@@ -108,8 +108,8 @@ export declare const PluginAutomationsManifest: z.ZodEffects<z.ZodObject<{
108
108
  en?: string | undefined;
109
109
  "zh-CN"?: string | undefined;
110
110
  };
111
- permissionLevel: "full" | "read-only" | "workspace-write";
112
111
  schedule: string;
112
+ permissionLevel: "full" | "read-only" | "workspace-write";
113
113
  description?: string | undefined;
114
114
  timezone?: string | undefined;
115
115
  }[];
@@ -140,8 +140,8 @@ export declare const PluginAutomationsManifest: z.ZodEffects<z.ZodObject<{
140
140
  en?: string | undefined;
141
141
  "zh-CN"?: string | undefined;
142
142
  };
143
- permissionLevel: "full" | "read-only" | "workspace-write";
144
143
  schedule: string;
144
+ permissionLevel: "full" | "read-only" | "workspace-write";
145
145
  description?: string | undefined;
146
146
  timezone?: string | undefined;
147
147
  }[];
@@ -249,8 +249,8 @@ export declare const CodeShellPluginOverlay: z.ZodObject<{
249
249
  en?: string | undefined;
250
250
  "zh-CN"?: string | undefined;
251
251
  };
252
- permissionLevel: "full" | "read-only" | "workspace-write";
253
252
  schedule: string;
253
+ permissionLevel: "full" | "read-only" | "workspace-write";
254
254
  description?: string | undefined;
255
255
  timezone?: string | undefined;
256
256
  }, {
@@ -278,8 +278,8 @@ export declare const CodeShellPluginOverlay: z.ZodObject<{
278
278
  en?: string | undefined;
279
279
  "zh-CN"?: string | undefined;
280
280
  };
281
- permissionLevel: "full" | "read-only" | "workspace-write";
282
281
  schedule: string;
282
+ permissionLevel: "full" | "read-only" | "workspace-write";
283
283
  description?: string | undefined;
284
284
  timezone?: string | undefined;
285
285
  }[];
@@ -310,8 +310,8 @@ export declare const CodeShellPluginOverlay: z.ZodObject<{
310
310
  en?: string | undefined;
311
311
  "zh-CN"?: string | undefined;
312
312
  };
313
- permissionLevel: "full" | "read-only" | "workspace-write";
314
313
  schedule: string;
314
+ permissionLevel: "full" | "read-only" | "workspace-write";
315
315
  description?: string | undefined;
316
316
  timezone?: string | undefined;
317
317
  }[];
@@ -345,8 +345,8 @@ export declare const CodeShellPluginOverlay: z.ZodObject<{
345
345
  en?: string | undefined;
346
346
  "zh-CN"?: string | undefined;
347
347
  };
348
- permissionLevel: "full" | "read-only" | "workspace-write";
349
348
  schedule: string;
349
+ permissionLevel: "full" | "read-only" | "workspace-write";
350
350
  description?: string | undefined;
351
351
  timezone?: string | undefined;
352
352
  }[];
@@ -634,8 +634,8 @@ export declare const CanonicalPluginManifest: z.ZodObject<{
634
634
  en?: string | undefined;
635
635
  "zh-CN"?: string | undefined;
636
636
  };
637
- permissionLevel: "full" | "read-only" | "workspace-write";
638
637
  schedule: string;
638
+ permissionLevel: "full" | "read-only" | "workspace-write";
639
639
  description?: string | undefined;
640
640
  timezone?: string | undefined;
641
641
  }, {
@@ -663,8 +663,8 @@ export declare const CanonicalPluginManifest: z.ZodObject<{
663
663
  en?: string | undefined;
664
664
  "zh-CN"?: string | undefined;
665
665
  };
666
- permissionLevel: "full" | "read-only" | "workspace-write";
667
666
  schedule: string;
667
+ permissionLevel: "full" | "read-only" | "workspace-write";
668
668
  description?: string | undefined;
669
669
  timezone?: string | undefined;
670
670
  }[];
@@ -695,8 +695,8 @@ export declare const CanonicalPluginManifest: z.ZodObject<{
695
695
  en?: string | undefined;
696
696
  "zh-CN"?: string | undefined;
697
697
  };
698
- permissionLevel: "full" | "read-only" | "workspace-write";
699
698
  schedule: string;
699
+ permissionLevel: "full" | "read-only" | "workspace-write";
700
700
  description?: string | undefined;
701
701
  timezone?: string | undefined;
702
702
  }[];
@@ -750,8 +750,8 @@ export declare const CanonicalPluginManifest: z.ZodObject<{
750
750
  en?: string | undefined;
751
751
  "zh-CN"?: string | undefined;
752
752
  };
753
- permissionLevel: "full" | "read-only" | "workspace-write";
754
753
  schedule: string;
754
+ permissionLevel: "full" | "read-only" | "workspace-write";
755
755
  description?: string | undefined;
756
756
  timezone?: string | undefined;
757
757
  }[];