@cjhyy/code-shell-core 0.8.9 → 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.
- package/dist/automation/scheduler.js +49 -0
- package/dist/automation/store.d.ts +1 -1
- package/dist/automation/store.js +184 -10
- package/dist/cli/agent-server-stdio.js +7 -0
- package/dist/credentials/store.d.ts +14 -0
- package/dist/credentials/store.js +245 -42
- package/dist/engine/engine.js +45 -6
- package/dist/engine/file-history-hook.js +24 -5
- package/dist/engine/run-types.d.ts +9 -0
- package/dist/engine/turn-loop.js +9 -8
- package/dist/goal/lifecycle.d.ts +2 -0
- package/dist/goal/lifecycle.js +56 -33
- package/dist/index.d.ts +2 -3
- package/dist/index.internal.d.ts +1 -0
- package/dist/index.internal.js +1 -0
- package/dist/index.js +2 -2
- package/dist/links/cli.d.ts +2 -0
- package/dist/links/cli.js +11 -4
- package/dist/model-catalog/index.js +19 -4
- package/dist/model-catalog/save-entry.js +122 -61
- package/dist/model-catalog/types.js +27 -23
- package/dist/panel-apps/installer.js +27 -14
- package/dist/panel-apps/registry.js +60 -12
- package/dist/plugins/installedPlugins.d.ts +4 -0
- package/dist/plugins/installedPlugins.js +70 -30
- package/dist/plugins/installer/types.d.ts +12 -12
- package/dist/plugins/installer/update.js +37 -38
- package/dist/plugins/knownMarketplaces.d.ts +7 -3
- package/dist/plugins/knownMarketplaces.js +127 -23
- package/dist/plugins/pluginCatalog.js +18 -4
- package/dist/plugins/pluginHookApproval.js +56 -60
- package/dist/plugins/pluginMcpApproval.js +50 -52
- package/dist/profile/catalog-store.js +39 -4
- package/dist/profile/catalog.js +55 -15
- package/dist/profile/store.js +51 -21
- package/dist/protocol/chat-session-manager.d.ts +9 -0
- package/dist/protocol/chat-session-manager.js +13 -0
- package/dist/protocol/chat-session.d.ts +5 -0
- package/dist/protocol/chat-session.js +1 -0
- package/dist/protocol/server.d.ts +2 -0
- package/dist/protocol/server.js +75 -29
- package/dist/protocol/types.d.ts +8 -0
- package/dist/run/FileRunStore.d.ts +2 -0
- package/dist/run/FileRunStore.js +153 -18
- package/dist/run/Heartbeat.js +63 -4
- package/dist/services/auto-dream.js +39 -17
- package/dist/services/session-memory.js +107 -8
- package/dist/session/file-history.d.ts +63 -2
- package/dist/session/file-history.js +593 -86
- package/dist/session/session-manager.d.ts +1 -0
- package/dist/session/session-manager.js +52 -21
- package/dist/session/transcript.js +33 -3
- package/dist/session/undo-target.d.ts +15 -6
- package/dist/session/undo-target.js +26 -9
- package/dist/settings/manager.d.ts +22 -3
- package/dist/settings/manager.js +185 -50
- package/dist/settings/schema.d.ts +3 -3
- package/dist/sources/adapters/local-files.js +49 -4
- package/dist/sources/catalog.js +64 -18
- package/dist/sources/types.d.ts +3 -3
- package/dist/sources/types.js +7 -4
- package/dist/themes/installer.js +192 -28
- package/dist/tool-system/builtin/add-marketplace.js +21 -1
- package/dist/tool-system/builtin/cron.d.ts +2 -1
- package/dist/tool-system/builtin/cron.js +20 -6
- package/dist/tool-system/builtin/index.js +44 -0
- package/dist/tool-system/builtin/install-capability.d.ts +52 -0
- package/dist/tool-system/builtin/install-capability.js +1057 -0
- package/dist/tool-system/builtin/skill.js +3 -1
- package/dist/tool-system/executor.js +1 -0
- package/dist/tool-system/registry.js +5 -0
- package/dist/utils/file-mutex.d.ts +2 -0
- package/dist/utils/file-mutex.js +29 -4
- package/package.json +2 -1
package/dist/goal/lifecycle.js
CHANGED
|
@@ -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:
|
|
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.
|
|
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
|
-
|
|
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.
|
|
108
|
+
if (field !== undefined && !Number.isSafeInteger(field))
|
|
90
109
|
return undefined;
|
|
91
110
|
}
|
|
92
111
|
if (config.setAtMs !== undefined &&
|
|
93
|
-
(typeof config.setAtMs !== "number" ||
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
344
|
-
|
|
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 (
|
|
370
|
+
if (positiveSafeNumber(obj.tokenBudget))
|
|
349
371
|
out.tokenBudget = obj.tokenBudget;
|
|
350
|
-
if (
|
|
372
|
+
if (positiveSafeNumber(obj.timeBudgetMs))
|
|
351
373
|
out.timeBudgetMs = obj.timeBudgetMs;
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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 (
|
|
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
|
-
|
|
395
|
-
|
|
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
|
-
|
|
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 (
|
|
446
|
-
tokenBudget = (tokenBudget ?? tokensUsed
|
|
467
|
+
if (positiveSafeInteger(ext.addTokenBudget) !== undefined) {
|
|
468
|
+
tokenBudget = extendGoalLimit(tokenBudget ?? tokensUsed, ext.addTokenBudget);
|
|
447
469
|
}
|
|
448
|
-
if (
|
|
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
|
|
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
|
|
463
|
-
tracker.tokensUsed
|
|
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.
|
|
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";
|
package/dist/index.internal.d.ts
CHANGED
|
@@ -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";
|
package/dist/index.internal.js
CHANGED
|
@@ -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.
|
|
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";
|
package/dist/links/cli.d.ts
CHANGED
|
@@ -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
|
-
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
|
|
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 {
|
|
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
|
-
|
|
30
|
-
return [];
|
|
30
|
+
let descriptor;
|
|
31
31
|
try {
|
|
32
|
-
const
|
|
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.
|
|
@@ -6,10 +6,91 @@
|
|
|
6
6
|
* here (those go through the user's own Edit, by design).
|
|
7
7
|
* See docs/superpowers/specs/2026-06-15-unified-model-catalog-design.md §7.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { chmodSync, closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
10
11
|
import { dirname } from "node:path";
|
|
11
12
|
import { catalogEntrySchema, userCatalogFileSchema } from "./types.js";
|
|
12
13
|
import { upsertCatalogEntry } from "./upsert.js";
|
|
14
|
+
import { acquireFileLock } from "../utils/file-mutex.js";
|
|
15
|
+
const MAX_USER_CATALOG_BYTES = 4 * 1024 * 1024;
|
|
16
|
+
const BACKUP_STAMP_RE = /^[A-Za-z0-9._-]{1,128}$/;
|
|
17
|
+
function pathEntry(path) {
|
|
18
|
+
try {
|
|
19
|
+
return lstatSync(path);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error.code === "ENOENT")
|
|
23
|
+
return undefined;
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function readExistingCatalog(path) {
|
|
28
|
+
const metadata = pathEntry(path);
|
|
29
|
+
if (!metadata)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (metadata.isSymbolicLink() ||
|
|
32
|
+
!metadata.isFile() ||
|
|
33
|
+
metadata.size > MAX_USER_CATALOG_BYTES) {
|
|
34
|
+
throw new Error("catalog must be a bounded regular file");
|
|
35
|
+
}
|
|
36
|
+
const descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
37
|
+
try {
|
|
38
|
+
const opened = fstatSync(descriptor);
|
|
39
|
+
if (!opened.isFile() || opened.size > MAX_USER_CATALOG_BYTES) {
|
|
40
|
+
throw new Error("catalog must be a bounded regular file");
|
|
41
|
+
}
|
|
42
|
+
const raw = readFileSync(descriptor, "utf8");
|
|
43
|
+
try {
|
|
44
|
+
const parsed = userCatalogFileSchema.safeParse(JSON.parse(raw));
|
|
45
|
+
return { raw, entries: parsed.success ? parsed.data : [] };
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return { raw, entries: [] };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
closeSync(descriptor);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function writeBackup(path, raw) {
|
|
56
|
+
try {
|
|
57
|
+
writeFileSync(path, raw, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function writeCatalogAtomic(path, entries) {
|
|
65
|
+
const serialized = `${JSON.stringify(userCatalogFileSchema.parse(entries), null, 2)}\n`;
|
|
66
|
+
if (Buffer.byteLength(serialized, "utf8") > MAX_USER_CATALOG_BYTES) {
|
|
67
|
+
throw new Error("catalog exceeds its size limit");
|
|
68
|
+
}
|
|
69
|
+
const parent = dirname(path);
|
|
70
|
+
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
71
|
+
const parentInfo = lstatSync(parent);
|
|
72
|
+
if (parentInfo.isSymbolicLink() || !parentInfo.isDirectory()) {
|
|
73
|
+
throw new Error("catalog parent must be a real directory");
|
|
74
|
+
}
|
|
75
|
+
const target = pathEntry(path);
|
|
76
|
+
if (target && (target.isSymbolicLink() || !target.isFile())) {
|
|
77
|
+
throw new Error("catalog target must be a regular file");
|
|
78
|
+
}
|
|
79
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
80
|
+
try {
|
|
81
|
+
writeFileSync(temporary, serialized, {
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
mode: 0o600,
|
|
84
|
+
flag: "wx",
|
|
85
|
+
});
|
|
86
|
+
renameSync(temporary, path);
|
|
87
|
+
if (process.platform !== "win32")
|
|
88
|
+
chmodSync(path, 0o600);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
rmSync(temporary, { force: true });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
13
94
|
/**
|
|
14
95
|
* @param stamp caller-supplied unique suffix for the backup filename. Pass a
|
|
15
96
|
* timestamp/counter from the caller — core forbids Date.now() in some paths
|
|
@@ -22,47 +103,27 @@ export function saveCatalogEntry(entry, opts) {
|
|
|
22
103
|
return { ok: false, error: `invalid catalog entry: ${parsed.error.issues.map((i) => i.message).join("; ")}` };
|
|
23
104
|
}
|
|
24
105
|
const valid = parsed.data;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
let
|
|
28
|
-
if (existsSync(opts.path)) {
|
|
29
|
-
backup = `${opts.path}.bak-${opts.stamp}`;
|
|
30
|
-
try {
|
|
31
|
-
copyFileSync(opts.path, backup);
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
backup = undefined; // best-effort; don't abort the write on backup failure
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
const raw = JSON.parse(readFileSync(opts.path, "utf-8"));
|
|
38
|
-
const safe = userCatalogFileSchema.safeParse(raw);
|
|
39
|
-
current = safe.success ? safe.data : [];
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
current = []; // corrupt → start fresh (original preserved in backup)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
const action = current.some((e) => e.id === valid.id) ? "updated" : "added";
|
|
46
|
-
const next = upsertCatalogEntry(current, valid);
|
|
47
|
-
// Ensure the parent dir exists — a first-ever catalog write on a machine
|
|
48
|
-
// whose ~/.code-shell hasn't been created yet would otherwise throw ENOENT
|
|
49
|
-
// (the agent-facing tool wants a clean {ok:false} or a real write, not a crash).
|
|
106
|
+
if (!BACKUP_STAMP_RE.test(opts.stamp))
|
|
107
|
+
return { ok: false, error: "invalid backup stamp" };
|
|
108
|
+
let release;
|
|
50
109
|
try {
|
|
51
|
-
|
|
110
|
+
release = acquireFileLock(opts.path);
|
|
111
|
+
const existing = readExistingCatalog(opts.path);
|
|
112
|
+
let backup = existing ? `${opts.path}.bak-${opts.stamp}` : undefined;
|
|
113
|
+
if (existing && !writeBackup(backup, existing.raw))
|
|
114
|
+
backup = undefined;
|
|
115
|
+
const action = existing?.entries.some((e) => e.id === valid.id)
|
|
116
|
+
? "updated"
|
|
117
|
+
: "added";
|
|
118
|
+
writeCatalogAtomic(opts.path, upsertCatalogEntry(existing?.entries ?? [], valid));
|
|
119
|
+
return { ok: true, action, backup };
|
|
52
120
|
}
|
|
53
121
|
catch (e) {
|
|
54
|
-
return { ok: false, error: `could not
|
|
122
|
+
return { ok: false, error: `could not write catalog: ${e instanceof Error ? e.message : String(e)}` };
|
|
55
123
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
catch (e) {
|
|
60
|
-
// IO error (perms / disk full / bad path): return a clean {ok:false} with the
|
|
61
|
-
// backup filename preserved, never let the throw escape past the tool's
|
|
62
|
-
// expected result shape (the original file is intact — we only upsert-wrote).
|
|
63
|
-
return { ok: false, error: `could not write catalog: ${e instanceof Error ? e.message : String(e)}`, backup };
|
|
124
|
+
finally {
|
|
125
|
+
release?.();
|
|
64
126
|
}
|
|
65
|
-
return { ok: true, action, backup };
|
|
66
127
|
}
|
|
67
128
|
/**
|
|
68
129
|
* Remove the entry with `id` from the user catalog file. Mirrors saveCatalogEntry's
|
|
@@ -72,33 +133,33 @@ export function saveCatalogEntry(entry, opts) {
|
|
|
72
133
|
* built-in version ("reset" semantics).
|
|
73
134
|
*/
|
|
74
135
|
export function deleteUserCatalogEntry(id, opts) {
|
|
75
|
-
if (!
|
|
76
|
-
return { ok:
|
|
77
|
-
let backup = `${opts.path}.bak-${opts.stamp}`;
|
|
78
|
-
try {
|
|
79
|
-
copyFileSync(opts.path, backup);
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
backup = undefined;
|
|
136
|
+
if (!BACKUP_STAMP_RE.test(opts.stamp)) {
|
|
137
|
+
return { ok: false, removed: false, error: "invalid backup stamp" };
|
|
83
138
|
}
|
|
84
|
-
let
|
|
139
|
+
let release;
|
|
85
140
|
try {
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
141
|
+
release = acquireFileLock(opts.path);
|
|
142
|
+
const existing = readExistingCatalog(opts.path);
|
|
143
|
+
if (!existing)
|
|
144
|
+
return { ok: true, removed: false };
|
|
145
|
+
let backup = `${opts.path}.bak-${opts.stamp}`;
|
|
146
|
+
if (!writeBackup(backup, existing.raw))
|
|
147
|
+
backup = undefined;
|
|
148
|
+
const next = existing.entries.filter((e) => e.id !== id);
|
|
149
|
+
const removed = next.length !== existing.entries.length;
|
|
150
|
+
if (!removed)
|
|
151
|
+
return { ok: true, removed: false, backup };
|
|
152
|
+
writeCatalogAtomic(opts.path, next);
|
|
153
|
+
return { ok: true, removed: true, backup };
|
|
99
154
|
}
|
|
100
155
|
catch (e) {
|
|
101
|
-
return {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
removed: false,
|
|
159
|
+
error: `could not write catalog: ${e instanceof Error ? e.message : String(e)}`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
release?.();
|
|
102
164
|
}
|
|
103
|
-
return { ok: true, removed: true, backup };
|
|
104
165
|
}
|