@cjhyy/code-shell-core 0.8.8 → 0.8.10

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 (86) 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 +81 -27
  8. package/dist/engine/file-history-hook.js +24 -5
  9. package/dist/engine/run-finalize.js +8 -6
  10. package/dist/engine/run-session-open.js +1 -1
  11. package/dist/engine/run-setup.d.ts +4 -0
  12. package/dist/engine/run-setup.js +5 -1
  13. package/dist/engine/run-tooling.js +7 -1
  14. package/dist/engine/run-types.d.ts +33 -0
  15. package/dist/engine/run-types.js +21 -0
  16. package/dist/engine/run-workspace.js +11 -5
  17. package/dist/engine/turn-loop.js +9 -8
  18. package/dist/goal/lifecycle.d.ts +2 -0
  19. package/dist/goal/lifecycle.js +56 -33
  20. package/dist/index.d.ts +2 -3
  21. package/dist/index.internal.d.ts +1 -0
  22. package/dist/index.internal.js +1 -0
  23. package/dist/index.js +2 -2
  24. package/dist/links/cli.d.ts +2 -0
  25. package/dist/links/cli.js +11 -4
  26. package/dist/model-catalog/index.js +19 -4
  27. package/dist/model-catalog/save-entry.js +122 -61
  28. package/dist/model-catalog/types.js +27 -23
  29. package/dist/panel-apps/installer.js +27 -14
  30. package/dist/panel-apps/manifest.d.ts +11 -11
  31. package/dist/panel-apps/manifest.js +3 -1
  32. package/dist/panel-apps/registry.js +60 -12
  33. package/dist/plugins/installedPlugins.d.ts +4 -0
  34. package/dist/plugins/installedPlugins.js +70 -30
  35. package/dist/plugins/installer/types.d.ts +12 -12
  36. package/dist/plugins/installer/update.js +37 -38
  37. package/dist/plugins/knownMarketplaces.d.ts +7 -3
  38. package/dist/plugins/knownMarketplaces.js +127 -23
  39. package/dist/plugins/pluginCatalog.js +18 -4
  40. package/dist/plugins/pluginHookApproval.js +56 -60
  41. package/dist/plugins/pluginMcpApproval.js +50 -52
  42. package/dist/profile/catalog-store.js +39 -4
  43. package/dist/profile/catalog.js +55 -15
  44. package/dist/profile/store.js +51 -21
  45. package/dist/prompt/composer.d.ts +8 -0
  46. package/dist/prompt/composer.js +8 -0
  47. package/dist/protocol/chat-session-manager.d.ts +9 -0
  48. package/dist/protocol/chat-session-manager.js +22 -0
  49. package/dist/protocol/chat-session.d.ts +5 -0
  50. package/dist/protocol/chat-session.js +1 -0
  51. package/dist/protocol/server.d.ts +2 -0
  52. package/dist/protocol/server.js +112 -29
  53. package/dist/protocol/types.d.ts +20 -0
  54. package/dist/run/FileRunStore.d.ts +2 -0
  55. package/dist/run/FileRunStore.js +153 -18
  56. package/dist/run/Heartbeat.js +63 -4
  57. package/dist/services/auto-dream.js +39 -17
  58. package/dist/services/session-memory.js +107 -8
  59. package/dist/session/file-history.d.ts +63 -2
  60. package/dist/session/file-history.js +593 -86
  61. package/dist/session/session-manager.d.ts +4 -1
  62. package/dist/session/session-manager.js +94 -47
  63. package/dist/session/transcript.js +33 -3
  64. package/dist/session/undo-target.d.ts +15 -6
  65. package/dist/session/undo-target.js +26 -9
  66. package/dist/settings/manager.d.ts +22 -3
  67. package/dist/settings/manager.js +185 -50
  68. package/dist/settings/schema.d.ts +3 -3
  69. package/dist/sources/adapters/local-files.js +49 -4
  70. package/dist/sources/catalog.js +64 -18
  71. package/dist/sources/types.d.ts +3 -3
  72. package/dist/sources/types.js +7 -4
  73. package/dist/themes/installer.js +192 -28
  74. package/dist/tool-system/builtin/add-marketplace.js +21 -1
  75. package/dist/tool-system/builtin/cron.d.ts +2 -1
  76. package/dist/tool-system/builtin/cron.js +20 -6
  77. package/dist/tool-system/builtin/index.js +48 -1
  78. package/dist/tool-system/builtin/install-capability.d.ts +52 -0
  79. package/dist/tool-system/builtin/install-capability.js +1057 -0
  80. package/dist/tool-system/builtin/skill.js +3 -1
  81. package/dist/tool-system/executor.js +1 -0
  82. package/dist/tool-system/path-policy.js +25 -33
  83. package/dist/tool-system/registry.js +5 -0
  84. package/dist/utils/file-mutex.d.ts +2 -0
  85. package/dist/utils/file-mutex.js +29 -4
  86. package/package.json +2 -1
