@agent-native/core 0.175.3 → 0.175.4-nightly-20260826190205

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 (36) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/server/plugins/agent-chat.ts +3 -0
  3. package/corpus/templates/content/actions/_linked-local-document-edit.ts +167 -0
  4. package/corpus/templates/content/actions/edit-document.ts +181 -61
  5. package/corpus/templates/content/actions/import-content-source.ts +33 -1
  6. package/corpus/templates/content/actions/update-document.ts +2 -0
  7. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +68 -0
  8. package/corpus/templates/content/app/components/editor/LinkedLocalDocumentAgentBridge.tsx +245 -0
  9. package/corpus/templates/content/app/lib/local-content-source-files.ts +96 -9
  10. package/corpus/templates/content/shared/api.ts +2 -0
  11. package/corpus/templates/content/shared/content-source.ts +14 -0
  12. package/corpus/templates/content/shared/document-text-edits.ts +39 -0
  13. package/corpus/templates/content/shared/nfm.ts +347 -1
  14. package/corpus/templates/slides/server/plugins/agent-chat.ts +2 -0
  15. package/dist/agent/engine/ai-sdk-engine.js +14 -4
  16. package/dist/agent/engine/anthropic-engine.js +6 -4
  17. package/dist/agent/engine/builder-engine.js +11 -4
  18. package/dist/agent/engine/tool-name.d.ts +9 -0
  19. package/dist/agent/engine/tool-name.js +56 -0
  20. package/dist/agent/engine/translate-ai-sdk.d.ts +5 -3
  21. package/dist/agent/engine/translate-ai-sdk.js +16 -13
  22. package/dist/agent/engine/translate-anthropic.d.ts +8 -6
  23. package/dist/agent/engine/translate-anthropic.js +21 -22
  24. package/dist/agent/production-agent.js +23 -5
  25. package/dist/collab/routes.d.ts +1 -1
  26. package/dist/collab/struct-routes.d.ts +1 -1
  27. package/dist/extensions/fetch-tool.js +73 -5
  28. package/dist/extensions/proxy-security.d.ts +5 -0
  29. package/dist/extensions/proxy-security.js +21 -13
  30. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  31. package/dist/notifications/routes.d.ts +3 -3
  32. package/dist/server/agent-chat/context-tools.js +5 -0
  33. package/dist/server/credential-provider.js +47 -10
  34. package/dist/server/realtime-token.d.ts +1 -1
  35. package/docs/content/webmcp.mdx +5 -5
  36. package/package.json +1 -1
