@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.
Files changed (3) hide show
  1. package/dist/index.d.ts +51 -27
  2. package/dist/index.js +298 -301
  3. 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 StoredAttachmentRecord = {
3
+ type StoredAssetRecord = {
4
4
  id: string;
5
5
  uri: string;
6
6
  storageKey: string;
7
- originalName: string;
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 SaveAttachmentParams = {
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
- type AttachmentTextSnapshot = {
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 LocalAttachmentStore {
41
+ declare class LocalAssetStore {
30
42
  readonly rootDir: string;
31
43
  constructor(options: {
32
44
  rootDir: string;
33
45
  });
34
- saveAttachment(params: SaveAttachmentParams): Promise<StoredAttachmentRecord>;
35
- getAttachmentByUri(uri: string): StoredAttachmentRecord | null;
36
- readAttachmentBytes(uri: string): Promise<Buffer | null>;
37
- readAttachmentBytesSync(uri: string): Buffer | null;
38
- readTextSnapshotSync(uri: string, options: {
39
- maxBytes: number;
40
- }): AttachmentTextSnapshot | null;
41
- statAttachment(uri: string): Promise<StoredAttachmentRecord | null>;
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 buildAttachmentContentPath(params: {
72
+ declare function buildAssetContentPath(params: {
46
73
  basePath: string;
47
- attachmentUri: string;
74
+ assetUri: string;
48
75
  }): string;
49
76
 
50
77
  type DefaultNcpContextBuilderOptions = {
51
78
  toolRegistry?: NcpToolRegistry;
52
- attachmentStore?: LocalAttachmentStore | null;
53
- attachmentTextMaxBytes?: number;
79
+ assetStore?: LocalAssetStore | null;
54
80
  };
55
81
  declare class DefaultNcpContextBuilder implements NcpContextBuilder {
56
82
  private readonly toolRegistry?;
57
- private readonly attachmentStore?;
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
- attachmentStore?: LocalAttachmentStore | null;
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 AttachmentTextSnapshot, DefaultNcpAgentRuntime, type DefaultNcpAgentRuntimeConfig, DefaultNcpContextBuilder, DefaultNcpRoundBuffer, DefaultNcpStreamEncoder, DefaultNcpToolRegistry, EchoNcpLLMApi, LocalAttachmentStore, type SaveAttachmentParams, type StoredAttachmentRecord, buildAttachmentContentPath, buildNcpUserContent, isTextLikeAttachment };
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 decodeInlineBase64(base64) {
223
- try {
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 suffix = params.truncated ? "\n[Attachment truncated]" : "";
233
- return `[Attachment: ${fileName}]
234
- [MIME: ${mimeType}]
235
- ${params.text}${suffix}`;
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 resolveImageDataUrl(part, attachmentStore) {
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 maxTextBytes = options.maxTextBytes ?? DEFAULT_ATTACHMENT_TEXT_MAX_BYTES;
261
- if (attachmentUri) {
262
- const snapshot = options.attachmentStore?.readTextSnapshotSync(attachmentUri, {
263
- maxBytes: maxTextBytes
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
- const bytes = decodeInlineBase64(contentBase64);
279
- if (!bytes) {
280
- return null;
41
+ if (url || part.contentBase64) {
42
+ return formatAssetReferenceBlock({
43
+ fileName,
44
+ mimeType,
45
+ url,
46
+ sizeBytes
47
+ });
281
48
  }
282
- const truncated = bytes.length > maxTextBytes;
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 textAttachmentBlock = resolveTextAttachmentBlock(part, options);
302
- if (textAttachmentBlock) {
303
- content.push({ type: "text", text: textAttachmentBlock });
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
- attachmentStore: options.attachmentStore,
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
- attachmentStore;
440
- attachmentTextMaxBytes;
187
+ assetStore;
441
188
  constructor(toolRegistryOrOptions) {
442
189
  if (isDefaultNcpContextBuilderOptions(toolRegistryOrOptions)) {
443
190
  this.toolRegistry = toolRegistryOrOptions.toolRegistry;
444
- this.attachmentStore = toolRegistryOrOptions.attachmentStore;
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
- attachmentStore: this.attachmentStore,
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
- attachmentStore: this.attachmentStore,
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
- LocalAttachmentStore,
1161
- buildAttachmentContentPath,
1157
+ LocalAssetStore,
1158
+ buildAssetContentPath,
1162
1159
  buildNcpUserContent,
1163
- isTextLikeAttachment
1160
+ isTextLikeAsset
1164
1161
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ncp-agent-runtime",
3
- "version": "0.2.4",
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.3.3"
18
+ "@nextclaw/ncp": "0.4.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^20.17.6",