@cjhyy/code-shell-core 0.8.12 → 0.8.20
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/cli/agent-server-stdio.js +4 -3
- package/dist/engine/engine.js +24 -11
- package/dist/engine/run-session-open.js +1 -1
- package/dist/engine/run-types.d.ts +2 -0
- package/dist/engine/turn-loop.js +17 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.internal.d.ts +1 -0
- package/dist/index.internal.js +1 -0
- package/dist/index.js +2 -2
- package/dist/llm/client-base.d.ts +2 -1
- package/dist/llm/client-base.js +3 -1
- package/dist/llm/providers/anthropic.js +24 -25
- package/dist/llm/providers/openai.d.ts +4 -1
- package/dist/llm/providers/openai.js +76 -13
- package/dist/panel-apps/index.d.ts +1 -1
- package/dist/panel-apps/index.js +1 -1
- package/dist/panel-apps/installer.d.ts +29 -0
- package/dist/panel-apps/installer.js +124 -15
- package/dist/plugins/pluginCatalog.d.ts +6 -0
- package/dist/plugins/pluginCatalog.js +7 -2
- package/dist/plugins/pluginContent.d.ts +1 -1
- package/dist/plugins/pluginContent.js +2 -10
- package/dist/protocol/chat-session.d.ts +2 -0
- package/dist/protocol/server.js +7 -0
- package/dist/protocol/types.d.ts +2 -0
- package/dist/session/session-manager.d.ts +7 -0
- package/dist/session/session-manager.js +19 -6
- package/dist/session/transcript.d.ts +9 -0
- package/dist/session/transcript.js +81 -0
- package/dist/settings/manager.d.ts +12 -0
- package/dist/settings/manager.js +31 -0
- package/dist/tool-system/builtin/configure-model-connection.d.ts +36 -0
- package/dist/tool-system/builtin/configure-model-connection.js +396 -0
- package/dist/tool-system/builtin/edit-model-catalog.d.ts +4 -6
- package/dist/tool-system/builtin/edit-model-catalog.js +5 -8
- package/dist/tool-system/builtin/index.js +14 -0
- package/dist/tool-system/builtin/install-capability.js +22 -8
- package/dist/tool-system/builtin/settings-changed.d.ts +9 -0
- package/dist/tool-system/builtin/settings-changed.js +18 -0
- package/dist/tool-system/context.d.ts +8 -2
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
|
@@ -16,6 +16,9 @@ const MAX_FILE_BYTES = 16 * 1024 * 1024;
|
|
|
16
16
|
const MAX_DEPTH = 16;
|
|
17
17
|
const MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
18
18
|
const MAX_AGENT_SKILL_BYTES = 256 * 1024;
|
|
19
|
+
const MAX_PANEL_DISCOVERY_DEPTH = 4;
|
|
20
|
+
const MAX_PANEL_DISCOVERY_DIRECTORIES = 512;
|
|
21
|
+
const MAX_PANEL_DISCOVERY_RESULTS = 16;
|
|
19
22
|
const REVIEWED_GIT_SNAPSHOT_TTL_MS = 10 * 60 * 1000;
|
|
20
23
|
const MAX_REVIEWED_GIT_SNAPSHOTS = 8;
|
|
21
24
|
const ALLOWED_ASSET_EXTENSIONS = new Set([
|
|
@@ -61,9 +64,14 @@ function normalizeGitPanelAppSource(input) {
|
|
|
61
64
|
if (!raw || raw.length > MAX_SOURCE_PATH || raw.includes("\0")) {
|
|
62
65
|
throw new PanelAppInstallError("GitHub repository URL is invalid");
|
|
63
66
|
}
|
|
67
|
+
const urlText = /^[A-Za-z0-9_.-]{1,100}\/[A-Za-z0-9_.-]{1,100}(?:\.git)?\/?$/.test(raw)
|
|
68
|
+
? `https://github.com/${raw}`
|
|
69
|
+
: /^github\.com\//i.test(raw)
|
|
70
|
+
? `https://${raw}`
|
|
71
|
+
: raw;
|
|
64
72
|
let parsed;
|
|
65
73
|
try {
|
|
66
|
-
parsed = new URL(
|
|
74
|
+
parsed = new URL(urlText);
|
|
67
75
|
}
|
|
68
76
|
catch {
|
|
69
77
|
throw new PanelAppInstallError("GitHub repository URL is invalid");
|
|
@@ -135,19 +143,52 @@ function normalizedGitSourceKey(input) {
|
|
|
135
143
|
function looksLikePanelAppRoot(directory) {
|
|
136
144
|
return existsSync(join(directory, PANEL_APP_MANIFEST_FILE));
|
|
137
145
|
}
|
|
146
|
+
async function discoverPanelAppRoots(directory) {
|
|
147
|
+
const found = [];
|
|
148
|
+
const pending = [{ directory, depth: 0 }];
|
|
149
|
+
let visited = 0;
|
|
150
|
+
while (pending.length > 0 &&
|
|
151
|
+
visited < MAX_PANEL_DISCOVERY_DIRECTORIES &&
|
|
152
|
+
found.length < MAX_PANEL_DISCOVERY_RESULTS) {
|
|
153
|
+
const current = pending.shift();
|
|
154
|
+
visited += 1;
|
|
155
|
+
if (looksLikePanelAppRoot(current.directory)) {
|
|
156
|
+
found.push(current.directory);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (current.depth >= MAX_PANEL_DISCOVERY_DEPTH)
|
|
160
|
+
continue;
|
|
161
|
+
const entries = await readdir(current.directory, { withFileTypes: true });
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
pending.push({ directory: join(current.directory, entry.name), depth: current.depth + 1 });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return found;
|
|
170
|
+
}
|
|
138
171
|
async function findPanelAppRoot(directory) {
|
|
139
172
|
if (looksLikePanelAppRoot(directory))
|
|
140
173
|
return directory;
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
174
|
+
const found = await discoverPanelAppRoots(directory);
|
|
175
|
+
if (found.length === 1)
|
|
176
|
+
return found[0];
|
|
177
|
+
if (found.length > 1) {
|
|
178
|
+
const candidates = found
|
|
179
|
+
.map((root) => relative(directory, root).split(sep).join(posix.sep))
|
|
180
|
+
.sort()
|
|
181
|
+
.join(", ");
|
|
182
|
+
throw new PanelAppInstallError(`multiple Panel Apps found; choose an app subdirectory: ${candidates}`);
|
|
183
|
+
}
|
|
184
|
+
throw new PanelAppInstallError(`no Panel App found (expected ${PANEL_APP_MANIFEST_FILE}); ` +
|
|
185
|
+
"for a monorepo, provide the app subdirectory or a GitHub /tree/<ref>/<path> URL");
|
|
149
186
|
}
|
|
150
|
-
|
|
187
|
+
function joinedPanelAppSubdirectory(base, nested) {
|
|
188
|
+
const parts = [base, nested].filter((part) => Boolean(part));
|
|
189
|
+
return parts.length > 0 ? parts.join("/") : undefined;
|
|
190
|
+
}
|
|
191
|
+
async function openGitPanelAppArchive(input) {
|
|
151
192
|
const source = normalizeGitPanelAppSource(input);
|
|
152
193
|
const temporaryRoot = await mkdtemp(join(tmpdir(), "cs-panel-app-git-"));
|
|
153
194
|
try {
|
|
@@ -157,18 +198,86 @@ async function openGitPanelAppSource(input) {
|
|
|
157
198
|
await downloadGitHubPanelAppArchive(source, archivePath);
|
|
158
199
|
await extractZipSubdirectory(archivePath, extractedRoot, source.subdir);
|
|
159
200
|
await rm(archivePath, { force: true });
|
|
201
|
+
return { source, extractedRoot, temporaryRoot };
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
await rm(temporaryRoot, { recursive: true, force: true }).catch(() => undefined);
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function openGitPanelAppSource(input) {
|
|
209
|
+
const opened = await openGitPanelAppArchive(input);
|
|
210
|
+
try {
|
|
160
211
|
return {
|
|
161
|
-
source,
|
|
162
|
-
sourceKey: JSON.stringify(source),
|
|
163
|
-
sourceRoot: await findPanelAppRoot(extractedRoot),
|
|
164
|
-
temporaryRoot,
|
|
212
|
+
source: opened.source,
|
|
213
|
+
sourceKey: JSON.stringify(opened.source),
|
|
214
|
+
sourceRoot: await findPanelAppRoot(opened.extractedRoot),
|
|
215
|
+
temporaryRoot: opened.temporaryRoot,
|
|
165
216
|
};
|
|
166
217
|
}
|
|
167
218
|
catch (error) {
|
|
168
|
-
await rm(temporaryRoot, { recursive: true, force: true }).catch(() => undefined);
|
|
219
|
+
await rm(opened.temporaryRoot, { recursive: true, force: true }).catch(() => undefined);
|
|
169
220
|
throw error;
|
|
170
221
|
}
|
|
171
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Download a public GitHub repository once and enumerate every independently
|
|
225
|
+
* installable Panel App beneath it. Discovery is read-only: selecting a result
|
|
226
|
+
* still runs the normal full review and digest-bound install flow.
|
|
227
|
+
*/
|
|
228
|
+
export async function discoverGitPanelApps(input) {
|
|
229
|
+
const opened = await openGitPanelAppArchive(input);
|
|
230
|
+
try {
|
|
231
|
+
const roots = await discoverPanelAppRoots(opened.extractedRoot);
|
|
232
|
+
if (roots.length === 0) {
|
|
233
|
+
throw new PanelAppInstallError(`no Panel App found (expected ${PANEL_APP_MANIFEST_FILE}); ` +
|
|
234
|
+
"check the repository, branch, or optional search subdirectory");
|
|
235
|
+
}
|
|
236
|
+
const panels = [];
|
|
237
|
+
const issues = [];
|
|
238
|
+
for (const root of roots) {
|
|
239
|
+
const nested = relative(opened.extractedRoot, root).split(sep).join(posix.sep);
|
|
240
|
+
const subdir = joinedPanelAppSubdirectory(opened.source.subdir, nested);
|
|
241
|
+
const label = subdir ?? ".";
|
|
242
|
+
try {
|
|
243
|
+
const inspected = await inspectPanelAppSource(root);
|
|
244
|
+
panels.push({
|
|
245
|
+
subdir: label,
|
|
246
|
+
source: {
|
|
247
|
+
kind: "git",
|
|
248
|
+
url: opened.source.url,
|
|
249
|
+
...(opened.source.ref ? { ref: opened.source.ref } : {}),
|
|
250
|
+
...(subdir ? { subdir } : {}),
|
|
251
|
+
},
|
|
252
|
+
id: inspected.manifest.id,
|
|
253
|
+
version: inspected.manifest.version,
|
|
254
|
+
title: inspected.manifest.title,
|
|
255
|
+
...(inspected.manifest.description
|
|
256
|
+
? { description: inspected.manifest.description }
|
|
257
|
+
: {}),
|
|
258
|
+
icon: inspected.manifest.icon,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
issues.push({
|
|
263
|
+
subdir: label,
|
|
264
|
+
error: error instanceof Error ? error.message : String(error),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
panels.sort((left, right) => left.subdir.localeCompare(right.subdir));
|
|
269
|
+
issues.sort((left, right) => left.subdir.localeCompare(right.subdir));
|
|
270
|
+
if (panels.length === 0) {
|
|
271
|
+
throw new PanelAppInstallError(`found ${issues.length} Panel App manifest(s), but none passed validation: ${issues
|
|
272
|
+
.map((issue) => `${issue.subdir}: ${issue.error}`)
|
|
273
|
+
.join("; ")}`);
|
|
274
|
+
}
|
|
275
|
+
return { source: opened.source, panels, issues };
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
await rm(opened.temporaryRoot, { recursive: true, force: true }).catch(() => undefined);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
172
281
|
async function disposeReviewedGitSnapshot(snapshot) {
|
|
173
282
|
clearTimeout(snapshot.expiryTimer);
|
|
174
283
|
await rm(snapshot.temporaryRoot, { recursive: true, force: true }).catch(() => undefined);
|
|
@@ -33,6 +33,12 @@ export interface PluginAutomationTemplateContribution {
|
|
|
33
33
|
* instantiated later. Install paths and timestamps are intentionally excluded.
|
|
34
34
|
*/
|
|
35
35
|
export declare function pluginAutomationTemplateRevision(installKey: string, template: PluginAutomationTemplate): string;
|
|
36
|
+
/**
|
|
37
|
+
* Read an installed plugin manifest without following links or accepting an
|
|
38
|
+
* unbounded/non-regular file. Shared by the runtime catalog and UI inventory so
|
|
39
|
+
* those views cannot disagree about which manifest bytes are trusted.
|
|
40
|
+
*/
|
|
41
|
+
export declare function readCatalogPluginManifest(installPath: string): CanonicalPluginManifestData | null;
|
|
36
42
|
/**
|
|
37
43
|
* Load the installed plugin catalog at the core boundary.
|
|
38
44
|
*
|
|
@@ -19,7 +19,12 @@ export function pluginAutomationTemplateRevision(installKey, template) {
|
|
|
19
19
|
.update(JSON.stringify(template))
|
|
20
20
|
.digest("hex");
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Read an installed plugin manifest without following links or accepting an
|
|
24
|
+
* unbounded/non-regular file. Shared by the runtime catalog and UI inventory so
|
|
25
|
+
* those views cannot disagree about which manifest bytes are trusted.
|
|
26
|
+
*/
|
|
27
|
+
export function readCatalogPluginManifest(installPath) {
|
|
23
28
|
const file = join(installPath, CANONICAL_PLUGIN_MANIFEST_FILE);
|
|
24
29
|
let descriptor;
|
|
25
30
|
try {
|
|
@@ -69,7 +74,7 @@ export function loadPluginCatalog(options = {}) {
|
|
|
69
74
|
const installPath = resolveSafePluginPath(entry.installPath, root);
|
|
70
75
|
if (!installPath)
|
|
71
76
|
continue;
|
|
72
|
-
const manifest =
|
|
77
|
+
const manifest = readCatalogPluginManifest(installPath);
|
|
73
78
|
const identity = identityFromInstallKey(installKey, manifest);
|
|
74
79
|
catalog.push({
|
|
75
80
|
installKey,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type PluginHookEntry } from "./loadPluginHooks.js";
|
|
9
9
|
import { type PluginHookReview } from "./pluginHookApproval.js";
|
|
10
|
-
import {
|
|
10
|
+
import type { PluginAutomationTemplate } from "./installer/types.js";
|
|
11
11
|
export type PluginAutomationTemplateDescriptor = PluginAutomationTemplate & {
|
|
12
12
|
revision: string;
|
|
13
13
|
};
|
|
@@ -11,8 +11,7 @@ import { parseFrontmatter } from "../skills/frontmatter.js";
|
|
|
11
11
|
import { listPluginHooks } from "./loadPluginHooks.js";
|
|
12
12
|
import { reviewPluginHooks } from "./pluginHookApproval.js";
|
|
13
13
|
import { readPluginMcp } from "./installer/loadPluginMcp.js";
|
|
14
|
-
import {
|
|
15
|
-
import { pluginAutomationTemplateRevision } from "./pluginCatalog.js";
|
|
14
|
+
import { pluginAutomationTemplateRevision, readCatalogPluginManifest } from "./pluginCatalog.js";
|
|
16
15
|
function resolveContainedPath(root, candidate) {
|
|
17
16
|
try {
|
|
18
17
|
const realRoot = realpathSync(root);
|
|
@@ -117,14 +116,7 @@ export function describePluginContent(pluginName, installPath, installKey) {
|
|
|
117
116
|
return undefined;
|
|
118
117
|
}
|
|
119
118
|
})();
|
|
120
|
-
const manifest = (
|
|
121
|
-
try {
|
|
122
|
-
return CanonicalPluginManifest.parse(JSON.parse(readFileSync(join(installPath, CANONICAL_PLUGIN_MANIFEST_FILE), "utf-8")));
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
})();
|
|
119
|
+
const manifest = readCatalogPluginManifest(installPath);
|
|
128
120
|
return {
|
|
129
121
|
skills: listSkills(installPath),
|
|
130
122
|
commands: listMdNames(installPath, "commands"),
|
|
@@ -37,6 +37,8 @@ export interface TurnOpts {
|
|
|
37
37
|
archiveBeforeCurrentTurn?: {
|
|
38
38
|
fromClientMessageId?: string;
|
|
39
39
|
segmentId?: string;
|
|
40
|
+
/** Host-authored replacement for the archived span; skips the summarizer. */
|
|
41
|
+
summary?: string;
|
|
40
42
|
};
|
|
41
43
|
/** Structured input attachments for this turn. */
|
|
42
44
|
attachments?: InputAttachmentMeta[];
|
package/dist/protocol/server.js
CHANGED
|
@@ -85,6 +85,13 @@ function runInputError(params) {
|
|
|
85
85
|
return `archiveBeforeCurrentTurn.${field} must be a bounded non-empty string`;
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
+
if (archive.summary !== undefined &&
|
|
89
|
+
(typeof archive.summary !== "string" ||
|
|
90
|
+
archive.summary.trim().length === 0 ||
|
|
91
|
+
archive.summary.length > 4_000 ||
|
|
92
|
+
/[\u0000]/u.test(archive.summary))) {
|
|
93
|
+
return "archiveBeforeCurrentTurn.summary must be a non-empty string up to 4000 characters";
|
|
94
|
+
}
|
|
88
95
|
}
|
|
89
96
|
if (params.behaviorMode !== undefined &&
|
|
90
97
|
(typeof params.behaviorMode !== "string" || params.behaviorMode.length === 0)) {
|
package/dist/protocol/types.d.ts
CHANGED
|
@@ -87,6 +87,8 @@ export interface RunParams {
|
|
|
87
87
|
archiveBeforeCurrentTurn?: {
|
|
88
88
|
fromClientMessageId?: string;
|
|
89
89
|
segmentId?: string;
|
|
90
|
+
/** Host-authored replacement for the archived span; skips the summarizer. */
|
|
91
|
+
summary?: string;
|
|
90
92
|
};
|
|
91
93
|
/** Desktop host ownership generation for process-local Quick Chat runs. */
|
|
92
94
|
quickChatClaimId?: string;
|
|
@@ -221,6 +221,13 @@ export declare class SessionManager {
|
|
|
221
221
|
state: SessionState;
|
|
222
222
|
} | undefined;
|
|
223
223
|
resume(sessionId: string): SessionBundle;
|
|
224
|
+
/**
|
|
225
|
+
* Resume a model run with a bounded active replay for large Pet transcripts
|
|
226
|
+
* that already contain a full-history archive boundary. Audit/detail callers
|
|
227
|
+
* continue to use resume() and receive every persisted event.
|
|
228
|
+
*/
|
|
229
|
+
resumeForRun(sessionId: string): SessionBundle;
|
|
230
|
+
private resumeWithTranscriptMode;
|
|
224
231
|
/**
|
|
225
232
|
* Return the exact persistence directory owned by this manager.
|
|
226
233
|
*
|
|
@@ -1001,6 +1001,17 @@ export class SessionManager {
|
|
|
1001
1001
|
return undefined;
|
|
1002
1002
|
}
|
|
1003
1003
|
resume(sessionId) {
|
|
1004
|
+
return this.resumeWithTranscriptMode(sessionId, false);
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Resume a model run with a bounded active replay for large Pet transcripts
|
|
1008
|
+
* that already contain a full-history archive boundary. Audit/detail callers
|
|
1009
|
+
* continue to use resume() and receive every persisted event.
|
|
1010
|
+
*/
|
|
1011
|
+
resumeForRun(sessionId) {
|
|
1012
|
+
return this.resumeWithTranscriptMode(sessionId, true);
|
|
1013
|
+
}
|
|
1014
|
+
resumeWithTranscriptMode(sessionId, contextOnly) {
|
|
1004
1015
|
assertSafeSessionId(sessionId);
|
|
1005
1016
|
const processLocal = this.processLocalBundle(sessionId);
|
|
1006
1017
|
if (processLocal) {
|
|
@@ -1037,8 +1048,10 @@ export class SessionManager {
|
|
|
1037
1048
|
chmodSync(sessionDir, 0o700);
|
|
1038
1049
|
chmodSync(stateFile, 0o600);
|
|
1039
1050
|
}
|
|
1040
|
-
const transcript = Transcript.loadFromFile(transcriptFile);
|
|
1041
1051
|
state.kind = normalizedSessionKind(state.kind);
|
|
1052
|
+
const transcript = contextOnly && state.kind === "pet"
|
|
1053
|
+
? Transcript.loadContextFromFile(transcriptFile)
|
|
1054
|
+
: Transcript.loadFromFile(transcriptFile);
|
|
1042
1055
|
state.status = "active";
|
|
1043
1056
|
delete state.lastCompletionKind;
|
|
1044
1057
|
Object.assign(state, normalizeCumulativeUsageCounters(state, state.tokenUsage));
|
|
@@ -1697,11 +1710,11 @@ export class SessionManager {
|
|
|
1697
1710
|
list(limit = 20, opts) {
|
|
1698
1711
|
if (!existsSync(this.sessionsDir))
|
|
1699
1712
|
return [];
|
|
1700
|
-
//
|
|
1701
|
-
//
|
|
1702
|
-
//
|
|
1703
|
-
//
|
|
1704
|
-
const excludeKinds = opts?.excludeKinds ?? [
|
|
1713
|
+
// Core is domain-agnostic: only the composition root knows which
|
|
1714
|
+
// extension-owned kinds belong outside its generic list. AgentServer passes
|
|
1715
|
+
// the resolved hidden-kind union explicitly; direct SDK callers see every
|
|
1716
|
+
// kind unless they choose exclusions themselves.
|
|
1717
|
+
const excludeKinds = opts?.excludeKinds ?? [];
|
|
1705
1718
|
const dirs = readdirSync(this.sessionsDir, { withFileTypes: true })
|
|
1706
1719
|
.filter((d) => d.isDirectory() && !d.name.startsWith(".pending-") && !d.name.startsWith("qchat-"))
|
|
1707
1720
|
.map((d) => d.name);
|
|
@@ -184,6 +184,15 @@ export declare class Transcript {
|
|
|
184
184
|
*/
|
|
185
185
|
static selectContextRange(events: readonly TranscriptEvent[], range: ContextEventRange): SelectedContextRange;
|
|
186
186
|
static loadFromFile(filePath: string): Transcript;
|
|
187
|
+
/**
|
|
188
|
+
* Load the active replay for a model run while leaving the append-only audit
|
|
189
|
+
* transcript untouched on disk. A host-authored from-less range archive
|
|
190
|
+
* replaces everything before its `to` anchor, so a large Mimi transcript can
|
|
191
|
+
* retain only that boundary and the live tail in memory. If the boundary is
|
|
192
|
+
* absent, too old, malformed, or not self-contained in the bounded tail, we
|
|
193
|
+
* fail open to the full loader.
|
|
194
|
+
*/
|
|
195
|
+
static loadContextFromFile(filePath: string, maxTailBytes?: number): Transcript;
|
|
187
196
|
private loadEvents;
|
|
188
197
|
}
|
|
189
198
|
export {};
|
|
@@ -6,6 +6,7 @@ import { appendFileSync, chmodSync, closeSync, existsSync, fchmodSync, fstatSync
|
|
|
6
6
|
import { dirname } from "node:path";
|
|
7
7
|
import { nanoid } from "nanoid";
|
|
8
8
|
import { logger } from "../logging/logger.js";
|
|
9
|
+
const DEFAULT_CONTEXT_TAIL_SCAN_BYTES = 32 * 1024 * 1024;
|
|
9
10
|
function appendTranscriptLine(filePath, data) {
|
|
10
11
|
// Use one append-mode descriptor so concurrent OS writers cannot overwrite
|
|
11
12
|
// one another. Also repair the record boundary after a crash-torn final line;
|
|
@@ -678,6 +679,86 @@ export class Transcript {
|
|
|
678
679
|
transcript.repairToolResultPairs();
|
|
679
680
|
return transcript;
|
|
680
681
|
}
|
|
682
|
+
/**
|
|
683
|
+
* Load the active replay for a model run while leaving the append-only audit
|
|
684
|
+
* transcript untouched on disk. A host-authored from-less range archive
|
|
685
|
+
* replaces everything before its `to` anchor, so a large Mimi transcript can
|
|
686
|
+
* retain only that boundary and the live tail in memory. If the boundary is
|
|
687
|
+
* absent, too old, malformed, or not self-contained in the bounded tail, we
|
|
688
|
+
* fail open to the full loader.
|
|
689
|
+
*/
|
|
690
|
+
static loadContextFromFile(filePath, maxTailBytes = DEFAULT_CONTEXT_TAIL_SCAN_BYTES) {
|
|
691
|
+
if (!existsSync(filePath))
|
|
692
|
+
return new Transcript(filePath);
|
|
693
|
+
let tail;
|
|
694
|
+
try {
|
|
695
|
+
const fd = openSync(filePath, "r");
|
|
696
|
+
try {
|
|
697
|
+
const fileSize = fstatSync(fd).size;
|
|
698
|
+
if (fileSize <= maxTailBytes)
|
|
699
|
+
return Transcript.loadFromFile(filePath);
|
|
700
|
+
const length = Math.min(fileSize, Math.max(1, maxTailBytes));
|
|
701
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
702
|
+
const bytesRead = readSync(fd, buffer, 0, length, fileSize - length);
|
|
703
|
+
let window = buffer.subarray(0, bytesRead);
|
|
704
|
+
const newline = window.indexOf(0x0a);
|
|
705
|
+
if (newline < 0)
|
|
706
|
+
return Transcript.loadFromFile(filePath);
|
|
707
|
+
window = window.subarray(newline + 1);
|
|
708
|
+
tail = window.toString("utf8");
|
|
709
|
+
}
|
|
710
|
+
finally {
|
|
711
|
+
closeSync(fd);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
return Transcript.loadFromFile(filePath);
|
|
716
|
+
}
|
|
717
|
+
const events = [];
|
|
718
|
+
for (const line of tail.split("\n")) {
|
|
719
|
+
if (!line.trim())
|
|
720
|
+
continue;
|
|
721
|
+
try {
|
|
722
|
+
events.push(JSON.parse(line));
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
// A malformed tail must not turn a partial window into authoritative
|
|
726
|
+
// context. The normal loader keeps its established skip behavior.
|
|
727
|
+
return Transcript.loadFromFile(filePath);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
let markerIndex = -1;
|
|
731
|
+
let toClientMessageId;
|
|
732
|
+
for (const [index, event] of events.entries()) {
|
|
733
|
+
if (event.type !== "range_archive")
|
|
734
|
+
continue;
|
|
735
|
+
const data = event.data;
|
|
736
|
+
if (data.fromClientMessageId === undefined &&
|
|
737
|
+
typeof data.summary === "string" &&
|
|
738
|
+
typeof data.toClientMessageId === "string") {
|
|
739
|
+
markerIndex = index;
|
|
740
|
+
toClientMessageId = data.toClientMessageId;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (markerIndex < 0 || !toClientMessageId)
|
|
744
|
+
return Transcript.loadFromFile(filePath);
|
|
745
|
+
const anchorIndex = events.findIndex((event, index) => index < markerIndex &&
|
|
746
|
+
event.type === "message" &&
|
|
747
|
+
event.data.clientMessageId === toClientMessageId);
|
|
748
|
+
if (anchorIndex < 0)
|
|
749
|
+
return Transcript.loadFromFile(filePath);
|
|
750
|
+
let startIndex = anchorIndex;
|
|
751
|
+
for (let index = anchorIndex - 1; index >= 0; index -= 1) {
|
|
752
|
+
if (events[index]?.type !== "turn_boundary")
|
|
753
|
+
continue;
|
|
754
|
+
startIndex = index;
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
const transcript = new Transcript(filePath);
|
|
758
|
+
transcript.loadEvents(events.slice(startIndex));
|
|
759
|
+
transcript.repairToolResultPairs();
|
|
760
|
+
return transcript;
|
|
761
|
+
}
|
|
681
762
|
loadEvents(events) {
|
|
682
763
|
this.events = structuredClone([...events]);
|
|
683
764
|
this.currentTurn = 0;
|
|
@@ -153,6 +153,18 @@ export declare class SettingsManager {
|
|
|
153
153
|
* be shared with collaborators.
|
|
154
154
|
*/
|
|
155
155
|
saveLocalSetting(key: string, value: unknown, cwd: string): void;
|
|
156
|
+
/**
|
|
157
|
+
* Mutate one writable settings layer under the same cross-process lock used
|
|
158
|
+
* by the desktop settings service. Domain tools use this when two related
|
|
159
|
+
* fields must change atomically (for example modelConnections + defaults):
|
|
160
|
+
* composing multiple save*Setting calls would expose an intermediate state
|
|
161
|
+
* and could interleave with another process between writes.
|
|
162
|
+
*
|
|
163
|
+
* The callback receives only the selected layer's raw object, not the merged
|
|
164
|
+
* settings view. The result is schema-validated before it replaces the file.
|
|
165
|
+
* Returning false makes the operation a no-op.
|
|
166
|
+
*/
|
|
167
|
+
mutateSettingsForScope(scope: "user" | "project", cwd: string, mutate: (current: Record<string, unknown>) => boolean | void): void;
|
|
156
168
|
/**
|
|
157
169
|
* Delete a single dotted key from the PROJECT-level config file. Used to
|
|
158
170
|
* express "inherit" — we don't persist the literal "inherit"; we remove the
|
package/dist/settings/manager.js
CHANGED
|
@@ -417,6 +417,37 @@ export class SettingsManager {
|
|
|
417
417
|
});
|
|
418
418
|
this.invalidate();
|
|
419
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* Mutate one writable settings layer under the same cross-process lock used
|
|
422
|
+
* by the desktop settings service. Domain tools use this when two related
|
|
423
|
+
* fields must change atomically (for example modelConnections + defaults):
|
|
424
|
+
* composing multiple save*Setting calls would expose an intermediate state
|
|
425
|
+
* and could interleave with another process between writes.
|
|
426
|
+
*
|
|
427
|
+
* The callback receives only the selected layer's raw object, not the merged
|
|
428
|
+
* settings view. The result is schema-validated before it replaces the file.
|
|
429
|
+
* Returning false makes the operation a no-op.
|
|
430
|
+
*/
|
|
431
|
+
mutateSettingsForScope(scope, cwd, mutate) {
|
|
432
|
+
const path = scope === "user"
|
|
433
|
+
? join(this.userConfigDir(), "settings.json")
|
|
434
|
+
: this.projectSettingsPath(cwd);
|
|
435
|
+
if (scope === "project") {
|
|
436
|
+
this.validateProjectCwd(cwd, "project");
|
|
437
|
+
if (!existsSync(cwd))
|
|
438
|
+
throw new Error(`project directory does not exist: ${cwd}`);
|
|
439
|
+
}
|
|
440
|
+
this.mutateSettingsFile(path, (current) => {
|
|
441
|
+
if (mutate(current) === false)
|
|
442
|
+
return false;
|
|
443
|
+
// Validate the complete resulting layer before persistence. We keep the
|
|
444
|
+
// original object for serialization so forward-compatible unknown keys
|
|
445
|
+
// are preserved instead of being stripped by Zod's parsed result.
|
|
446
|
+
validateSettings(current);
|
|
447
|
+
return true;
|
|
448
|
+
});
|
|
449
|
+
this.invalidate();
|
|
450
|
+
}
|
|
420
451
|
/**
|
|
421
452
|
* Delete a single dotted key from the PROJECT-level config file. Used to
|
|
422
453
|
* express "inherit" — we don't persist the literal "inherit"; we remove the
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfigureModelConnection — safely materialize one catalog model into the
|
|
3
|
+
* unified settings.modelConnections store without exposing or copying API
|
|
4
|
+
* keys. The write is schema-validated, lock-protected, atomic, and updates the
|
|
5
|
+
* selected tag default in the same transaction when requested.
|
|
6
|
+
*/
|
|
7
|
+
import type { ToolDefinition } from "../../types.js";
|
|
8
|
+
import type { ToolContext } from "../context.js";
|
|
9
|
+
import { SettingsManager } from "../../settings/manager.js";
|
|
10
|
+
import { type CatalogEntry } from "../../model-catalog/index.js";
|
|
11
|
+
import { createLLMClient } from "../../llm/client-factory.js";
|
|
12
|
+
import { type Credential, type ModelInstance } from "../../model-catalog/resolve.js";
|
|
13
|
+
interface ConfigureModelConnectionDeps {
|
|
14
|
+
makeSettingsManager(cwd: string, scope: "full" | "project"): SettingsManager;
|
|
15
|
+
getCatalog(): CatalogEntry[];
|
|
16
|
+
notifySettingsChanged(): void;
|
|
17
|
+
testTextConnection(connection: ModelInstance, credentials: Credential[], catalog: CatalogEntry[]): Promise<ConnectionTestResult>;
|
|
18
|
+
}
|
|
19
|
+
interface ConnectionTestResult {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
response?: string;
|
|
22
|
+
stopReason?: string;
|
|
23
|
+
usage?: {
|
|
24
|
+
promptTokens: number;
|
|
25
|
+
completionTokens: number;
|
|
26
|
+
totalTokens: number;
|
|
27
|
+
};
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function probeTextModelConnection(connection: ModelInstance, credentials: Credential[], catalog: CatalogEntry[], options?: {
|
|
31
|
+
fetch?: typeof globalThis.fetch;
|
|
32
|
+
createClient?: typeof createLLMClient;
|
|
33
|
+
}): Promise<ConnectionTestResult>;
|
|
34
|
+
export declare const configureModelConnectionToolDef: ToolDefinition;
|
|
35
|
+
export declare function configureModelConnectionTool(args: Record<string, unknown>, ctx?: ToolContext, deps?: ConfigureModelConnectionDeps): Promise<string>;
|
|
36
|
+
export {};
|