@@ -0,0 +1,245 @@
1
+ import { getBrowserTabId } from "@agent-native/core/client/hooks";
2
+ import { startAgentNativeBrowserSessionBridge } from "@agent-native/core/client/host";
3
+ import type { AgentNativeClientAction } from "@agent-native/core/client/host";
4
+ import type { Document } from "@shared/api";
5
+ import {
6
+ applyDocumentTextEdits,
7
+ type DocumentTextEdit,
8
+ } from "@shared/document-text-edits";
9
+ import { useEffect, useRef } from "react";
10
+
11
+ import type { DesktopContentFileRevision } from "@/lib/desktop-content-files";
12
+ import {
13
+ readDocumentFromLinkedLocalSource,
14
+ writeDocumentToLinkedLocalSource,
15
+ } from "@/lib/local-content-source-files";
16
+
17
+ type EditArgs = {
18
+ documentId: string;
19
+ expectedContent: string;
20
+ expectedTitle: string;
21
+ expectedDescription: string;
22
+ expectedMetadata: string;
23
+ edits: DocumentTextEdit[];
24
+ };
25
+
26
+ const actionName = (documentId: string) =>
27
+ `content-edit-linked-document:${documentId}`;
28
+
29
+ function managedDocumentMetadata(document: Document) {
30
+ return {
31
+ parentId: document.parentId,
32
+ icon: document.icon,
33
+ position: document.position,
34
+ isFavorite: document.isFavorite,
35
+ hideFromSearch: document.hideFromSearch,
36
+ visibility: document.visibility,
37
+ };
38
+ }
39
+
40
+ export function LinkedLocalDocumentAgentBridge({
41
+ document,
42
+ getEditorSnapshot,
43
+ onPersisted,
44
+ }: {
45
+ document: Document;
46
+ getEditorSnapshot(): { title: string; content: string };
47
+ onPersisted(document: Document, revision?: DesktopContentFileRevision): void;
48
+ }) {
49
+ const documentRef = useRef(document);
50
+ documentRef.current = document;
51
+
52
+ useEffect(() => {
53
+ let active = true;
54
+ const action: AgentNativeClientAction<EditArgs> = {
55
+ name: actionName(document.id),
56
+ description: actionName(document.id),
57
+ schema: {
58
+ type: "object",
59
+ properties: {
60
+ documentId: { type: "string" },
61
+ expectedContent: { type: "string" },
62
+ expectedTitle: { type: "string" },
63
+ expectedDescription: { type: "string" },
64
+ expectedMetadata: { type: "string" },
65
+ edits: { type: "array" },
66
+ },
67
+ required: [
68
+ "documentId",
69
+ "expectedContent",
70
+ "expectedTitle",
71
+ "expectedDescription",
72
+ "expectedMetadata",
73
+ "edits",
74
+ ],
75
+ },
76
+ async run(args) {
77
+ const current = documentRef.current;
78
+ if (args.documentId !== current.id) {
79
+ return { status: "conflict", error: "The open document changed." };
80
+ }
81
+ const editorMatchesExpected = () => {
82
+ const editor = getEditorSnapshot();
83
+ return (
84
+ editor.content === args.expectedContent &&
85
+ editor.title === args.expectedTitle
86
+ );
87
+ };
88
+ if (!editorMatchesExpected()) {
89
+ return {
90
+ status: "conflict",
91
+ error:
92
+ "Save the pending editor changes before retrying the agent edit.",
93
+ };
94
+ }
95
+ const baseline = await readDocumentFromLinkedLocalSource(current);
96
+ if (!active || documentRef.current.id !== args.documentId) {
97
+ return { status: "conflict", error: "The open document changed." };
98
+ }
99
+ if (!baseline.ok) {
100
+ return {
101
+ status: baseline.unavailable ? "unavailable" : "failed",
102
+ error: baseline.error,
103
+ };
104
+ }
105
+ if (
106
+ baseline.document.content !== args.expectedContent ||
107
+ baseline.document.title !== args.expectedTitle ||
108
+ (baseline.document.description ?? "") !== args.expectedDescription ||
109
+ JSON.stringify(managedDocumentMetadata(baseline.document)) !==
110
+ args.expectedMetadata
111
+ ) {
112
+ return {
113
+ status: "conflict",
114
+ error: "The linked file changed after the agent loaded it.",
115
+ };
116
+ }
117
+ const applied = applyDocumentTextEdits(
118
+ baseline.document.content,
119
+ args.edits,
120
+ );
121
+ if (applied.changeCount === 0) {
122
+ return {
123
+ status: "failed",
124
+ error: "None of the requested text was found in the linked file.",
125
+ };
126
+ }
127
+ const next = { ...baseline.document, content: applied.content };
128
+ if (!editorMatchesExpected()) {
129
+ return {
130
+ status: "conflict",
131
+ error:
132
+ "The editor changed while the agent edit was being prepared.",
133
+ };
134
+ }
135
+ if (!active || documentRef.current.id !== args.documentId) {
136
+ return { status: "conflict", error: "The open document changed." };
137
+ }
138
+ const written = await writeDocumentToLinkedLocalSource(
139
+ next,
140
+ current.source,
141
+ { expectedRevision: baseline.revision },
142
+ );
143
+ if (!written.ok) {
144
+ return {
145
+ status: written.conflict
146
+ ? "conflict"
147
+ : written.unavailable
148
+ ? "unavailable"
149
+ : "failed",
150
+ error: written.error,
151
+ };
152
+ }
153
+ const readBack = await readDocumentFromLinkedLocalSource(next);
154
+ if (!active || documentRef.current.id !== args.documentId) {
155
+ return {
156
+ status: "source-persisted/history-pending",
157
+ content: next.content,
158
+ title: next.title,
159
+ description: next.description ?? "",
160
+ metadata: managedDocumentMetadata(next),
161
+ path: written.path,
162
+ runtime: written.runtime,
163
+ revision: written.revision,
164
+ };
165
+ }
166
+ if (!readBack.ok) {
167
+ return {
168
+ status: "source-persisted/readback-pending",
169
+ content: next.content,
170
+ title: next.title,
171
+ path: written.path,
172
+ runtime: written.runtime,
173
+ revision: written.revision,
174
+ };
175
+ }
176
+ const revisionMatches =
177
+ !written.revision ||
178
+ !readBack.revision ||
179
+ written.revision === readBack.revision;
180
+ const managedMetadataMatches =
181
+ JSON.stringify(managedDocumentMetadata(readBack.document)) ===
182
+ JSON.stringify(managedDocumentMetadata(next));
183
+ if (
184
+ !revisionMatches ||
185
+ readBack.document.content !== applied.content ||
186
+ !managedMetadataMatches ||
187
+ (readBack.document.description ?? "") !== (next.description ?? "") ||
188
+ readBack.document.title !== next.title
189
+ ) {
190
+ return {
191
+ status: "source-persisted/history-pending",
192
+ content: readBack.document.content,
193
+ title: readBack.document.title,
194
+ description: readBack.document.description ?? "",
195
+ metadata: managedDocumentMetadata(readBack.document),
196
+ path: readBack.path,
197
+ runtime: readBack.runtime,
198
+ revision: readBack.revision,
199
+ };
200
+ }
201
+ if (!editorMatchesExpected()) {
202
+ return {
203
+ status: "source-persisted/history-pending",
204
+ content: readBack.document.content,
205
+ title: readBack.document.title,
206
+ description: readBack.document.description ?? "",
207
+ metadata: managedDocumentMetadata(readBack.document),
208
+ path: readBack.path,
209
+ runtime: readBack.runtime,
210
+ revision: readBack.revision,
211
+ };
212
+ }
213
+ onPersisted(readBack.document, readBack.revision);
214
+ return {
215
+ status: "persisted",
216
+ content: readBack.document.content,
217
+ title: readBack.document.title,
218
+ path: readBack.path,
219
+ runtime: readBack.runtime,
220
+ revision: readBack.revision,
221
+ };
222
+ },
223
+ };
224
+ const bridge = startAgentNativeBrowserSessionBridge({
225
+ session: {
226
+ id: `${getBrowserTabId()}:content-page:${document.id}`,
227
+ label: document.title,
228
+ connectedAt: new Date().toISOString(),
229
+ url: window.location.href,
230
+ },
231
+ label: `Content: ${document.title}`,
232
+ getContext: () => ({
233
+ route: { pathname: window.location.pathname },
234
+ resource: { type: "document", id: document.id },
235
+ }),
236
+ actions: [action],
237
+ });
238
+ return () => {
239
+ active = false;
240
+ bridge.stop();
241
+ };
242
+ }, [document.id, getEditorSnapshot, onPersisted]);
243
+
244
+ return null;
245
+ }
@@ -15,13 +15,18 @@ import type {
15
15
  type PermissionState = "granted" | "denied" | "prompt";
16
16
  type LocalWritable = {
17
17
  write(data: string): Promise<void>;
18
+ truncate?(size: number): Promise<void>;
18
19
  close(): Promise<void>;
20
+ abort?(reason?: unknown): Promise<void>;
19
21
  };
20
22
  type LocalFileHandle = {
21
23
  kind: "file";
22
24
  name: string;
23
25
  getFile(): Promise<File>;
24
- createWritable(): Promise<LocalWritable>;
26
+ createWritable(options?: {
27
+ keepExistingData?: boolean;
28
+ mode?: "exclusive" | "siloed";
29
+ }): Promise<LocalWritable>;
25
30
  };
26
31
  type LocalDirectoryHandle = {
27
32
  kind: "directory";
@@ -309,6 +314,7 @@ async function writeBrowserFile(
309
314
  root: LocalDirectoryHandle,
310
315
  filePath: string,
311
316
  content: string,
317
+ expectedRevision: string,
312
318
  ) {
313
319
  const writePath =
314
320
  root.name === CONTENT_SOURCE_ROOT &&
@@ -323,10 +329,55 @@ async function writeBrowserFile(
323
329
  for (const part of parts) {
324
330
  dir = await dir.getDirectoryHandle(part, { create: true });
325
331
  }
326
- const file = await dir.getFileHandle(filename, { create: true });
327
- const writable = await file.createWritable();
328
- await writable.write(content);
329
- await writable.close();
332
+ const file = await dir.getFileHandle(filename);
333
+ let writable: LocalWritable;
334
+ try {
335
+ writable = await file.createWritable({
336
+ keepExistingData: true,
337
+ mode: "exclusive",
338
+ });
339
+ } catch (error) {
340
+ if (
341
+ error instanceof DOMException &&
342
+ error.name === "NoModificationAllowedError"
343
+ ) {
344
+ return { ok: false as const, locked: true as const };
345
+ }
346
+ throw error;
347
+ }
348
+ try {
349
+ const current = await file.getFile();
350
+ const currentRevision = await browserFileRevision(await current.text());
351
+ if (currentRevision !== expectedRevision) {
352
+ await writable.abort?.(
353
+ new DOMException("The local file changed.", "AbortError"),
354
+ );
355
+ return { ok: false as const, actualRevision: currentRevision };
356
+ }
357
+ if (!writable.truncate) {
358
+ throw new Error("The browser cannot safely replace this local file.");
359
+ }
360
+ await writable.truncate(0);
361
+ await writable.write(content);
362
+ await writable.close();
363
+ } catch (error) {
364
+ await writable.abort?.(error).catch(() => undefined);
365
+ throw error;
366
+ }
367
+ return {
368
+ ok: true as const,
369
+ revision: await browserFileRevision(content),
370
+ };
371
+ }
372
+
373
+ async function browserFileRevision(content: string) {
374
+ const digest = await crypto.subtle.digest(
375
+ "SHA-256",
376
+ new TextEncoder().encode(content),
377
+ );
378
+ return `sha256:${Array.from(new Uint8Array(digest), (byte) =>
379
+ byte.toString(16).padStart(2, "0"),
380
+ ).join("")}`;
330
381
  }
331
382
 
332
383
  async function readBrowserFile(root: LocalDirectoryHandle, filePath: string) {
@@ -345,9 +396,11 @@ async function readBrowserFile(root: LocalDirectoryHandle, filePath: string) {
345
396
  }
346
397
  const handle = await dir.getFileHandle(filename);
347
398
  const file = await handle.getFile();
399
+ const content = await file.text();
348
400
  return {
349
- content: await file.text(),
401
+ content,
350
402
  updatedAt: new Date(file.lastModified).toISOString(),
403
+ revision: await browserFileRevision(content),
351
404
  };
352
405
  }
353
406
 
@@ -373,6 +426,7 @@ export function sourceFileContent(document: Document, existingSource?: string) {
373
426
  parentId: document.parentId,
374
427
  title: document.title,
375
428
  content: document.content,
429
+ description: document.description,
376
430
  icon: document.icon,
377
431
  position: document.position,
378
432
  isFavorite: document.isFavorite,
@@ -419,10 +473,12 @@ function documentFromSourceContent(input: {
419
473
  parsed.parentId === undefined ? input.base.parentId : parsed.parentId,
420
474
  title: parsed.title,
421
475
  content: parsed.content,
476
+ description: parsed.description ?? input.base.description,
422
477
  icon: parsed.icon === undefined ? null : parsed.icon,
423
478
  position: parsed.position ?? input.base.position,
424
479
  isFavorite: parsed.isFavorite ?? false,
425
480
  hideFromSearch: parsed.hideFromSearch ?? false,
481
+ visibility: parsed.visibility ?? input.base.visibility,
426
482
  updatedAt: input.updatedAt,
427
483
  source: input.source,
428
484
  },
@@ -538,9 +594,39 @@ export async function writeDocumentToLinkedLocalSource(
538
594
  browserSource.handle,
539
595
  browserSource.path,
540
596
  );
597
+ const expectedRevision = options.expectedRevision;
598
+ if (!expectedRevision) {
599
+ return {
600
+ ok: false,
601
+ error: `Local file "${filePath}" has no observed revision.`,
602
+ };
603
+ }
541
604
  const content = sourceFileContent(document, existingSource.content);
542
- await writeBrowserFile(browserSource.handle, browserSource.path, content);
543
- return { ok: true, path: filePath, runtime: "browser" };
605
+ const result = await writeBrowserFile(
606
+ browserSource.handle,
607
+ browserSource.path,
608
+ content,
609
+ expectedRevision,
610
+ );
611
+ if (!result.ok) {
612
+ return {
613
+ ok: false,
614
+ error: result.locked
615
+ ? `Local file "${filePath}" is already open for writing.`
616
+ : `Local file "${filePath}" changed after it was read.`,
617
+ conflict: {
618
+ path: filePath,
619
+ expectedRevision,
620
+ actualRevision: result.locked ? undefined : result.actualRevision,
621
+ },
622
+ };
623
+ }
624
+ return {
625
+ ok: true,
626
+ path: filePath,
627
+ runtime: "browser",
628
+ revision: result.revision,
629
+ };
544
630
  }
545
631
 
546
632
  /**
@@ -677,13 +763,14 @@ export async function readDocumentFromLinkedLocalSource(
677
763
  browserSource.handle,
678
764
  browserSource.path,
679
765
  );
680
- return documentFromSourceContent({
766
+ const read = documentFromSourceContent({
681
767
  base: document,
682
768
  path: filePath,
683
769
  source,
684
770
  content: file.content,
685
771
  updatedAt: file.updatedAt,
686
772
  });
773
+ return read.ok ? { ...read, revision: file.revision } : read;
687
774
  } catch (error) {
688
775
  return {
689
776
  ok: false,
@@ -1,4 +1,5 @@
1
1
  import type { BlocksFieldIdentity } from "./blocks-field-identity";
2
+ import type { NfmFidelityReport } from "./nfm";
2
3
  import type {
3
4
  DocumentPropertyOptions,
4
5
  DocumentPropertyType,
@@ -46,6 +47,7 @@ export interface Document {
46
47
  contextPath?: ContentContextPathEntry[];
47
48
  createdAt: string;
48
49
  updatedAt: string;
50
+ contentFidelity?: NfmFidelityReport;
49
51
  }
50
52
 
51
53
  export interface DocumentSourceInfo {
@@ -37,6 +37,7 @@ export interface ParsedContentSourceFile {
37
37
  position?: number;
38
38
  isFavorite?: boolean;
39
39
  hideFromSearch?: boolean;
40
+ visibility?: "private" | "org" | "public";
40
41
  errors?: string[];
41
42
  }
42
43
 
@@ -192,6 +193,18 @@ export function parseContentSourceFile(
192
193
  const rawTitle = typeof metadata.title === "string" ? metadata.title : "";
193
194
  const title = rawTitle.trim() || titleFromPath(filePath) || "Untitled";
194
195
  const rawPosition = metadata.position;
196
+ let visibility: "private" | "org" | "public" | undefined;
197
+ if (hasOwn(metadata, "visibility")) {
198
+ if (
199
+ metadata.visibility === "private" ||
200
+ metadata.visibility === "org" ||
201
+ metadata.visibility === "public"
202
+ ) {
203
+ visibility = metadata.visibility;
204
+ } else {
205
+ errors.push("Invalid visibility frontmatter.");
206
+ }
207
+ }
195
208
 
196
209
  return {
197
210
  path: filePath,
@@ -220,6 +233,7 @@ export function parseContentSourceFile(
220
233
  typeof metadata.hideFromSearch === "boolean"
221
234
  ? metadata.hideFromSearch
222
235
  : undefined,
236
+ visibility,
223
237
  errors: errors.length > 0 ? errors : undefined,
224
238
  };
225
239
  }
@@ -0,0 +1,39 @@
1
+ export interface DocumentTextEdit {
2
+ find: string;
3
+ replace: string;
4
+ }
5
+
6
+ export function applyDocumentTextEdits(
7
+ source: string,
8
+ edits: DocumentTextEdit[],
9
+ ) {
10
+ let content = source;
11
+ const results: string[] = [];
12
+ const appliedEdits: DocumentTextEdit[] = [];
13
+
14
+ for (const edit of edits) {
15
+ const index = content.indexOf(edit.find);
16
+ if (index === -1) {
17
+ results.push(
18
+ `NOT FOUND: "${edit.find.slice(0, 60)}${edit.find.length > 60 ? "..." : ""}"`,
19
+ );
20
+ continue;
21
+ }
22
+ content =
23
+ content.slice(0, index) +
24
+ edit.replace +
25
+ content.slice(index + edit.find.length);
26
+ appliedEdits.push(edit);
27
+ const action = edit.replace === "" ? "deleted" : "replaced";
28
+ results.push(
29
+ `${action}: "${edit.find.slice(0, 40)}${edit.find.length > 40 ? "..." : ""}"`,
30
+ );
31
+ }
32
+
33
+ return {
34
+ content,
35
+ results,
36
+ appliedEdits,
37
+ changeCount: appliedEdits.length,
38
+ };
39
+ }