@nextclaw/ncp-agent-runtime 0.2.4 → 0.3.0
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/index.d.ts +51 -27
- package/dist/index.js +298 -301
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,69 +1,93 @@
|
|
|
1
1
|
import { NcpContextBuilder, NcpToolRegistry, NcpAgentRunInput, NcpContextPrepareOptions, NcpLLMApiInput, NcpMessagePart, OpenAIContentPart, NcpRoundBuffer, NcpToolCallResult, NcpStreamEncoder, NcpAssistantReasoningNormalizationMode, OpenAIChatChunk, NcpEncodeContext, NcpEndpointEvent, NcpTool, NcpToolDefinition, NcpLLMApi, NcpLLMApiOptions, NcpAgentRuntime, NcpAgentConversationStateManager, NcpAgentRunOptions } from '@nextclaw/ncp';
|
|
2
2
|
|
|
3
|
-
type
|
|
3
|
+
type StoredAssetRecord = {
|
|
4
4
|
id: string;
|
|
5
5
|
uri: string;
|
|
6
6
|
storageKey: string;
|
|
7
|
-
|
|
7
|
+
fileName: string;
|
|
8
8
|
storedName: string;
|
|
9
9
|
mimeType: string;
|
|
10
10
|
sizeBytes: number;
|
|
11
11
|
createdAt: string;
|
|
12
12
|
sha256: string;
|
|
13
13
|
};
|
|
14
|
-
type
|
|
14
|
+
type AssetRef = {
|
|
15
|
+
uri: string;
|
|
16
|
+
};
|
|
17
|
+
type AssetMeta = {
|
|
18
|
+
uri: string;
|
|
15
19
|
fileName: string;
|
|
20
|
+
mimeType: string;
|
|
21
|
+
sizeBytes: number;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
};
|
|
24
|
+
type AssetPutInput = {
|
|
25
|
+
kind: "path";
|
|
26
|
+
path: string;
|
|
27
|
+
fileName?: string;
|
|
16
28
|
mimeType?: string | null;
|
|
29
|
+
createdAt?: Date;
|
|
30
|
+
} | {
|
|
31
|
+
kind: "bytes";
|
|
17
32
|
bytes: Uint8Array;
|
|
33
|
+
fileName: string;
|
|
34
|
+
mimeType?: string | null;
|
|
18
35
|
createdAt?: Date;
|
|
19
36
|
};
|
|
20
|
-
|
|
21
|
-
text: string;
|
|
22
|
-
truncated: boolean;
|
|
23
|
-
record: StoredAttachmentRecord;
|
|
24
|
-
};
|
|
25
|
-
declare function isTextLikeAttachment(params: {
|
|
37
|
+
declare function isTextLikeAsset(params: {
|
|
26
38
|
mimeType?: string | null;
|
|
27
39
|
fileName?: string | null;
|
|
28
40
|
}): boolean;
|
|
29
|
-
declare class
|
|
41
|
+
declare class LocalAssetStore {
|
|
30
42
|
readonly rootDir: string;
|
|
31
43
|
constructor(options: {
|
|
32
44
|
rootDir: string;
|
|
33
45
|
});
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}):
|
|
41
|
-
|
|
46
|
+
put(input: AssetPutInput): Promise<AssetRef>;
|
|
47
|
+
putBytes(params: {
|
|
48
|
+
fileName: string;
|
|
49
|
+
mimeType?: string | null;
|
|
50
|
+
bytes: Uint8Array;
|
|
51
|
+
createdAt?: Date;
|
|
52
|
+
}): Promise<StoredAssetRecord>;
|
|
53
|
+
putPath(params: {
|
|
54
|
+
path: string;
|
|
55
|
+
fileName?: string;
|
|
56
|
+
mimeType?: string | null;
|
|
57
|
+
createdAt?: Date;
|
|
58
|
+
}): Promise<StoredAssetRecord>;
|
|
59
|
+
export(ref: AssetRef, targetPath: string): Promise<string>;
|
|
60
|
+
stat(ref: AssetRef): Promise<AssetMeta | null>;
|
|
61
|
+
getByUri(uri: string): StoredAssetRecord | null;
|
|
62
|
+
statRecord(uri: string): Promise<StoredAssetRecord | null>;
|
|
63
|
+
readAssetBytes(uri: string): Promise<Buffer | null>;
|
|
42
64
|
resolveContentPath(uri: string): string | null;
|
|
65
|
+
private putFromPath;
|
|
66
|
+
private putFromBytes;
|
|
67
|
+
private buildRecord;
|
|
68
|
+
private writeMeta;
|
|
69
|
+
private resolveContentPathOrThrow;
|
|
43
70
|
private resolveStorageKeyDirectory;
|
|
44
71
|
}
|
|
45
|
-
declare function
|
|
72
|
+
declare function buildAssetContentPath(params: {
|
|
46
73
|
basePath: string;
|
|
47
|
-
|
|
74
|
+
assetUri: string;
|
|
48
75
|
}): string;
|
|
49
76
|
|
|
50
77
|
type DefaultNcpContextBuilderOptions = {
|
|
51
78
|
toolRegistry?: NcpToolRegistry;
|
|
52
|
-
|
|
53
|
-
attachmentTextMaxBytes?: number;
|
|
79
|
+
assetStore?: LocalAssetStore | null;
|
|
54
80
|
};
|
|
55
81
|
declare class DefaultNcpContextBuilder implements NcpContextBuilder {
|
|
56
82
|
private readonly toolRegistry?;
|
|
57
|
-
private readonly
|
|
58
|
-
private readonly attachmentTextMaxBytes?;
|
|
83
|
+
private readonly assetStore?;
|
|
59
84
|
constructor(toolRegistry?: NcpToolRegistry);
|
|
60
85
|
constructor(options?: DefaultNcpContextBuilderOptions);
|
|
61
86
|
prepare: (input: NcpAgentRunInput, options?: NcpContextPrepareOptions) => NcpLLMApiInput;
|
|
62
87
|
}
|
|
63
88
|
|
|
64
89
|
declare function buildNcpUserContent(parts: NcpMessagePart[], options?: {
|
|
65
|
-
|
|
66
|
-
maxTextBytes?: number;
|
|
90
|
+
assetStore?: LocalAssetStore | null;
|
|
67
91
|
}): string | OpenAIContentPart[];
|
|
68
92
|
|
|
69
93
|
declare class DefaultNcpRoundBuffer implements NcpRoundBuffer {
|
|
@@ -137,4 +161,4 @@ declare class DefaultNcpAgentRuntime implements NcpAgentRuntime {
|
|
|
137
161
|
private tapStream;
|
|
138
162
|
}
|
|
139
163
|
|
|
140
|
-
export { type
|
|
164
|
+
export { type AssetMeta, type AssetPutInput, type AssetRef, DefaultNcpAgentRuntime, type DefaultNcpAgentRuntimeConfig, DefaultNcpContextBuilder, DefaultNcpRoundBuffer, DefaultNcpStreamEncoder, DefaultNcpToolRegistry, EchoNcpLLMApi, LocalAssetStore, type StoredAssetRecord, buildAssetContentPath, buildNcpUserContent, isTextLikeAsset };
|
package/dist/index.js
CHANGED
|
@@ -1,217 +1,4 @@
|
|
|
1
|
-
// src/attachment-store.ts
|
|
2
|
-
import { createHash, randomUUID } from "crypto";
|
|
3
|
-
import { mkdir, readFile, stat, writeFile } from "fs/promises";
|
|
4
|
-
import { existsSync, readFileSync } from "fs";
|
|
5
|
-
import { dirname, join, resolve } from "path";
|
|
6
|
-
var ATTACHMENT_URI_SCHEME = "attachment://local/";
|
|
7
|
-
function normalizeSegment(value) {
|
|
8
|
-
return value.replace(/[^\w.-]+/g, "_").replace(/^_+|_+$/g, "") || "attachment.bin";
|
|
9
|
-
}
|
|
10
|
-
function normalizeOriginalName(value) {
|
|
11
|
-
const trimmed = value.trim();
|
|
12
|
-
return trimmed.length > 0 ? trimmed : "attachment.bin";
|
|
13
|
-
}
|
|
14
|
-
function normalizeMimeType(value) {
|
|
15
|
-
const normalized = value?.trim().toLowerCase() ?? "";
|
|
16
|
-
return normalized.length > 0 ? normalized : "application/octet-stream";
|
|
17
|
-
}
|
|
18
|
-
function buildAttachmentId() {
|
|
19
|
-
return `att_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
|
|
20
|
-
}
|
|
21
|
-
function ensureAttachmentRoot(rootDir) {
|
|
22
|
-
const normalized = rootDir.trim();
|
|
23
|
-
if (!normalized) {
|
|
24
|
-
throw new Error("LocalAttachmentStore requires a non-empty rootDir.");
|
|
25
|
-
}
|
|
26
|
-
return resolve(normalized);
|
|
27
|
-
}
|
|
28
|
-
function buildStorageKey(timestamp, attachmentId) {
|
|
29
|
-
const year = String(timestamp.getFullYear());
|
|
30
|
-
const month = String(timestamp.getMonth() + 1).padStart(2, "0");
|
|
31
|
-
const day = String(timestamp.getDate()).padStart(2, "0");
|
|
32
|
-
return `${year}/${month}/${day}/${attachmentId}`;
|
|
33
|
-
}
|
|
34
|
-
function buildAttachmentUri(storageKey) {
|
|
35
|
-
return `${ATTACHMENT_URI_SCHEME}${storageKey}`;
|
|
36
|
-
}
|
|
37
|
-
function parseAttachmentUri(uri) {
|
|
38
|
-
const normalized = uri.trim();
|
|
39
|
-
if (!normalized.startsWith(ATTACHMENT_URI_SCHEME)) {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
const storageKey = normalized.slice(ATTACHMENT_URI_SCHEME.length).replace(/^\/+/, "").trim();
|
|
43
|
-
return storageKey.length > 0 ? storageKey : null;
|
|
44
|
-
}
|
|
45
|
-
function ensureStorageKey(storageKey) {
|
|
46
|
-
const normalized = storageKey.trim().replace(/^\/+|\/+$/g, "");
|
|
47
|
-
if (!normalized) {
|
|
48
|
-
throw new Error("Attachment storage key must not be empty.");
|
|
49
|
-
}
|
|
50
|
-
const segments = normalized.split("/");
|
|
51
|
-
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
52
|
-
throw new Error(`Invalid attachment storage key: ${storageKey}`);
|
|
53
|
-
}
|
|
54
|
-
return segments.join("/");
|
|
55
|
-
}
|
|
56
|
-
function hydrateStoredAttachmentRecord(value) {
|
|
57
|
-
const originalName = normalizeOriginalName(value.originalName);
|
|
58
|
-
return {
|
|
59
|
-
...value,
|
|
60
|
-
originalName,
|
|
61
|
-
storedName: normalizeSegment(value.storedName || originalName),
|
|
62
|
-
mimeType: normalizeMimeType(value.mimeType)
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
function isTextLikeAttachment(params) {
|
|
66
|
-
const mimeType = normalizeMimeType(params.mimeType);
|
|
67
|
-
if (mimeType.startsWith("text/") || mimeType === "application/json" || mimeType === "application/xml" || mimeType === "text/xml" || mimeType === "application/yaml" || mimeType === "text/yaml" || mimeType === "application/x-yaml" || mimeType === "text/csv") {
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
const normalizedName = (params.fileName ?? "").trim().toLowerCase();
|
|
71
|
-
return [
|
|
72
|
-
".json",
|
|
73
|
-
".md",
|
|
74
|
-
".txt",
|
|
75
|
-
".csv",
|
|
76
|
-
".xml",
|
|
77
|
-
".yaml",
|
|
78
|
-
".yml",
|
|
79
|
-
".js",
|
|
80
|
-
".mjs",
|
|
81
|
-
".cjs",
|
|
82
|
-
".ts",
|
|
83
|
-
".tsx",
|
|
84
|
-
".jsx",
|
|
85
|
-
".py",
|
|
86
|
-
".rb",
|
|
87
|
-
".go",
|
|
88
|
-
".rs",
|
|
89
|
-
".java",
|
|
90
|
-
".kt",
|
|
91
|
-
".swift",
|
|
92
|
-
".php",
|
|
93
|
-
".css",
|
|
94
|
-
".scss",
|
|
95
|
-
".html",
|
|
96
|
-
".sql",
|
|
97
|
-
".sh"
|
|
98
|
-
].some((suffix) => normalizedName.endsWith(suffix));
|
|
99
|
-
}
|
|
100
|
-
var LocalAttachmentStore = class {
|
|
101
|
-
rootDir;
|
|
102
|
-
constructor(options) {
|
|
103
|
-
this.rootDir = ensureAttachmentRoot(options.rootDir);
|
|
104
|
-
}
|
|
105
|
-
async saveAttachment(params) {
|
|
106
|
-
const createdAt = params.createdAt ?? /* @__PURE__ */ new Date();
|
|
107
|
-
const id = buildAttachmentId();
|
|
108
|
-
const storageKey = buildStorageKey(createdAt, id);
|
|
109
|
-
const attachmentDir = this.resolveStorageKeyDirectory(storageKey);
|
|
110
|
-
const originalName = normalizeOriginalName(params.fileName);
|
|
111
|
-
const storedName = normalizeSegment(originalName);
|
|
112
|
-
const mimeType = normalizeMimeType(params.mimeType);
|
|
113
|
-
const bytes = Buffer.from(params.bytes);
|
|
114
|
-
const record = {
|
|
115
|
-
id,
|
|
116
|
-
uri: buildAttachmentUri(storageKey),
|
|
117
|
-
storageKey,
|
|
118
|
-
originalName,
|
|
119
|
-
storedName,
|
|
120
|
-
mimeType,
|
|
121
|
-
sizeBytes: bytes.length,
|
|
122
|
-
createdAt: createdAt.toISOString(),
|
|
123
|
-
sha256: createHash("sha256").update(bytes).digest("hex")
|
|
124
|
-
};
|
|
125
|
-
await mkdir(attachmentDir, { recursive: true });
|
|
126
|
-
await writeFile(join(attachmentDir, storedName), bytes);
|
|
127
|
-
await writeFile(
|
|
128
|
-
join(attachmentDir, "meta.json"),
|
|
129
|
-
`${JSON.stringify(record)}
|
|
130
|
-
`,
|
|
131
|
-
"utf8"
|
|
132
|
-
);
|
|
133
|
-
return record;
|
|
134
|
-
}
|
|
135
|
-
getAttachmentByUri(uri) {
|
|
136
|
-
const storageKey = parseAttachmentUri(uri);
|
|
137
|
-
if (!storageKey) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
const metaPath = join(this.resolveStorageKeyDirectory(storageKey), "meta.json");
|
|
141
|
-
if (!existsSync(metaPath)) {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
const text = readFileSync(metaPath, "utf8");
|
|
145
|
-
return hydrateStoredAttachmentRecord(JSON.parse(text));
|
|
146
|
-
}
|
|
147
|
-
async readAttachmentBytes(uri) {
|
|
148
|
-
const record = this.getAttachmentByUri(uri);
|
|
149
|
-
if (!record) {
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
const filePath = join(this.resolveStorageKeyDirectory(record.storageKey), record.storedName);
|
|
153
|
-
try {
|
|
154
|
-
return await readFile(filePath);
|
|
155
|
-
} catch {
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
readAttachmentBytesSync(uri) {
|
|
160
|
-
const record = this.getAttachmentByUri(uri);
|
|
161
|
-
if (!record) {
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
const filePath = join(this.resolveStorageKeyDirectory(record.storageKey), record.storedName);
|
|
165
|
-
return existsSync(filePath) ? readFileSync(filePath) : null;
|
|
166
|
-
}
|
|
167
|
-
readTextSnapshotSync(uri, options) {
|
|
168
|
-
const record = this.getAttachmentByUri(uri);
|
|
169
|
-
if (!record || !isTextLikeAttachment({ mimeType: record.mimeType, fileName: record.originalName })) {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
const bytes = this.readAttachmentBytesSync(uri);
|
|
173
|
-
if (!bytes) {
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
const truncated = bytes.length > options.maxBytes;
|
|
177
|
-
const snapshotBytes = truncated ? bytes.subarray(0, options.maxBytes) : bytes;
|
|
178
|
-
return {
|
|
179
|
-
text: snapshotBytes.toString("utf8"),
|
|
180
|
-
truncated,
|
|
181
|
-
record
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
async statAttachment(uri) {
|
|
185
|
-
const record = this.getAttachmentByUri(uri);
|
|
186
|
-
if (!record) {
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
const filePath = join(this.resolveStorageKeyDirectory(record.storageKey), record.storedName);
|
|
190
|
-
try {
|
|
191
|
-
await stat(filePath);
|
|
192
|
-
return record;
|
|
193
|
-
} catch {
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
resolveContentPath(uri) {
|
|
198
|
-
const record = this.getAttachmentByUri(uri);
|
|
199
|
-
if (!record) {
|
|
200
|
-
return null;
|
|
201
|
-
}
|
|
202
|
-
return join(this.resolveStorageKeyDirectory(record.storageKey), record.storedName);
|
|
203
|
-
}
|
|
204
|
-
resolveStorageKeyDirectory(storageKey) {
|
|
205
|
-
return join(this.rootDir, ensureStorageKey(storageKey));
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
function buildAttachmentContentPath(params) {
|
|
209
|
-
const query = new URLSearchParams({ uri: params.attachmentUri });
|
|
210
|
-
return `${params.basePath}?${query.toString()}`;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
1
|
// src/user-content.ts
|
|
214
|
-
var DEFAULT_ATTACHMENT_TEXT_MAX_BYTES = 32 * 1024;
|
|
215
2
|
function readOptionalString(value) {
|
|
216
3
|
if (typeof value !== "string") {
|
|
217
4
|
return null;
|
|
@@ -219,74 +6,47 @@ function readOptionalString(value) {
|
|
|
219
6
|
const trimmed = value.trim();
|
|
220
7
|
return trimmed.length > 0 ? trimmed : null;
|
|
221
8
|
}
|
|
222
|
-
function
|
|
223
|
-
|
|
224
|
-
return Buffer.from(base64, "base64");
|
|
225
|
-
} catch {
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
function formatTextAttachmentBlock(params) {
|
|
230
|
-
const fileName = readOptionalString(params.fileName) ?? "attachment";
|
|
9
|
+
function formatAssetReferenceBlock(params) {
|
|
10
|
+
const fileName = readOptionalString(params.fileName) ?? "asset";
|
|
231
11
|
const mimeType = readOptionalString(params.mimeType) ?? "application/octet-stream";
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
12
|
+
const assetUri = readOptionalString(params.assetUri);
|
|
13
|
+
const url = readOptionalString(params.url);
|
|
14
|
+
const sizeText = typeof params.sizeBytes === "number" && Number.isFinite(params.sizeBytes) ? String(params.sizeBytes) : null;
|
|
15
|
+
const lines = [
|
|
16
|
+
`[Asset: ${fileName}]`,
|
|
17
|
+
`[MIME: ${mimeType}]`,
|
|
18
|
+
...assetUri ? [`[Asset URI: ${assetUri}]`] : [],
|
|
19
|
+
...sizeText ? [`[Size Bytes: ${sizeText}]`] : [],
|
|
20
|
+
...url ? [`[Preview URL: ${url}]`] : [],
|
|
21
|
+
"[Instruction: This file is not embedded in the prompt. If you need to inspect or transform it, use asset_export to copy it to a normal file path first.]"
|
|
22
|
+
];
|
|
23
|
+
return lines.join("\n");
|
|
236
24
|
}
|
|
237
|
-
function
|
|
238
|
-
const attachmentUri = readOptionalString(part.attachmentUri);
|
|
239
|
-
const mimeType = readOptionalString(part.mimeType);
|
|
240
|
-
if (attachmentUri && mimeType?.startsWith("image/")) {
|
|
241
|
-
const bytes = attachmentStore?.readAttachmentBytesSync(attachmentUri);
|
|
242
|
-
if (bytes) {
|
|
243
|
-
return `data:${mimeType};base64,${bytes.toString("base64")}`;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
const url = readOptionalString(part.url);
|
|
247
|
-
if (url) {
|
|
248
|
-
return url;
|
|
249
|
-
}
|
|
250
|
-
const contentBase64 = readOptionalString(part.contentBase64);
|
|
251
|
-
if (!mimeType || !contentBase64 || !mimeType.startsWith("image/")) {
|
|
252
|
-
return null;
|
|
253
|
-
}
|
|
254
|
-
return `data:${mimeType};base64,${contentBase64}`;
|
|
255
|
-
}
|
|
256
|
-
function resolveTextAttachmentBlock(part, options) {
|
|
257
|
-
const attachmentUri = readOptionalString(part.attachmentUri);
|
|
25
|
+
function resolveAssetReferenceBlock(part, assetStore) {
|
|
258
26
|
const fileName = readOptionalString(part.name);
|
|
259
27
|
const mimeType = readOptionalString(part.mimeType);
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
28
|
+
const assetUri = readOptionalString(part.assetUri);
|
|
29
|
+
const url = readOptionalString(part.url);
|
|
30
|
+
const sizeBytes = typeof part.sizeBytes === "number" ? part.sizeBytes : void 0;
|
|
31
|
+
if (assetUri) {
|
|
32
|
+
const stored = assetStore?.getByUri(assetUri);
|
|
33
|
+
return formatAssetReferenceBlock({
|
|
34
|
+
fileName: stored?.fileName ?? fileName,
|
|
35
|
+
mimeType: stored?.mimeType ?? mimeType,
|
|
36
|
+
assetUri,
|
|
37
|
+
url,
|
|
38
|
+
sizeBytes: stored?.sizeBytes ?? sizeBytes
|
|
264
39
|
});
|
|
265
|
-
if (snapshot) {
|
|
266
|
-
return formatTextAttachmentBlock({
|
|
267
|
-
fileName: snapshot.record.originalName,
|
|
268
|
-
mimeType: snapshot.record.mimeType,
|
|
269
|
-
text: snapshot.text,
|
|
270
|
-
truncated: snapshot.truncated
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
const contentBase64 = readOptionalString(part.contentBase64);
|
|
275
|
-
if (!contentBase64 || !isTextLikeAttachment({ mimeType, fileName })) {
|
|
276
|
-
return null;
|
|
277
40
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
41
|
+
if (url || part.contentBase64) {
|
|
42
|
+
return formatAssetReferenceBlock({
|
|
43
|
+
fileName,
|
|
44
|
+
mimeType,
|
|
45
|
+
url,
|
|
46
|
+
sizeBytes
|
|
47
|
+
});
|
|
281
48
|
}
|
|
282
|
-
|
|
283
|
-
const text = (truncated ? bytes.subarray(0, maxTextBytes) : bytes).toString("utf8");
|
|
284
|
-
return formatTextAttachmentBlock({
|
|
285
|
-
fileName,
|
|
286
|
-
mimeType,
|
|
287
|
-
text,
|
|
288
|
-
truncated
|
|
289
|
-
});
|
|
49
|
+
return null;
|
|
290
50
|
}
|
|
291
51
|
function buildNcpUserContent(parts, options = {}) {
|
|
292
52
|
const content = [];
|
|
@@ -298,21 +58,10 @@ function buildNcpUserContent(parts, options = {}) {
|
|
|
298
58
|
if (part.type !== "file") {
|
|
299
59
|
continue;
|
|
300
60
|
}
|
|
301
|
-
const
|
|
302
|
-
if (
|
|
303
|
-
content.push({ type: "text", text:
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
const imageUrl = resolveImageDataUrl(part, options.attachmentStore);
|
|
307
|
-
if (!imageUrl) {
|
|
308
|
-
continue;
|
|
61
|
+
const assetReferenceBlock = resolveAssetReferenceBlock(part, options.assetStore);
|
|
62
|
+
if (assetReferenceBlock) {
|
|
63
|
+
content.push({ type: "text", text: assetReferenceBlock });
|
|
309
64
|
}
|
|
310
|
-
content.push({
|
|
311
|
-
type: "image_url",
|
|
312
|
-
image_url: {
|
|
313
|
-
url: imageUrl
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
65
|
}
|
|
317
66
|
if (content.length === 0) {
|
|
318
67
|
return "";
|
|
@@ -370,8 +119,7 @@ function messageToOpenAI(msg, options) {
|
|
|
370
119
|
{
|
|
371
120
|
role,
|
|
372
121
|
content: buildNcpUserContent(parts, {
|
|
373
|
-
|
|
374
|
-
maxTextBytes: options.attachmentTextMaxBytes
|
|
122
|
+
assetStore: options.assetStore
|
|
375
123
|
})
|
|
376
124
|
}
|
|
377
125
|
];
|
|
@@ -436,13 +184,11 @@ function messageToOpenAI(msg, options) {
|
|
|
436
184
|
}
|
|
437
185
|
var DefaultNcpContextBuilder = class {
|
|
438
186
|
toolRegistry;
|
|
439
|
-
|
|
440
|
-
attachmentTextMaxBytes;
|
|
187
|
+
assetStore;
|
|
441
188
|
constructor(toolRegistryOrOptions) {
|
|
442
189
|
if (isDefaultNcpContextBuilderOptions(toolRegistryOrOptions)) {
|
|
443
190
|
this.toolRegistry = toolRegistryOrOptions.toolRegistry;
|
|
444
|
-
this.
|
|
445
|
-
this.attachmentTextMaxBytes = toolRegistryOrOptions.attachmentTextMaxBytes;
|
|
191
|
+
this.assetStore = toolRegistryOrOptions.assetStore;
|
|
446
192
|
return;
|
|
447
193
|
}
|
|
448
194
|
this.toolRegistry = toolRegistryOrOptions;
|
|
@@ -460,16 +206,14 @@ var DefaultNcpContextBuilder = class {
|
|
|
460
206
|
for (const msg of sessionMessages.slice(-maxMessages)) {
|
|
461
207
|
messages.push(
|
|
462
208
|
...messageToOpenAI(msg, {
|
|
463
|
-
|
|
464
|
-
attachmentTextMaxBytes: this.attachmentTextMaxBytes
|
|
209
|
+
assetStore: this.assetStore
|
|
465
210
|
})
|
|
466
211
|
);
|
|
467
212
|
}
|
|
468
213
|
for (const msg of input.messages) {
|
|
469
214
|
messages.push(
|
|
470
215
|
...messageToOpenAI(msg, {
|
|
471
|
-
|
|
472
|
-
attachmentTextMaxBytes: this.attachmentTextMaxBytes
|
|
216
|
+
assetStore: this.assetStore
|
|
473
217
|
})
|
|
474
218
|
);
|
|
475
219
|
}
|
|
@@ -492,6 +236,259 @@ var DefaultNcpContextBuilder = class {
|
|
|
492
236
|
};
|
|
493
237
|
};
|
|
494
238
|
|
|
239
|
+
// src/asset-store.ts
|
|
240
|
+
import { createHash, randomUUID } from "crypto";
|
|
241
|
+
import { copyFileSync, existsSync, readFileSync } from "fs";
|
|
242
|
+
import { copyFile, mkdir, readFile, stat, writeFile } from "fs/promises";
|
|
243
|
+
import { basename, dirname, join, resolve } from "path";
|
|
244
|
+
var ASSET_URI_SCHEME = "asset://store/";
|
|
245
|
+
function normalizeSegment(value) {
|
|
246
|
+
return value.replace(/[^\w.-]+/g, "_").replace(/^_+|_+$/g, "") || "asset.bin";
|
|
247
|
+
}
|
|
248
|
+
function normalizeFileName(value) {
|
|
249
|
+
const trimmed = value.trim();
|
|
250
|
+
return trimmed.length > 0 ? trimmed : "asset.bin";
|
|
251
|
+
}
|
|
252
|
+
function normalizeMimeType(value) {
|
|
253
|
+
const normalized = value?.trim().toLowerCase() ?? "";
|
|
254
|
+
return normalized.length > 0 ? normalized : "application/octet-stream";
|
|
255
|
+
}
|
|
256
|
+
function buildAssetId() {
|
|
257
|
+
return `asset_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
|
|
258
|
+
}
|
|
259
|
+
function ensureAssetRoot(rootDir) {
|
|
260
|
+
const normalized = rootDir.trim();
|
|
261
|
+
if (!normalized) {
|
|
262
|
+
throw new Error("LocalAssetStore requires a non-empty rootDir.");
|
|
263
|
+
}
|
|
264
|
+
return resolve(normalized);
|
|
265
|
+
}
|
|
266
|
+
function buildStorageKey(timestamp, assetId) {
|
|
267
|
+
const year = String(timestamp.getFullYear());
|
|
268
|
+
const month = String(timestamp.getMonth() + 1).padStart(2, "0");
|
|
269
|
+
const day = String(timestamp.getDate()).padStart(2, "0");
|
|
270
|
+
return `${year}/${month}/${day}/${assetId}`;
|
|
271
|
+
}
|
|
272
|
+
function buildAssetUri(storageKey) {
|
|
273
|
+
return `${ASSET_URI_SCHEME}${storageKey}`;
|
|
274
|
+
}
|
|
275
|
+
function parseAssetUri(uri) {
|
|
276
|
+
const normalized = uri.trim();
|
|
277
|
+
if (!normalized.startsWith(ASSET_URI_SCHEME)) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
const storageKey = normalized.slice(ASSET_URI_SCHEME.length).replace(/^\/+/, "").trim();
|
|
281
|
+
return storageKey.length > 0 ? storageKey : null;
|
|
282
|
+
}
|
|
283
|
+
function ensureStorageKey(storageKey) {
|
|
284
|
+
const normalized = storageKey.trim().replace(/^\/+|\/+$/g, "");
|
|
285
|
+
if (!normalized) {
|
|
286
|
+
throw new Error("Asset storage key must not be empty.");
|
|
287
|
+
}
|
|
288
|
+
const segments = normalized.split("/");
|
|
289
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
290
|
+
throw new Error(`Invalid asset storage key: ${storageKey}`);
|
|
291
|
+
}
|
|
292
|
+
return segments.join("/");
|
|
293
|
+
}
|
|
294
|
+
function hydrateStoredAssetRecord(value) {
|
|
295
|
+
const fileName = normalizeFileName(value.fileName);
|
|
296
|
+
return {
|
|
297
|
+
...value,
|
|
298
|
+
fileName,
|
|
299
|
+
storedName: normalizeSegment(value.storedName || fileName),
|
|
300
|
+
mimeType: normalizeMimeType(value.mimeType)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function toAssetMeta(record) {
|
|
304
|
+
return {
|
|
305
|
+
uri: record.uri,
|
|
306
|
+
fileName: record.fileName,
|
|
307
|
+
mimeType: record.mimeType,
|
|
308
|
+
sizeBytes: record.sizeBytes,
|
|
309
|
+
createdAt: record.createdAt
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function isTextLikeAsset(params) {
|
|
313
|
+
const mimeType = normalizeMimeType(params.mimeType);
|
|
314
|
+
if (mimeType.startsWith("text/") || mimeType === "application/json" || mimeType === "application/xml" || mimeType === "text/xml" || mimeType === "application/yaml" || mimeType === "text/yaml" || mimeType === "application/x-yaml" || mimeType === "text/csv") {
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
const normalizedName = (params.fileName ?? "").trim().toLowerCase();
|
|
318
|
+
return [
|
|
319
|
+
".json",
|
|
320
|
+
".md",
|
|
321
|
+
".txt",
|
|
322
|
+
".csv",
|
|
323
|
+
".xml",
|
|
324
|
+
".yaml",
|
|
325
|
+
".yml",
|
|
326
|
+
".js",
|
|
327
|
+
".mjs",
|
|
328
|
+
".cjs",
|
|
329
|
+
".ts",
|
|
330
|
+
".tsx",
|
|
331
|
+
".jsx",
|
|
332
|
+
".py",
|
|
333
|
+
".rb",
|
|
334
|
+
".go",
|
|
335
|
+
".rs",
|
|
336
|
+
".java",
|
|
337
|
+
".kt",
|
|
338
|
+
".swift",
|
|
339
|
+
".php",
|
|
340
|
+
".css",
|
|
341
|
+
".scss",
|
|
342
|
+
".html",
|
|
343
|
+
".sql",
|
|
344
|
+
".sh"
|
|
345
|
+
].some((suffix) => normalizedName.endsWith(suffix));
|
|
346
|
+
}
|
|
347
|
+
var LocalAssetStore = class {
|
|
348
|
+
rootDir;
|
|
349
|
+
constructor(options) {
|
|
350
|
+
this.rootDir = ensureAssetRoot(options.rootDir);
|
|
351
|
+
}
|
|
352
|
+
async put(input) {
|
|
353
|
+
const record = input.kind === "path" ? await this.putFromPath(input) : await this.putFromBytes({
|
|
354
|
+
fileName: input.fileName,
|
|
355
|
+
mimeType: input.mimeType,
|
|
356
|
+
bytes: input.bytes,
|
|
357
|
+
createdAt: input.createdAt
|
|
358
|
+
});
|
|
359
|
+
return { uri: record.uri };
|
|
360
|
+
}
|
|
361
|
+
async putBytes(params) {
|
|
362
|
+
return this.putFromBytes(params);
|
|
363
|
+
}
|
|
364
|
+
async putPath(params) {
|
|
365
|
+
return this.putFromPath({
|
|
366
|
+
kind: "path",
|
|
367
|
+
...params
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
async export(ref, targetPath) {
|
|
371
|
+
const record = await this.statRecord(ref.uri);
|
|
372
|
+
if (!record) {
|
|
373
|
+
throw new Error(`Asset not found: ${ref.uri}`);
|
|
374
|
+
}
|
|
375
|
+
const outputPath = resolve(targetPath);
|
|
376
|
+
await ensureParentDirectory(outputPath);
|
|
377
|
+
await copyFile(this.resolveContentPathOrThrow(record), outputPath);
|
|
378
|
+
return outputPath;
|
|
379
|
+
}
|
|
380
|
+
async stat(ref) {
|
|
381
|
+
const record = await this.statRecord(ref.uri);
|
|
382
|
+
return record ? toAssetMeta(record) : null;
|
|
383
|
+
}
|
|
384
|
+
getByUri(uri) {
|
|
385
|
+
const storageKey = parseAssetUri(uri);
|
|
386
|
+
if (!storageKey) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
const metaPath = join(this.resolveStorageKeyDirectory(storageKey), "meta.json");
|
|
390
|
+
if (!existsSync(metaPath)) {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
const text = readFileSync(metaPath, "utf8");
|
|
394
|
+
return hydrateStoredAssetRecord(JSON.parse(text));
|
|
395
|
+
}
|
|
396
|
+
async statRecord(uri) {
|
|
397
|
+
const record = this.getByUri(uri);
|
|
398
|
+
if (!record) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
await stat(this.resolveContentPathOrThrow(record));
|
|
403
|
+
return record;
|
|
404
|
+
} catch {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
async readAssetBytes(uri) {
|
|
409
|
+
const record = await this.statRecord(uri);
|
|
410
|
+
if (!record) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
return await readFile(this.resolveContentPathOrThrow(record));
|
|
415
|
+
} catch {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
resolveContentPath(uri) {
|
|
420
|
+
const record = this.getByUri(uri);
|
|
421
|
+
return record ? this.resolveContentPathOrThrow(record) : null;
|
|
422
|
+
}
|
|
423
|
+
async putFromPath(input) {
|
|
424
|
+
const sourcePath = resolve(input.path);
|
|
425
|
+
const sourceStats = await stat(sourcePath);
|
|
426
|
+
if (!sourceStats.isFile()) {
|
|
427
|
+
throw new Error(`Asset source path is not a file: ${sourcePath}`);
|
|
428
|
+
}
|
|
429
|
+
const bytes = readFileSync(sourcePath);
|
|
430
|
+
const record = this.buildRecord({
|
|
431
|
+
fileName: input.fileName ?? basename(sourcePath),
|
|
432
|
+
mimeType: input.mimeType,
|
|
433
|
+
bytes,
|
|
434
|
+
createdAt: input.createdAt
|
|
435
|
+
});
|
|
436
|
+
const assetDir = this.resolveStorageKeyDirectory(record.storageKey);
|
|
437
|
+
await mkdir(assetDir, { recursive: true });
|
|
438
|
+
copyFileSync(sourcePath, join(assetDir, record.storedName));
|
|
439
|
+
await this.writeMeta(assetDir, record);
|
|
440
|
+
return record;
|
|
441
|
+
}
|
|
442
|
+
async putFromBytes(params) {
|
|
443
|
+
const bytes = Buffer.from(params.bytes);
|
|
444
|
+
const record = this.buildRecord({
|
|
445
|
+
fileName: params.fileName,
|
|
446
|
+
mimeType: params.mimeType,
|
|
447
|
+
bytes,
|
|
448
|
+
createdAt: params.createdAt
|
|
449
|
+
});
|
|
450
|
+
const assetDir = this.resolveStorageKeyDirectory(record.storageKey);
|
|
451
|
+
await mkdir(assetDir, { recursive: true });
|
|
452
|
+
await writeFile(join(assetDir, record.storedName), bytes);
|
|
453
|
+
await this.writeMeta(assetDir, record);
|
|
454
|
+
return record;
|
|
455
|
+
}
|
|
456
|
+
buildRecord(params) {
|
|
457
|
+
const createdAt = params.createdAt ?? /* @__PURE__ */ new Date();
|
|
458
|
+
const id = buildAssetId();
|
|
459
|
+
const storageKey = buildStorageKey(createdAt, id);
|
|
460
|
+
const fileName = normalizeFileName(params.fileName);
|
|
461
|
+
return {
|
|
462
|
+
id,
|
|
463
|
+
uri: buildAssetUri(storageKey),
|
|
464
|
+
storageKey,
|
|
465
|
+
fileName,
|
|
466
|
+
storedName: normalizeSegment(fileName),
|
|
467
|
+
mimeType: normalizeMimeType(params.mimeType),
|
|
468
|
+
sizeBytes: params.bytes.length,
|
|
469
|
+
createdAt: createdAt.toISOString(),
|
|
470
|
+
sha256: createHash("sha256").update(params.bytes).digest("hex")
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
async writeMeta(assetDir, record) {
|
|
474
|
+
await writeFile(join(assetDir, "meta.json"), `${JSON.stringify(record)}
|
|
475
|
+
`, "utf8");
|
|
476
|
+
}
|
|
477
|
+
resolveContentPathOrThrow(record) {
|
|
478
|
+
return join(this.resolveStorageKeyDirectory(record.storageKey), record.storedName);
|
|
479
|
+
}
|
|
480
|
+
resolveStorageKeyDirectory(storageKey) {
|
|
481
|
+
return join(this.rootDir, ensureStorageKey(storageKey));
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
function buildAssetContentPath(params) {
|
|
485
|
+
const query = new URLSearchParams({ uri: params.assetUri });
|
|
486
|
+
return `${params.basePath}?${query.toString()}`;
|
|
487
|
+
}
|
|
488
|
+
async function ensureParentDirectory(filePath) {
|
|
489
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
490
|
+
}
|
|
491
|
+
|
|
495
492
|
// src/round-buffer.ts
|
|
496
493
|
var DefaultNcpRoundBuffer = class {
|
|
497
494
|
text = "";
|
|
@@ -1157,8 +1154,8 @@ export {
|
|
|
1157
1154
|
DefaultNcpStreamEncoder,
|
|
1158
1155
|
DefaultNcpToolRegistry,
|
|
1159
1156
|
EchoNcpLLMApi,
|
|
1160
|
-
|
|
1161
|
-
|
|
1157
|
+
LocalAssetStore,
|
|
1158
|
+
buildAssetContentPath,
|
|
1162
1159
|
buildNcpUserContent,
|
|
1163
|
-
|
|
1160
|
+
isTextLikeAsset
|
|
1164
1161
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/ncp-agent-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Default agent runtime implementation built on NCP interfaces.",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"dist"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@nextclaw/ncp": "0.
|
|
18
|
+
"@nextclaw/ncp": "0.4.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/node": "^20.17.6",
|