@@ -38,6 +38,50 @@ const MAX_SCHEDULE_INTERVAL_MS = 10 * 365 * 24 * 60 * 60 * 1000;
38
38
  export function isCronMisfire(scheduledFor, now) {
39
39
  return now - scheduledFor > CRON_MISFIRE_GRACE_MS;
40
40
  }
41
+ const MAX_JOB_NAME_CHARS = 512;
42
+ const MAX_JOB_SCHEDULE_CHARS = 512;
43
+ const MAX_JOB_PROMPT_CHARS = 1024 * 1024;
44
+ const MAX_JOB_CWD_CHARS = 32_768;
45
+ const MAX_JOB_TIMEZONE_CHARS = 128;
46
+ function validateJobFields(input) {
47
+ if (input.name !== undefined &&
48
+ (typeof input.name !== "string" ||
49
+ !input.name.trim() ||
50
+ input.name.length > MAX_JOB_NAME_CHARS ||
51
+ input.name.includes("\0"))) {
52
+ throw new Error("automation name must be a bounded non-empty string");
53
+ }
54
+ if (input.schedule !== undefined &&
55
+ (typeof input.schedule !== "string" ||
56
+ !input.schedule.trim() ||
57
+ input.schedule.length > MAX_JOB_SCHEDULE_CHARS ||
58
+ input.schedule.includes("\0"))) {
59
+ throw new Error("automation schedule must be a bounded non-empty string");
60
+ }
61
+ if (input.prompt !== undefined &&
62
+ (typeof input.prompt !== "string" ||
63
+ !input.prompt.trim() ||
64
+ input.prompt.length > MAX_JOB_PROMPT_CHARS ||
65
+ input.prompt.includes("\0"))) {
66
+ throw new Error("automation prompt must be a bounded non-empty string");
67
+ }
68
+ if (input.cwd !== undefined &&
69
+ (typeof input.cwd !== "string" || input.cwd.length > MAX_JOB_CWD_CHARS || input.cwd.includes("\0"))) {
70
+ throw new Error("automation cwd must be a bounded string");
71
+ }
72
+ if (input.timezone !== undefined &&
73
+ (typeof input.timezone !== "string" ||
74
+ input.timezone.length > MAX_JOB_TIMEZONE_CHARS ||
75
+ input.timezone.includes("\0"))) {
76
+ throw new Error("automation timezone must be a bounded string");
77
+ }
78
+ if (input.permissionLevel !== undefined &&
79
+ input.permissionLevel !== "read-only" &&
80
+ input.permissionLevel !== "workspace-write" &&
81
+ input.permissionLevel !== "full") {
82
+ throw new Error("invalid automation permission level");
83
+ }
84
+ }
41
85
  export class CronScheduler {
42
86
  jobs = new Map();
43
87
  timers = new Map();
@@ -301,6 +345,7 @@ export class CronScheduler {
301
345
  }
302
346
  }
