@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
@@ -32,6 +32,20 @@ export interface RunBehaviorProfile {
32
32
  disablePlanMode?: boolean;
33
33
  /** When true, MCP servers are neither connected nor exposed for the run. */
34
34
  disableMcp?: boolean;
35
+ /** Skip repository/user instruction discovery for this run. */
36
+ disableInstructions?: boolean;
37
+ /** Skip persistent-memory injection for this run. */
38
+ disableMemoryContext?: boolean;
39
+ /** Skip capability-owned volatile context providers for this run. */
40
+ disableCapabilityContext?: boolean;
41
+ /** Skip bound-source metadata injection for this run. */
42
+ disableSourcesContext?: boolean;
43
+ /** Ignore the project's default digital-human profile for this run. */
44
+ disableWorkspaceProfile?: boolean;
45
+ /** Skip SessionStart/UserPromptSubmit/SessionEnd hooks for this run. */
46
+ disableHooks?: boolean;
47
+ /** Skip auxiliary title generation for this run. */
48
+ disableSessionTitle?: boolean;
35
49
  /**
36
50
  * When true, the injected persistent-memory index is trimmed to this run's
37
51
  * project: global-layer project-type / dream-scope records tied to other
@@ -67,6 +81,10 @@ export interface RunBehaviorProfile {
67
81
  export declare const QUICK_CHAT_RESTRICTED_SYSTEM_PROMPT = "# Side Conversation Boundary\n\nThis is a side conversation, not the main-thread task execution environment.\n- Treat all content before this boundary as reference history only. Do not proactively continue any earlier plan, task, or modification.\n- Default to answering the user's question directly. Use lightweight read-only exploration only when needed.\n- Do not modify files, git state, configuration, or permissions unless the user explicitly asks after this boundary (for example, \"Allow you to modify files, please help me...\" or \"Please directly edit...\"). When explicitly requested, use the normally available tools subject to the current permission and approval mode.\n- Sub-agents are disabled for this side conversation. Do not create or invoke sub-agents.";
68
82
  /** The side-conversation restriction expressed as a generic behavior profile. */
69
83
  export declare const QUICK_CHAT_RESTRICTED_PROFILE: RunBehaviorProfile;
84
+ export declare const ISOLATED_TASK_BEHAVIOR_MODE: "isolatedTask";
85
+ export declare const ISOLATED_TASK_SYSTEM_PROMPT = "# Isolated Task Boundary\n\nThis is a bounded Task, not a continuation of any user conversation.\n- Use only the explicit Task input, the visible tool surface, and the selected Skill when one is available.\n- Do not infer or search for prior Session history, persistent memory, unrelated Skills, or other projects.\n- Do not create sub-agents or start unrelated work.\n- Finish the requested outcome, then return a concise result suitable for the owning application.";
86
+ /** Minimal, process-local run profile used by host-owned Tasks. */
87
+ export declare const ISOLATED_TASK_PROFILE: RunBehaviorProfile;
70
88
  export interface EngineRunOptions {
71
89
  cwd?: string;
72
90
  onStream?: StreamCallback;
@@ -80,9 +98,24 @@ export interface EngineRunOptions {
80
98
  goal?: string | GoalConfig;
81
99
  injected?: boolean;
82
100
  clientMessageId?: string;
101
+ /**
102
+ * Host-requested history boundary to archive after this run's user message
103
+ * has been appended, but before the first model call. The current
104
+ * `clientMessageId` is the exclusive end anchor.
105
+ */
106
+ archiveBeforeCurrentTurn?: {
107
+ fromClientMessageId?: string;
108
+ segmentId?: string;
109
+ };
83
110
  attachments?: InputAttachmentMeta[];
84
111
  /** Named per-run behavior profile supplied by interactive product surfaces. */
85
112
  behaviorMode?: RunBehaviorMode;
113
+ /** Per-run hard tool allowlist. An empty list exposes no tools. */
114
+ toolAllowlist?: readonly string[];
115
+ /** Per-run hard Skill allowlist. An empty list exposes no Skills. */
116
+ skillAllowlist?: readonly string[];
117
+ /** Keep a fresh Session in process memory only and omit it from Session pickers. */
118
+ ephemeral?: boolean;
86
119
  /**
87
120
  * Generic per-run parameters consumed by the active behavior profile
88
121
  * (createRunServices / buildVisibilityMeta / runtime-context injection).
@@ -10,3 +10,24 @@ export const QUICK_CHAT_RESTRICTED_PROFILE = {
10
10
  id: "quickChatRestricted",
11
11
  systemPromptAppend: QUICK_CHAT_RESTRICTED_SYSTEM_PROMPT,
12
12
  };
13
+ export const ISOLATED_TASK_BEHAVIOR_MODE = "isolatedTask";
14
+ export const ISOLATED_TASK_SYSTEM_PROMPT = `# Isolated Task Boundary
15
+
16
+ This is a bounded Task, not a continuation of any user conversation.
17
+ - Use only the explicit Task input, the visible tool surface, and the selected Skill when one is available.
18
+ - Do not infer or search for prior Session history, persistent memory, unrelated Skills, or other projects.
19
+ - Do not create sub-agents or start unrelated work.
20
+ - Finish the requested outcome, then return a concise result suitable for the owning application.`;
21
+ /** Minimal, process-local run profile used by host-owned Tasks. */
22
+ export const ISOLATED_TASK_PROFILE = {
23
+ id: ISOLATED_TASK_BEHAVIOR_MODE,
24
+ systemPromptAppend: ISOLATED_TASK_SYSTEM_PROMPT,
25
+ disableMcp: true,
26
+ disableInstructions: true,
27
+ disableMemoryContext: true,
28
+ disableCapabilityContext: true,
29
+ disableSourcesContext: true,
30
+ disableWorkspaceProfile: true,
31
+ disableHooks: true,
32
+ disableSessionTitle: true,
33
+ };
@@ -93,11 +93,17 @@ export async function resolveRunWorkspace(args) {
93
93
  configCwd: args.configCwd,
94
94
  processCwd: args.processCwd,
95
95
  });
96
- const profileState = resolveRunProfileState({
97
- sessionWorkspaceProfile,
98
- cwd,
99
- settings: args.settings,
100
- });
96
+ const profileState = profile?.disableWorkspaceProfile
97
+ ? {
98
+ workspaceProfile: undefined,
99
+ sessionProfileOverrides: undefined,
100
+ profileMemoryDir: undefined,
101
+ }
102
+ : resolveRunProfileState({
103
+ sessionWorkspaceProfile,
104
+ cwd,
105
+ settings: args.settings,
106
+ });
101
107
  return {
102
108
  ok: true,
103
109
  resolution: {
@@ -22,7 +22,7 @@ import { COMPLETE_GOAL_TOOL_NAME } from "../tool-system/builtin/complete-goal.js
22
22
  import { CANCEL_GOAL_TOOL_NAME } from "../tool-system/builtin/cancel-goal.js";
23
23
  import { redactSensitiveToolResultsInMessages, SENSITIVE_TOOL_RESULT_PLACEHOLDER, toolResultForDisplay, toolResultTranscriptText, toolResultsForDisplay, } from "../tool-system/tool-result-redaction.js";
24
24
  import { addTokenUsage, cacheHitRateFromUsage, cumulativeCacheHitRate, } from "../session/usage.js";
25
- import { createGoalBudgetTracker, recordGoalUsage, goalBudgetTerminationReason, applyGoalExtension, limitProximity, isSameGoalVersion, normalizeGoal, GOAL_DEFAULT_MAX_STOP_BLOCKS, } from "../goal/lifecycle.js";
25
+ import { createGoalBudgetTracker, recordGoalUsage, goalBudgetTerminationReason, applyGoalExtension, extendGoalLimit, limitProximity, isSameGoalVersion, normalizeGoal, GOAL_DEFAULT_MAX_STOP_BLOCKS, } from "../goal/lifecycle.js";
26
26
  /**
27
27
  * 把一个 ToolResult 映射成发给 LLM 的 tool_result ContentBlock。
28
28
  * 有 contentBlocks(view_image 的图片块)就原样用作 content;否则
@@ -148,6 +148,9 @@ export class TurnLoop {
148
148
  * effective limits.
149
149
  */
150
150
  extend(opts) {
151
+ const previousMaxTurns = this.config.maxTurns;
152
+ const previousTokenBudget = this.goalTracker?.goal.tokenBudget;
153
+ const previousTimeBudgetMs = this.goalTracker?.goal.timeBudgetMs;
151
154
  const elapsedMs = this.goalTracker ? Date.now() - this.goalTracker.startedAtMs : 0;
152
155
  const next = applyGoalExtension(this.config.maxTurns, this.goalTracker?.goal, this.goalTracker?.tokensUsed ?? 0, elapsedMs, opts);
153
156
  this.config = { ...this.config, maxTurns: next.maxTurns };
@@ -166,18 +169,16 @@ export class TurnLoop {
166
169
  // limit that actually bites, so an extend that only bumped maxTurns/budgets
167
170
  // couldn't keep it going. Resolve the current cap the same way the loop does.
168
171
  const curCap = this.config.maxStopBlocks ?? GOAL_DEFAULT_MAX_STOP_BLOCKS;
169
- const nextCap = typeof opts.addStopBlocks === "number" && opts.addStopBlocks > 0
170
- ? curCap + Math.floor(opts.addStopBlocks)
171
- : curCap;
172
+ const nextCap = extendGoalLimit(curCap, opts.addStopBlocks);
172
173
  this.config = { ...this.config, maxStopBlocks: nextCap };
173
174
  // ANY extension resets the consecutive stop-block streak: the user just
174
175
  // asked to keep going, so a goal that was repeatedly re-blocked shouldn't be
175
176
  // immediately re-capped. (Previously only addTurns reset it, leaving a
176
177
  // budget-only extension unable to un-stick a capped goal.)
177
- const extended = (opts.addTurns ?? 0) > 0 ||
178
- (opts.addStopBlocks ?? 0) > 0 ||
179
- (opts.addTokenBudget ?? 0) > 0 ||
180
- (opts.addTimeBudgetMs ?? 0) > 0;
178
+ const extended = next.maxTurns !== previousMaxTurns ||
179
+ nextCap !== curCap ||
180
+ next.tokenBudget !== previousTokenBudget ||
181
+ next.timeBudgetMs !== previousTimeBudgetMs;
181
182
  if (extended) {
182
183
  this.stopBlockCount = 0;
183
184
  // Let the next approach re-announce against the raised ceilings.
@@ -83,6 +83,8 @@ export type GoalLifecycleV1 = (GoalLifecycleBaseV1 & {
83
83
  };
84
84
  });
85
85
  export type GoalLifecyclePhase = GoalLifecycleV1["phase"];
86
+ /** Add a user-controlled positive integer delta without producing Infinity or an unsafe integer. */
87
+ export declare function extendGoalLimit(current: number, addition: unknown): number;
86
88
  /** Build one canonical lifecycle record from the compatibility GoalConfig view. */
87
89
  export declare function createGoalLifecycle(goal: GoalConfig, phase?: Exclude<GoalLifecyclePhase, "terminal" | "waiting">, nowMs?: number): GoalLifecycleV1;
88
90
  /** Compatibility view consumed by Engine/protocol while persistence uses the union. */
@@ -11,6 +11,25 @@
11
11
  * so an unattended goal run can't burn tokens or wall time without bound.
12
12
  */
13
13
  import { createHash, randomUUID } from "node:crypto";
14
+ function positiveSafeNumber(value) {
15
+ return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= Number.MAX_SAFE_INTEGER;
16
+ }
17
+ function positiveSafeInteger(value) {
18
+ if (!positiveSafeNumber(value))
19
+ return undefined;
20
+ const integer = Math.floor(value);
21
+ return integer >= 1 ? integer : undefined;
22
+ }
23
+ /** Add a user-controlled positive integer delta without producing Infinity or an unsafe integer. */
24
+ export function extendGoalLimit(current, addition) {
25
+ const delta = positiveSafeInteger(addition);
26
+ if (delta === undefined)
27
+ return current;
28
+ const safeCurrent = typeof current === "number" && Number.isFinite(current) && current >= 0
29
+ ? Math.min(Number.MAX_SAFE_INTEGER, Math.floor(current))
30
+ : Number.MAX_SAFE_INTEGER;
31
+ return Math.min(Number.MAX_SAFE_INTEGER, safeCurrent + delta);
32
+ }
14
33
  function goalLifecycleConfig(goal) {
15
34
  const { goalId: _goalId, revision: _revision, paused: _paused, ...config } = goal;
16
35
  return config;
@@ -22,7 +41,7 @@ export function createGoalLifecycle(goal, phase = goal.paused === true
22
41
  const base = {
23
42
  version: 1,
24
43
  goalId: goal.goalId ?? randomUUID(),
25
- revision: Math.max(1, Math.floor(goal.revision ?? 1)),
44
+ revision: positiveSafeInteger(goal.revision) ?? 1,
26
45
  config: goalLifecycleConfig(goal),
27
46
  updatedAtMs: nowMs,
28
47
  };
@@ -53,7 +72,7 @@ export function decodeGoalLifecycle(value) {
53
72
  if (typeof candidate.goalId !== "string" || candidate.goalId.length === 0)
54
73
  return undefined;
55
74
  if (typeof candidate.revision !== "number" ||
56
- !Number.isInteger(candidate.revision) ||
75
+ !Number.isSafeInteger(candidate.revision) ||
57
76
  candidate.revision < 1) {
58
77
  return undefined;
59
78
  }
@@ -80,17 +99,19 @@ export function decodeGoalLifecycle(value) {
80
99
  for (const key of ["tokenBudget", "timeBudgetMs", "maxTurns", "maxStopBlocks"]) {
81
100
  const field = config[key];
82
101
  if (field !== undefined &&
83
- (typeof field !== "number" || !Number.isFinite(field) || field <= 0)) {
102
+ !positiveSafeNumber(field)) {
84
103
  return undefined;
85
104
  }
86
105
  }
87
106
  for (const key of ["maxTurns", "maxStopBlocks"]) {
88
107
  const field = config[key];
89
- if (field !== undefined && !Number.isInteger(field))
108
+ if (field !== undefined && !Number.isSafeInteger(field))
90
109
  return undefined;
91
110
  }
92
111
  if (config.setAtMs !== undefined &&
93
- (typeof config.setAtMs !== "number" || !Number.isFinite(config.setAtMs) || config.setAtMs < 0)) {
112
+ (typeof config.setAtMs !== "number" ||
113
+ !Number.isSafeInteger(config.setAtMs) ||
114
+ config.setAtMs < 0)) {
94
115
  return undefined;
95
116
  }
96
117
  const phase = candidate.phase;
@@ -309,11 +330,12 @@ export const INTERACTIVE_DEFAULT_MAX_STOP_BLOCKS = 8;
309
330
  * Pure + injectable so engine and tests agree.
310
331
  */
311
332
  export function resolveMaxStopBlocks(configMaxStopBlocks, goal) {
312
- if (typeof configMaxStopBlocks === "number" && configMaxStopBlocks > 0) {
313
- return Math.floor(configMaxStopBlocks);
314
- }
315
- if (goal?.maxStopBlocks && goal.maxStopBlocks > 0)
316
- return goal.maxStopBlocks;
333
+ const configLimit = positiveSafeInteger(configMaxStopBlocks);
334
+ if (configLimit !== undefined)
335
+ return configLimit;
336
+ const goalLimit = positiveSafeInteger(goal?.maxStopBlocks);
337
+ if (goalLimit !== undefined)
338
+ return goalLimit;
317
339
  return goal ? GOAL_DEFAULT_MAX_STOP_BLOCKS : INTERACTIVE_DEFAULT_MAX_STOP_BLOCKS;
318
340
  }
319
341
  /**
@@ -340,24 +362,25 @@ export function normalizeGoal(raw) {
340
362
  const out = { objective };
341
363
  if (typeof obj.goalId === "string" && obj.goalId.trim())
342
364
  out.goalId = obj.goalId;
343
- if (typeof obj.revision === "number" && obj.revision > 0) {
344
- out.revision = Math.floor(obj.revision);
345
- }
365
+ const revision = positiveSafeInteger(obj.revision);
366
+ if (revision !== undefined)
367
+ out.revision = revision;
346
368
  if (obj.paused === true)
347
369
  out.paused = true;
348
- if (typeof obj.tokenBudget === "number" && obj.tokenBudget > 0)
370
+ if (positiveSafeNumber(obj.tokenBudget))
349
371
  out.tokenBudget = obj.tokenBudget;
350
- if (typeof obj.timeBudgetMs === "number" && obj.timeBudgetMs > 0)
372
+ if (positiveSafeNumber(obj.timeBudgetMs))
351
373
  out.timeBudgetMs = obj.timeBudgetMs;
352
- if (typeof obj.maxTurns === "number" && obj.maxTurns > 0)
353
- out.maxTurns = Math.floor(obj.maxTurns);
354
- if (typeof obj.maxStopBlocks === "number" && obj.maxStopBlocks > 0) {
355
- out.maxStopBlocks = Math.floor(obj.maxStopBlocks);
356
- }
374
+ const maxTurns = positiveSafeInteger(obj.maxTurns);
375
+ if (maxTurns !== undefined)
376
+ out.maxTurns = maxTurns;
377
+ const maxStopBlocks = positiveSafeInteger(obj.maxStopBlocks);
378
+ if (maxStopBlocks !== undefined)
379
+ out.maxStopBlocks = maxStopBlocks;
357
380
  // Preserve a positive goal-set timestamp so a resumed/inherited goal keeps
358
381
  // the anchor for relative deadlines. Non-positive/junk is dropped, matching
359
382
  // the budget fields (a bogus anchor is worse than none).
360
- if (typeof obj.setAtMs === "number" && obj.setAtMs > 0)
383
+ if (positiveSafeInteger(obj.setAtMs) !== undefined)
361
384
  out.setAtMs = obj.setAtMs;
362
385
  return out;
363
386
  }
@@ -391,10 +414,11 @@ export const INTERACTIVE_DEFAULT_MAX_TURNS = 100;
391
414
  * Pure + injectable so the engine and tests agree on the rule.
392
415
  */
393
416
  export function resolveMaxTurns(configMaxTurns, goal) {
394
- if (typeof configMaxTurns === "number" && configMaxTurns > 0)
395
- return configMaxTurns;
417
+ const configLimit = positiveSafeInteger(configMaxTurns);
418
+ if (configLimit !== undefined)
419
+ return configLimit;
396
420
  if (goal)
397
- return goal.maxTurns ?? GOAL_DEFAULT_MAX_TURNS;
421
+ return positiveSafeInteger(goal.maxTurns) ?? GOAL_DEFAULT_MAX_TURNS;
398
422
  return INTERACTIVE_DEFAULT_MAX_TURNS;
399
423
  }
400
424
  /** Warn when this many turns (or fewer) remain before maxTurns. */
@@ -436,19 +460,17 @@ export function limitProximity(turnCount, maxTurns, stopBlockCount, maxStopBlock
436
460
  */
437
461
  export function applyGoalExtension(currentMaxTurns, goal, tokensUsed, elapsedMs, ext) {
438
462
  let maxTurns = currentMaxTurns;
439
- if (typeof ext.addTurns === "number" && ext.addTurns > 0) {
440
- maxTurns += Math.floor(ext.addTurns);
441
- }
463
+ maxTurns = extendGoalLimit(maxTurns, ext.addTurns);
442
464
  let tokenBudget = goal?.tokenBudget;
443
465
  let timeBudgetMs = goal?.timeBudgetMs;
444
466
  if (goal) {
445
- if (typeof ext.addTokenBudget === "number" && ext.addTokenBudget > 0) {
446
- tokenBudget = (tokenBudget ?? tokensUsed) + Math.floor(ext.addTokenBudget);
467
+ if (positiveSafeInteger(ext.addTokenBudget) !== undefined) {
468
+ tokenBudget = extendGoalLimit(tokenBudget ?? tokensUsed, ext.addTokenBudget);
447
469
  }
448
- if (typeof ext.addTimeBudgetMs === "number" && ext.addTimeBudgetMs > 0) {
470
+ if (positiveSafeInteger(ext.addTimeBudgetMs) !== undefined) {
449
471
  // Seed an unset cap from elapsed time (not 0), mirroring the token branch,
450
472
  // so the new cap lands above current usage. Fixes B1.
451
- timeBudgetMs = (timeBudgetMs ?? elapsedMs) + Math.floor(ext.addTimeBudgetMs);
473
+ timeBudgetMs = extendGoalLimit(timeBudgetMs ?? elapsedMs, ext.addTimeBudgetMs);
452
474
  }
453
475
  }
454
476
  return { maxTurns, tokenBudget, timeBudgetMs };
@@ -459,8 +481,9 @@ export function createGoalBudgetTracker(goal, nowMs) {
459
481
  }
460
482
  /** Add this turn's token usage to the running total. */
461
483
  export function recordGoalUsage(tracker, turnTokens) {
462
- if (turnTokens > 0)
463
- tracker.tokensUsed += turnTokens;
484
+ if (positiveSafeInteger(turnTokens) !== undefined) {
485
+ tracker.tokensUsed = extendGoalLimit(tracker.tokensUsed, turnTokens);
486
+ }
464
487
  }
465
488
  /**
466
489
  * Has the run exceeded any configured budget? `nowMs` is injected so callers
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Public API exports.
5
5
  */
6
- export declare const VERSION = "0.8.8";
6
+ export declare const VERSION = "0.8.10";
7
7
  export type { Message, ContentBlock, ToolDefinition, ToolCall, ToolResult, RegisteredTool, TranscriptEvent, TranscriptEventType, SessionState, SessionKind, SessionWorkspace, SessionForkLineage, ContextUsageAnchor, SessionStatus, TokenUsage, CompiledInput, PermissionDecision, PermissionMode, PermissionRule, TurnPhase, TurnResult, TerminalReason, TurnCompletionKind, StreamEvent, StreamCallback, LLMConfig, ClientDefaults, LLMResponse, Settings, MCPServerConfig, } from "./types.js";
8
8
  export type { GoalConfig, GoalLifecycleConfig, GoalLifecyclePhase, GoalLifecycleTerminalReason, GoalLifecycleV1, } from "./goal/lifecycle.js";
9
9
  export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
@@ -66,7 +66,7 @@ export { Methods, ErrorCodes, type RpcMessage, type RunResult, type ForkSessionP
66
66
  export type * from "./protocol/mobile-remote-types.js";
67
67
  export { Transcript } from "./session/transcript.js";
68
68
  export { SessionManager, sessionMainRoot, codeShellHome, sessionsRoot, buildForkState, buildForkTranscript, type ForkSessionOptions, type ForkSessionResult, } from "./session/session-manager.js";
69
- export { FileHistory } from "./session/file-history.js";
69
+ export { FileHistory, type FileSnapshot, type RedoRecord, type TurnUndoPlan, } from "./session/file-history.js";
70
70
  export { latestUndoTarget, earliestSnapshotsPerFile, latestTurnUndoTargets, latestRedoTargets, } from "./session/undo-target.js";
71
71
  export { diffLines, renderDiffPreview, type DiffLine } from "./session/simple-diff.js";
72
72
  export { MemoryManager } from "./session/memory.js";
@@ -74,7 +74,6 @@ export type { MemoryEntry, MemoryOrigin, MemoryScope } from "./session/memory.js
74
74
  export type { RouteSessionMessageInput, SessionMessageRouter, SessionMessageTarget, SessionMessageToolService, } from "./session/session-message.js";
75
75
  export { runDreamConsolidation, type DreamConsolidationInput, type DreamConsolidationResult, } from "./services/dream-consolidation.js";
76
76
  export { authorize, refreshToken, generatePKCE, createHardenedOAuthFetch, type OAuthConfig, type OAuthTokens, type OAuthAuthorizeOptions, type OAuthRefreshOptions, type HardenedOAuthFetchOptions, } from "./services/oauth.js";
77
- export type { FileSnapshot, RedoRecord } from "./session/file-history.js";
78
77
  export { PromptComposer } from "./prompt/composer.js";
79
78
  export { SectionCache } from "./prompt/section-cache.js";
80
79
  export { scanInstructions, combineInstructions } from "./prompt/instruction-scanner.js";
@@ -12,6 +12,7 @@ export { execFileNoThrow } from "./utils/execFileNoThrow.js";
12
12
  export { findExecutable, resolveExecutable, setGitPathOverride, resolveGit, isGitAvailable, resolveGitPath, } from "./utils/exec.js";
13
13
  export { gte } from "./utils/semver.js";
14
14
  export { lock, lockSync, unlock, check } from "./utils/lockfile.js";
15
+ export { acquireFileLock, acquireLockOnPath, mutateJsonFile } from "./utils/file-mutex.js";
15
16
  export { logForDebugging } from "./utils/debug.js";
16
17
  export { isEnvTruthy, isEnvDefinedFalsy, getClaudeConfigHomeDir, isBareMode, parseEnvVars, shouldMaintainProjectWorkingDir, getAWSRegion, getDefaultVertexRegion, getVertexRegionForModel, } from "./utils/envUtils.js";
17
18
  export { startCapturingEarlyInput, stopCapturingEarlyInput, consumeEarlyInput, hasEarlyInput, seedEarlyInput, isCapturingEarlyInput, } from "./utils/earlyInput.js";
@@ -13,6 +13,7 @@ export { execFileNoThrow } from "./utils/execFileNoThrow.js";
13
13
  export { findExecutable, resolveExecutable, setGitPathOverride, resolveGit, isGitAvailable, resolveGitPath, } from "./utils/exec.js";
14
14
  export { gte } from "./utils/semver.js";
15
15
  export { lock, lockSync, unlock, check } from "./utils/lockfile.js";
16
+ export { acquireFileLock, acquireLockOnPath, mutateJsonFile } from "./utils/file-mutex.js";
16
17
  export { logForDebugging } from "./utils/debug.js";
17
18
  export { isEnvTruthy, isEnvDefinedFalsy, getClaudeConfigHomeDir, isBareMode, parseEnvVars, shouldMaintainProjectWorkingDir, getAWSRegion, getDefaultVertexRegion, getVertexRegionForModel, } from "./utils/envUtils.js";
18
19
  export { startCapturingEarlyInput, stopCapturingEarlyInput, consumeEarlyInput, hasEarlyInput, seedEarlyInput, isCapturingEarlyInput, } from "./utils/earlyInput.js";
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Public API exports.
5
5
  */
6
- export const VERSION = "0.8.8";
6
+ export const VERSION = "0.8.10";
7
7
  // ─── Exceptions ──────────────────────────────────────────────────
8
8
  export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
9
9
  // ─── Engine (primary API) ────────────────────────────────────────
@@ -55,7 +55,7 @@ export { Methods, ErrorCodes, } from "./protocol/types.js";
55
55
  // ─── Session ─────────────────────────────────────────────────────
56
56
  export { Transcript } from "./session/transcript.js";
57
57
  export { SessionManager, sessionMainRoot, codeShellHome, sessionsRoot, buildForkState, buildForkTranscript, } from "./session/session-manager.js";
58
- export { FileHistory } from "./session/file-history.js";
58
+ export { FileHistory, } from "./session/file-history.js";
59
59
  export { latestUndoTarget, earliestSnapshotsPerFile, latestTurnUndoTargets, latestRedoTargets, } from "./session/undo-target.js";
60
60
  export { diffLines, renderDiffPreview } from "./session/simple-diff.js";
61
61
  export { MemoryManager } from "./session/memory.js";
@@ -17,6 +17,8 @@ export interface CliLinkRunOptions {
17
17
  signal?: AbortSignal;
18
18
  timeoutMs: number;
19
19
  input?: string;
20
+ /** Keep the pipe alive for browser/device login CLIs that inspect stdin. */
21
+ keepStdinOpen?: boolean;
20
22
  }
21
23
  export type CliLinkCommandRunner = (providerId: CliLinkProviderId, command: string, args: string[], options: CliLinkRunOptions) => Promise<CliLinkCommandResult>;
22
24
  /** App-private CLI location. It never mutates the user's system PATH. */
package/dist/links/cli.js CHANGED
@@ -103,10 +103,15 @@ export const runCliLinkCommand = (providerId, command, args, options) => new Pro
103
103
  }
104
104
  resolve({ stdout: String(stdout), stderr: String(stderr) });
105
105
  });
106
- // All supported flows are deliberately non-interactive (browser/device
107
- // authorization or machine-readable API calls). Always close stdin so a
108
- // CLI cannot mistake the Electron pipe for pending JSON or a hidden prompt.
109
- child.stdin?.end(options.input ?? "");
106
+ if (options.input !== undefined)
107
+ child.stdin?.write(options.input);
108
+ // API calls consume a complete JSON body and must see EOF. Browser/device
109
+ // login is different: gh/glab prompt before opening the browser and some
110
+ // versions of Vercel first present a default login method. Feed one Enter
111
+ // below, but keep the pipe alive so those CLIs do not fail immediately with
112
+ // EOF while the browser authorization is still in progress.
113
+ if (!options.keepStdinOpen)
114
+ child.stdin?.end();
110
115
  });
111
116
  function parseJson(result, providerId) {
112
117
  try {
@@ -289,6 +294,8 @@ export async function connectCliLink(providerId, options, run = runCliLinkComman
289
294
  cwd: options.cwd,
290
295
  signal: options.signal,
291
296
  timeoutMs: 15 * 60_000,
297
+ input: "\n",
298
+ keepStdinOpen: true,
292
299
  });
293
300
  }
294
301
  catch (error) {
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * No remote source this version (design doc §5).
10
10
  */
11
- import { readFileSync, existsSync } from "node:fs";
11
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync } from "node:fs";
12
12
  import { join } from "node:path";
13
13
  import { userHome } from "../settings/manager.js";
14
14
  import { logger } from "../logging/logger.js";
@@ -16,6 +16,7 @@ import { BUILTIN_CATALOG } from "./builtin.js";
16
16
  import { userCatalogFileSchema } from "./types.js";
17
17
  export { BUILTIN_CATALOG } from "./builtin.js";
18
18
  export { saveCatalogEntry, deleteUserCatalogEntry } from "./save-entry.js";
19
+ const MAX_USER_CATALOG_BYTES = 4 * 1024 * 1024;
19
20
  /** Path to the user catalog file (source B). */
20
21
  export function userCatalogPath() {
21
22
  return join(userHome(), ".code-shell", "model-catalog.user.json");
@@ -26,10 +27,18 @@ export function userCatalogPath() {
26
27
  */
27
28
  export function loadUserCatalog() {
28
29
  const path = userCatalogPath();
29
- if (!existsSync(path))
30
- return [];
30
+ let descriptor;
31
31
  try {
32
- const parsed = JSON.parse(readFileSync(path, "utf-8"));
32
+ const metadata = lstatSync(path);
33
+ if (metadata.isSymbolicLink() || !metadata.isFile() || metadata.size > MAX_USER_CATALOG_BYTES) {
34
+ throw new Error("model catalog must be a bounded regular file");
35
+ }
36
+ descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
37
+ const opened = fstatSync(descriptor);
38
+ if (!opened.isFile() || opened.size > MAX_USER_CATALOG_BYTES) {
39
+ throw new Error("model catalog must be a bounded regular file");
40
+ }
41
+ const parsed = JSON.parse(readFileSync(descriptor, "utf-8"));
33
42
  const result = userCatalogFileSchema.safeParse(parsed);
34
43
  if (!result.success) {
35
44
  logger.warn("model_catalog_user_invalid", { path, issues: result.error.issues.length });
@@ -38,9 +47,15 @@ export function loadUserCatalog() {
38
47
  return result.data;
39
48
  }
40
49
  catch (err) {
50
+ if (err.code === "ENOENT")
51
+ return [];
41
52
  logger.warn("model_catalog_user_read_failed", { path, error: err.message });
42
53
  return [];
43
54
  }
55
+ finally {
56
+ if (descriptor !== undefined)
57
+ closeSync(descriptor);
58
+ }
44
59
  }
45
60
  /**
46
61
  * Merged catalog: built-in (A) ∪ user (B), deduped by `id` with user winning.