303
347
  create(name, schedule, prompt, opts) {
348
+ validateJobFields({ name, schedule, prompt, ...opts });
304
349
  // Validate the schedule up front (interval or cron expr) so a bad string
305
350
  // surfaces at create time, not silently at the first missed tick.
306
351
  validateSchedule(schedule, opts?.timezone);
@@ -459,6 +504,10 @@ export class CronScheduler {
459
504
  * or null if the id is unknown.
460
505
  */
461
506
  update(id, patch) {
507
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
508
+ throw new Error("automation update patch must be an object");
509
+ }
510
+ validateJobFields(patch);
462
511
  if (this.store) {
463
512
  const tx = this.store.mutate((jobs) => {
464
513
  let updated = null;
@@ -11,7 +11,7 @@
11
11
  * the same lock and one process cannot overwrite another process's new job
12
12
  * with a stale in-memory snapshot.
13
13
  */
14
- import type { CronJob } from "./scheduler.js";
14
+ import { type CronJob } from "./scheduler.js";
15
15
  /**
16
16
  * Default global location. Mirrors FileRunStore's `~/.code-shell/...` layout.
17
17
  * An explicit `root` (a `~/.code-shell`-equivalent data root) overrides the
@@ -11,11 +11,145 @@
11
11
  * the same lock and one process cannot overwrite another process's new job
12
12
  * with a stale in-memory snapshot.
13
13
  */
14
- import { mkdirSync, existsSync, readFileSync, writeFileSync, renameSync, rmSync } from "node:fs";
14
+ import { chmodSync, mkdirSync, existsSync, readFileSync, statSync, writeFileSync, renameSync, rmSync, } from "node:fs";
15
+ import { randomUUID } from "node:crypto";
15
16
  import { dirname, join } from "node:path";
16
17
  import { homedir } from "node:os";
18
+ import { validateSchedule } from "./scheduler.js";
17
19
  import { logger } from "../logging/logger.js";
18
20
  import { lockSync } from "../utils/lockfile.js";
21
+ const MAX_CRON_FILE_BYTES = 32 * 1024 * 1024;
22
+ const MAX_CRON_JOBS = 4_096;
23
+ const SAFE_ID = /^[A-Za-z0-9_.-]{1,128}$/;
24
+ function normalizeJob(value, strict) {
25
+ const invalid = (field) => {
26
+ if (strict)
27
+ throw new Error(`invalid cron job ${field}`);
28
+ return undefined;
29
+ };
30
+ if (!value || typeof value !== "object" || Array.isArray(value))
31
+ return invalid("object");
32
+ const raw = value;
33
+ if (typeof raw.id !== "string" || !SAFE_ID.test(raw.id) || raw.id.includes("..")) {
34
+ return invalid("id");
35
+ }
36
+ if (typeof raw.name !== "string" ||
37
+ !raw.name.trim() ||
38
+ raw.name.length > 512 ||
39
+ raw.name.includes("\0")) {
40
+ return invalid("name");
41
+ }
42
+ if (typeof raw.schedule !== "string" ||
43
+ !raw.schedule.trim() ||
44
+ raw.schedule.length > 512 ||
45
+ raw.schedule.includes("\0")) {
46
+ return invalid("schedule");
47
+ }
48
+ if (typeof raw.prompt !== "string" ||
49
+ !raw.prompt.trim() ||
50
+ raw.prompt.length > 1024 * 1024 ||
51
+ raw.prompt.includes("\0")) {
52
+ return invalid("prompt");
53
+ }
54
+ if (typeof raw.enabled !== "boolean")
55
+ return invalid("enabled");
56
+ if (!Number.isSafeInteger(raw.runCount) || raw.runCount < 0) {
57
+ return invalid("runCount");
58
+ }
59
+ if (!Number.isSafeInteger(raw.createdAt) || raw.createdAt < 0) {
60
+ return invalid("createdAt");
61
+ }
62
+ if (raw.cwd !== undefined &&
63
+ (typeof raw.cwd !== "string" || raw.cwd.length > 32_768 || raw.cwd.includes("\0"))) {
64
+ return invalid("cwd");
65
+ }
66
+ if (raw.timezone !== undefined &&
67
+ (typeof raw.timezone !== "string" || raw.timezone.length > 128 || raw.timezone.includes("\0"))) {
68
+ return invalid("timezone");
69
+ }
70
+ if (raw.permissionLevel !== undefined &&
71
+ raw.permissionLevel !== "read-only" &&
72
+ raw.permissionLevel !== "workspace-write" &&
73
+ raw.permissionLevel !== "full") {
74
+ return invalid("permissionLevel");
75
+ }
76
+ for (const field of ["lastRun", "nextRun"]) {
77
+ if (raw[field] !== undefined &&
78
+ (!Number.isSafeInteger(raw[field]) || raw[field] < 0)) {
79
+ return invalid(field);
80
+ }
81
+ }
82
+ for (const field of ["lastRunId", "resumeSessionId"]) {
83
+ if (raw[field] !== undefined &&
84
+ (typeof raw[field] !== "string" ||
85
+ !raw[field] ||
86
+ raw[field].length > 128 ||
87
+ raw[field].includes("\0"))) {
88
+ return invalid(field);
89
+ }
90
+ }
91
+ if (raw.once !== undefined && typeof raw.once !== "boolean")
92
+ return invalid("once");
93
+ if (raw.disabledReason !== undefined &&
94
+ (typeof raw.disabledReason !== "string" || raw.disabledReason.length > 4_096)) {
95
+ return invalid("disabledReason");
96
+ }
97
+ let templateSource;
98
+ if (raw.templateSource !== undefined) {
99
+ if (!raw.templateSource || typeof raw.templateSource !== "object" || Array.isArray(raw.templateSource)) {
100
+ return invalid("templateSource");
101
+ }
102
+ const source = raw.templateSource;
103
+ if (typeof source.installKey !== "string" ||
104
+ !source.installKey ||
105
+ source.installKey.length > 512 ||
106
+ typeof source.templateId !== "string" ||
107
+ !source.templateId ||
108
+ source.templateId.length > 512 ||
109
+ typeof source.revision !== "string" ||
110
+ !source.revision ||
111
+ source.revision.length > 512 ||
112
+ (source.pluginVersion !== undefined &&
113
+ (typeof source.pluginVersion !== "string" || source.pluginVersion.length > 128))) {
114
+ return invalid("templateSource");
115
+ }
116
+ templateSource = {
117
+ installKey: source.installKey,
118
+ templateId: source.templateId,
119
+ revision: source.revision,
120
+ ...(typeof source.pluginVersion === "string" ? { pluginVersion: source.pluginVersion } : {}),
121
+ };
122
+ }
123
+ try {
124
+ validateSchedule(raw.schedule, typeof raw.timezone === "string" ? raw.timezone : undefined);
125
+ }
126
+ catch {
127
+ return invalid("schedule");
128
+ }
129
+ return {
130
+ id: raw.id,
131
+ name: raw.name,
132
+ schedule: raw.schedule,
133
+ prompt: raw.prompt,
134
+ enabled: raw.enabled,
135
+ runCount: raw.runCount,
136
+ createdAt: raw.createdAt,
137
+ ...(typeof raw.lastRun === "number" ? { lastRun: raw.lastRun } : {}),
138
+ ...(typeof raw.nextRun === "number" ? { nextRun: raw.nextRun } : {}),
139
+ ...(typeof raw.cwd === "string" ? { cwd: raw.cwd } : {}),
140
+ ...(typeof raw.timezone === "string" ? { timezone: raw.timezone } : {}),
141
+ ...(raw.permissionLevel === "read-only" ||
142
+ raw.permissionLevel === "workspace-write" ||
143
+ raw.permissionLevel === "full"
144
+ ? { permissionLevel: raw.permissionLevel }
145
+ : {}),
146
+ ...(typeof raw.lastRunId === "string" ? { lastRunId: raw.lastRunId } : {}),
147
+ ...(raw.once === true ? { once: true } : {}),
148
+ ...(typeof raw.resumeSessionId === "string" ? { resumeSessionId: raw.resumeSessionId } : {}),
149
+ ...(typeof raw.disabledReason === "string" ? { disabledReason: raw.disabledReason } : {}),
150
+ ...(templateSource ? { templateSource } : {}),
151
+ };
152
+ }
19
153
  /**
20
154
  * Default global location. Mirrors FileRunStore's `~/.code-shell/...` layout.
21
155
  * An explicit `root` (a `~/.code-shell`-equivalent data root) overrides the
@@ -64,11 +198,23 @@ export class CronStore {
64
198
  if (!existsSync(this.file))
65
199
  return [];
66
200
  try {
201
+ if (statSync(this.file).size > MAX_CRON_FILE_BYTES) {
202
+ throw new Error("cron store exceeds the maximum file size");
203
+ }
67
204
  const raw = readFileSync(this.file, "utf-8");
68
205
  const parsed = JSON.parse(raw);
69
206
  if (!parsed || !Array.isArray(parsed.jobs))
70
207
  return [];
71
- return parsed.jobs;
208
+ const jobs = [];
209
+ const ids = new Set();
210
+ for (const value of parsed.jobs.slice(0, MAX_CRON_JOBS)) {
211
+ const job = normalizeJob(value, false);
212
+ if (!job || ids.has(job.id))
213
+ continue;
214
+ ids.add(job.id);
215
+ jobs.push(job);
216
+ }
217
+ return jobs;
72
218
  }
73
219
  catch (err) {
74
220
  // Corrupt snapshot — log and start fresh rather than crashing startup.
@@ -81,25 +227,53 @@ export class CronStore {
81
227
  }
82
228
  }
83
229
  saveUnlocked(jobs) {
230
+ if (!Array.isArray(jobs) || jobs.length > MAX_CRON_JOBS) {
231
+ throw new Error("cron store exceeds the maximum job count");
232
+ }
233
+ const normalized = [];
234
+ const ids = new Set();
235
+ let estimatedBytes = 32;
236
+ for (const value of jobs) {
237
+ const job = normalizeJob(value, true);
238
+ if (ids.has(job.id))
239
+ throw new Error(`duplicate cron job id: ${job.id}`);
240
+ ids.add(job.id);
241
+ estimatedBytes += Buffer.byteLength(JSON.stringify(job)) + 2;
242
+ if (estimatedBytes > MAX_CRON_FILE_BYTES) {
243
+ throw new Error("cron store exceeds the maximum file size");
244
+ }
245
+ normalized.push(job);
246
+ }
84
247
  const dir = dirname(this.file);
85
248
  if (!existsSync(dir))
86
- mkdirSync(dir, { recursive: true });
87
- const snapshot = { version: 1, jobs };
249
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
250
+ if (process.platform !== "win32")
251
+ chmodSync(dir, 0o700);
252
+ const snapshot = { version: 1, jobs: normalized };
88
253
  // Unique tmp name so a concurrent writer can't clobber our staging file.
89
- const tmp = `${this.file}.${process.pid}.tmp`;
254
+ const tmp = `${this.file}.${process.pid}.${randomUUID()}.tmp`;
90
255
  try {
91
- writeFileSync(tmp, JSON.stringify(snapshot, null, 2) + "\n", "utf-8");
256
+ writeFileSync(tmp, JSON.stringify(snapshot, null, 2) + "\n", {
257
+ encoding: "utf-8",
258
+ mode: 0o600,
259
+ });
92
260
  renameSync(tmp, this.file);
93
261
  }
94
- catch (err) {
95
- rmSync(tmp, { force: true });
96
- throw err;
262
+ finally {
263
+ try {
264
+ rmSync(tmp, { force: true });
265
+ }
266
+ catch {
267
+ // Preserve the original persistence error.
268
+ }
97
269
  }
98
270
  }
99
271
  acquireStoreLock() {
100
272
  const dir = dirname(this.file);
101
273
  if (!existsSync(dir))
102
- mkdirSync(dir, { recursive: true });
274
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
275
+ if (process.platform !== "win32")
276
+ chmodSync(dir, 0o700);
103
277
  const deadline = Date.now() + 1_000;
104
278
  let lastError;
105
279
  while (Date.now() <= deadline) {
@@ -40,6 +40,7 @@ import { StdioTransport } from "../protocol/transport.js";
40
40
  import { createNotification, Methods } from "../protocol/types.js";
41
41
  import { setCronChangedSink } from "../tool-system/builtin/cron.js";
42
42
  import { setModelCatalogChangedSink } from "../tool-system/builtin/edit-model-catalog.js";
43
+ import { setCapabilityChangedSink } from "../tool-system/builtin/install-capability.js";
43
44
  import { SettingsManager, noRepoDir } from "../settings/manager.js";
44
45
  import { personalizationFrom } from "../settings/personalization.js";
45
46
  import { MCPManager } from "../tool-system/mcp-manager.js";
@@ -338,6 +339,12 @@ setCronChangedSink(() => {
338
339
  setModelCatalogChangedSink(() => {
339
340
  stdioTransport.send(createNotification(Methods.SettingsChanged, {}));
340
341
  });
342
+ // Conversational Plugin/Skill/MCP installs happen inside this worker. Tell the
343
+ // mounted Desktop renderer to refresh catalogs and push a fresh settings patch
344
+ // back to all live sessions, matching installs initiated from Extensions.
345
+ setCapabilityChangedSink(() => {
346
+ stdioTransport.send(createNotification(Methods.SettingsChanged, {}));
347
+ });
341
348
  // Disk-only active-goal reader for agent/goalGet. In worker mode there is no
342
349
  // legacyEngine, and reopening a session after restart doesn't create a live
343
350
  // ChatSession (that only happens on a send), so chatManager.get() misses. This
@@ -47,7 +47,21 @@ export declare class CredentialStore {
47
47
  */
48
48
  private decryptSecret;
49
49
  private read;
50
+ /**
51
+ * Read the store, reporting whether the on-disk state was fully understood.
52
+ *
53
+ * `readable: false` means "there is a file here but this build could not
54
+ * parse it" — NOT "there are no credentials". The distinction matters because
55
+ * mutate() commits whatever this returns: treating an unreadable file as an
56
+ * empty list turns any later save() into a wipe of every stored credential.
57
+ *
58
+ * `unknown` carries entries this build's schema rejects (for example a
59
+ * credential type added by a newer version). They are kept verbatim so a
60
+ * round-trip through an older build preserves rather than deletes them.
61
+ */
62
+ private readGuarded;
50
63
  private write;
64
+ private mutate;
51
65
  /** Upsert by id within a scope. */
52
66
  save(scope: CredentialScope, cred: Credential): void;
53
67
  /**