2026-07-10 10:54:34 +08:00
import fs from "node:fs" ;
import path from "node:path" ;
import { fileURLToPath } from "node:url" ;
type ExtensionContext = {
hasUI? : boolean ;
ui ?: {
confirm ?: ( title : string , message : string , options? : Record < string , unknown >) => Promise < boolean >;
};
};
type ExtensionAPI = {
registerTool : ( spec : Record < string , unknown >) => void ;
on ?: (
eventName : string ,
handler : ( event : Record < string , unknown >) => Record < string , unknown > | void ,
) => void ;
};
type MnoteToolManifestEntry = {
piName : string ;
mnoteName : string ;
label : string ;
description : string ;
};
const DEFAULT_TOOLS : MnoteToolManifestEntry [] = [
{
piName : "mnote_current_page_read" ,
mnoteName : "mnote.current_page.read" ,
label : "MNote current page read" ,
description : "Read the current MNote page through MNote access scope." ,
},
{
piName : "mnote_selection_read" ,
mnoteName : "mnote.selection.read" ,
label : "MNote selection read" ,
description : "Read the current MNote editor selection snapshot supplied by MNote." ,
},
{
piName : "mnote_allowed_roots_describe" ,
mnoteName : "mnote.allowed_roots.describe" ,
label : "MNote allowed roots describe" ,
description : "Describe MNote allowed roots and disabled raw tools." ,
},
{
piName : "mnote_local_file_read" ,
mnoteName : "mnote.local_file.read" ,
label : "MNote local file read" ,
description : "Read a file only through MNote allowed roots." ,
},
{
piName : "mnote_local_file_patch" ,
mnoteName : "mnote.local_file.patch" ,
label : "MNote local file patch" ,
description : "Patch a file only through MNote allowed roots and watcher refresh." ,
},
{
piName : "mnote_knowledge_rag_status" ,
mnoteName : "mnote.knowledge_rag.status" ,
label : "MNote LightRAG status" ,
description : "Check the MNote LightRAG knowledge provider status, indexed source registry, dashboard URL, and sync state. Use before knowledge-base questions when availability is uncertain. Params: optional workspaceId/rootUri; MNote fills the current Pi session context when omitted." ,
},
{
piName : "mnote_knowledge_rag_query" ,
mnoteName : "mnote.knowledge_rag.query" ,
label : "MNote LightRAG query" ,
description : "Ask the MNote LightRAG knowledge library across indexed books, papers, Office files, PDFs, images, and attachments. Use this for knowledge-base questions and answers that require sources, citations, or evidence. For book or long-document questions, pass query, mode='naive' or 'mix', topK, chunkTopK, includeChunkContent=true, and includeDocumentStructureIndex=true. Use returned references/citations quotes as evidence; do not invent page numbers or hand-write /documents/mnote:// links." ,
},
{
piName : "mnote_knowledge_rag_section_context" ,
mnoteName : "mnote.knowledge_rag.section_context" ,
label : "MNote LightRAG section context" ,
description : "Read bounded section blocks/chunks from a LightRAG sidecar using documentStructureIndex ranges returned by mnote_knowledge_rag_query. Use this for second-pass reading of large books or long documents when query references are not enough. Params include sourcePath/sourceId/lightRagDocId/filePath/sectionId, block or paragraph ordinal range, contextBefore/contextAfter, maxBlocks, maxChars." ,
},
{
piName : "mnote_knowledge_rag_open_reference" ,
mnoteName : "mnote.knowledge_rag.open_reference" ,
label : "MNote LightRAG open reference" ,
description : "Convert a LightRAG reference, filePath, or chunkId returned by mnote_knowledge_rag_query into a MNote clickable local resource locator. Use when the user asks to open or verify a cited source." ,
},
{
piName : "mnote_reference_open" ,
mnoteName : "mnote.reference.open" ,
label : "MNote reference open" ,
description : "Legacy alias for opening a citation/reference through MNote mapping. Prefer mnote_knowledge_rag_open_reference for LightRAG references." ,
},
{
piName : "mnote_codex_rescue_request" ,
mnoteName : "mnote.codex_rescue.request" ,
label : "MNote Codex rescue" ,
description : "Ask local Codex to rescue hard MNote/Pi problems before escalating to the user. Use when tools, skills, MCP, LightRAG, environment, or local repo behavior looks broken and normal Pi troubleshooting is insufficient. This tool is admin-gated, approval-gated by default, runs codex exec with workspace-write sandbox and a timeout, and returns Codex's final answer plus stdout/stderr snippets. Provide issue, evidence/logs, attempted steps, and desired outcome. Call at most once per unresolved incident; if Codex cannot fix it, summarize the blocker to the user." ,
},
{
piName : "mnote_tool_receipt_write" ,
mnoteName : "mnote.tool_receipt.write" ,
label : "MNote tool receipt write" ,
description : "Write a provider-neutral MNote tool receipt." ,
},
];
const EXTENSION_DIR = path . dirname ( fileURLToPath ( import . meta . url ));
const DEFAULT_CONTEXT_FILE = path . join ( EXTENSION_DIR , "mnote-context.json" );
2026-07-11 20:39:18 +08:00
const EMBEDDED_CONTEXT : Record < string , unknown > | undefined = undefined ;
2026-07-10 10:54:34 +08:00
const CONTEXT_FILE = env ( "PI_MNOTE_CONTEXT_FILE" )
2026-07-11 20:39:18 +08:00
|| DEFAULT_CONTEXT_FILE ;
2026-07-10 10:54:34 +08:00
const RUNTIME_IMPL = env ( "PI_MNOTE_RUNTIME_IMPL" )
|| env ( "MNOTE_PI_RUNTIME_IMPL" )
|| ( CONTEXT_FILE ? "pi-rust" : "" );
const HTTP_BRIDGE_AVAILABLE = env ( "PI_MNOTE_HTTP_BRIDGE_AVAILABLE" ) === "1" ;
const TOOL_POLICIES = parseJsonRecord ( env ( "PI_MNOTE_BRIDGE_TOOL_POLICIES" ) || env ( "MNOTE_PI_BRIDGE_TOOL_POLICIES" ), {});
const TOOLS = normalizeTools ( parseJsonUnknown ( env ( "PI_MNOTE_BRIDGE_TOOLS" ) || env ( "MNOTE_PI_BRIDGE_TOOLS" )) ?? DEFAULT_TOOLS );
const CONTEXT_PREFIX = "[[MNOTE_PI_CONTEXT_V1:" ;
let liveContext : Record < string , unknown > | undefined ;
function env ( name : string ) : string {
return ( process . env [ name ] || "" ). trim ();
}
2026-07-11 20:39:18 +08:00
function contextFileExists () : boolean {
return Boolean ( CONTEXT_FILE ) && fs . existsSync ( CONTEXT_FILE );
}
2026-07-10 10:54:34 +08:00
function parseJsonUnknown ( raw : string ) : unknown {
if ( ! raw ) return undefined ;
try {
return JSON . parse ( raw );
} catch {
return undefined ;
}
}
function parseJsonRecord ( raw : string , fallback : Record < string , unknown >) : Record < string , unknown > {
const parsed = parseJsonUnknown ( raw );
return parsed && typeof parsed === "object" && ! Array . isArray ( parsed )
? parsed as Record < string , unknown >
: fallback ;
}
function normalizeTools ( value : unknown ) : MnoteToolManifestEntry [] {
if ( ! Array . isArray ( value )) return DEFAULT_TOOLS ;
const tools = value
. map (( entry ) => {
if ( ! entry || typeof entry !== "object" ) return undefined ;
const record = entry as Record < string , unknown >;
const piName = stringField ( record , "piName" ) || stringField ( record , "pi_name" );
const mnoteName = stringField ( record , "mnoteName" ) || stringField ( record , "mnote_name" );
const label = stringField ( record , "label" ) || piName ;
const description = stringField ( record , "description" ) || label ;
if ( ! piName || ! mnoteName ) return undefined ;
return { piName , mnoteName , label , description };
})
. filter (( entry ) : entry is MnoteToolManifestEntry => Boolean ( entry ));
return tools . length ? tools : DEFAULT_TOOLS ;
}
function stringField ( record : Record < string , unknown >, key : string ) : string {
const value = record [ key ];
return typeof value === "string" ? value . trim () : "" ;
}
2026-07-11 20:39:18 +08:00
function recordField ( record : Record < string , unknown > | undefined , key : string ) : Record < string , unknown > | undefined {
if ( ! record ) return undefined ;
const value = record [ key ];
return value && typeof value === "object" && ! Array . isArray ( value )
? value as Record < string , unknown >
: undefined ;
}
function selectedContextPart ( context : Record < string , unknown >, key : string ) : Record < string , unknown > | undefined {
return recordField ( recordField ( context , "selectedContext" ), key );
}
function contextRefs ( context : Record < string , unknown >) : string [] {
const refs = context . contextRefs ;
return Array . isArray ( refs ) ? refs . filter (( ref ) : ref is string => typeof ref === "string" ) : [];
}
2026-07-10 10:54:34 +08:00
function toolResult ( payload : Record < string , unknown >, isError = false ) {
return {
content : [{ type : "text" , text : JSON.stringify ( payload , null , 2 ) }],
details : payload ,
isError ,
};
}
function decodeHexJson ( value : string ) : Record < string , unknown > {
if ( ! value || value . length % 2 !== 0 || ! /^[0-9a-f]+$/i . test ( value )) {
throw new Error ( "MNote Pi context hex 无效" );
}
let jsonText = "" ;
for ( let index = 0 ; index < value . length ; index += 2 ) {
jsonText += String . fromCharCode ( Number . parseInt ( value . slice ( index , index + 2 ), 16 ));
}
const parsed = JSON . parse ( decodeURIComponent ( escape ( jsonText )));
if ( ! parsed || typeof parsed !== "object" || Array . isArray ( parsed )) {
throw new Error ( "MNote Pi context payload 格式无效" );
}
return parsed as Record < string , unknown >;
}
function captureInputContext ( event : Record < string , unknown >) {
const text = typeof event . text === "string" ? event . text : "" ;
const start = text . indexOf ( CONTEXT_PREFIX );
if ( start < 0 ) return { action : "continue" };
const end = text . indexOf ( "]]\n" , start );
if ( end < 0 ) return { action : "continue" };
const encoded = text . slice ( start + CONTEXT_PREFIX . length , end );
try {
liveContext = decodeHexJson ( encoded );
} catch ( error ) {
liveContext = {
contextError : error instanceof Error ? error.message : String ( error ),
};
}
return {
action : "transform" ,
text : ` ${ text . slice ( 0 , start ) }${ text . slice ( end + 3 ) } ` ,
images : event.images ,
};
}
2026-07-11 20:39:18 +08:00
function readContextFileSnapshot () : Record < string , unknown > {
if ( ! contextFileExists ()) {
throw new Error ( "MNote Pi 上下文快照不可用;Pi Rust bridge 需要 input hook、embedded context 或 context file" );
2026-07-10 10:54:34 +08:00
}
const raw = fs . readFileSync ( CONTEXT_FILE , "utf8" );
const parsed = JSON . parse ( raw );
if ( ! parsed || typeof parsed !== "object" || Array . isArray ( parsed )) {
throw new Error ( "MNote Pi 上下文文件格式无效" );
}
return parsed as Record < string , unknown >;
}
2026-07-11 20:39:18 +08:00
function embeddedContextSnapshot () : Record < string , unknown > | undefined {
return EMBEDDED_CONTEXT && typeof EMBEDDED_CONTEXT === "object" && ! Array . isArray ( EMBEDDED_CONTEXT )
? EMBEDDED_CONTEXT
: undefined ;
}
function activeContextSnapshot () : Record < string , unknown > | undefined {
try {
if ( contextFileExists ()) return readContextFileSnapshot ();
} catch {
// Fall back to the input-hook snapshot when the host cannot read files.
}
return liveContext || embeddedContextSnapshot ();
}
function readContextSnapshot () : Record < string , unknown > {
const snapshot = activeContextSnapshot ();
if ( snapshot ) return snapshot ;
return readContextFileSnapshot ();
}
function bridgeToken () : string {
const fromEnv = env ( "PI_MNOTE_BRIDGE_TOKEN" )
|| env ( "MNOTE_PI_BRIDGE_TOKEN" )
|| env ( "MNOTE_PI_LAB_BRIDGE_TOKEN" );
if ( fromEnv ) return fromEnv ;
const fromContext = activeContextSnapshot ();
if ( fromContext ) {
const value = stringField ( fromContext , "bridgeToken" );
if ( value ) return value ;
}
return "" ;
}
function bridgeBaseUrl () : string {
const fromEnv = env ( "PI_MNOTE_BRIDGE_BASE_URL" )
|| env ( "MNOTE_PI_BRIDGE_BASE_URL" )
|| env ( "MNOTE_PI_LAB_BASE_URL" );
if ( fromEnv ) return fromEnv ;
const fromContext = activeContextSnapshot ();
if ( fromContext ) {
const value = stringField ( fromContext , "bridgeBaseUrl" );
if ( value ) return value ;
}
return "http://127.0.0.1:3000" ;
}
function bridgeSessionId () : string {
const fromContext = activeContextSnapshot ();
const fromContextSession = fromContext ? stringField ( fromContext , "sessionId" ) : "" ;
if ( fromContextSession ) return fromContextSession ;
const fromEnv = env ( "PI_MNOTE_BRIDGE_SESSION_ID" )
|| env ( "MNOTE_PI_BRIDGE_SESSION_ID" );
if ( fromEnv ) return fromEnv ;
return "" ;
}
2026-07-10 10:54:34 +08:00
function contextAllowedRoots ( context : Record < string , unknown >) : Record < string , unknown >[] {
const snapshot = context . allowedRoots ;
if ( ! snapshot || typeof snapshot !== "object" || Array . isArray ( snapshot )) return [];
const roots = ( snapshot as Record < string , unknown >). roots ;
if ( ! Array . isArray ( roots )) return [];
return roots . filter (( root ) : root is Record < string , unknown > => (
Boolean ( root ) && typeof root === "object" && ! Array . isArray ( root )
));
}
function pathIsInside ( target : string , root : string ) : boolean {
const relative = path . relative ( root , target );
return relative === "" || ( ! relative . startsWith ( ".." ) && ! path . isAbsolute ( relative ));
}
function resolveAllowedTarget (
context : Record < string , unknown >,
rootUri : string ,
relativePath : string ,
2026-07-11 20:39:18 +08:00
options ?: { allowMissing? : boolean },
2026-07-10 10:54:34 +08:00
) : { target : string ; readPath : string } {
const roots = contextAllowedRoots ( context );
const matchedRoot = roots . find (( root ) => stringField ( root , "rootUri" ) === rootUri );
const rootPath = matchedRoot
? stringField ( matchedRoot , "rootPath" )
: stringField ( context , "primaryRootPath" ) || process . cwd ();
if ( ! rootPath ) throw new Error ( "MNote allowed root 缺少 rootPath" );
const canonicalRoot = fs . realpathSync ( rootPath );
const requestedTarget = path . isAbsolute ( relativePath )
? path . resolve ( relativePath )
: path . resolve ( canonicalRoot , relativePath );
2026-07-11 20:39:18 +08:00
const canonicalTarget = options ? . allowMissing
? canonicalOrParent ( requestedTarget )
: fs . realpathSync ( requestedTarget );
2026-07-10 10:54:34 +08:00
const allowedRoots = roots
. map (( root ) => stringField ( root , "rootPath" ))
. filter ( Boolean )
. map (( root ) => fs . realpathSync ( root ));
const candidates = allowedRoots . length ? allowedRoots : [ canonicalRoot ];
if ( ! candidates . some (( root ) => pathIsInside ( canonicalTarget , root ))) {
throw new Error ( `路径超出 MNote allowed roots: ${ relativePath } ` );
}
const processRoot = fs . realpathSync ( process . cwd ());
const readPath = pathIsInside ( canonicalTarget , processRoot )
? path . relative ( processRoot , canonicalTarget ) || "."
: canonicalTarget ;
return { target : canonicalTarget , readPath };
}
2026-07-11 20:39:18 +08:00
function canonicalOrParent ( target : string ) : string {
try {
return fs . realpathSync ( target );
} catch {
const parent = path . dirname ( target );
try {
return path . join ( fs . realpathSync ( parent ), path . basename ( target ));
} catch {
return path . resolve ( target );
}
}
}
function sessionRootUri ( context : Record < string , unknown >, input : Record < string , unknown >) : string {
return stringField ( input , "rootUri" )
|| stringField ( input , "root_uri" )
|| stringField ( selectedContextPart ( context , "currentPage" ) || {}, "rootUri" )
|| stringField ( selectedContextPart ( context , "currentFolder" ) || {}, "rootUri" )
|| stringField ( context , "rootUri" );
}
function joinRelativePath ( basePath : string , childPath : string ) : string {
const normalizedChild = childPath . replace ( /\\/g , "/" ). replace ( /^\/+/ , "" );
const normalizedBase = basePath . replace ( /\\/g , "/" ). replace ( /^\/+|\/+$/g , "" );
if ( ! normalizedBase || path . isAbsolute ( childPath )) return normalizedChild ;
if ( ! normalizedChild || normalizedChild === "." ) return normalizedBase ;
if ( normalizedChild === normalizedBase || normalizedChild . startsWith ( ` ${ normalizedBase } /` )) return normalizedChild ;
return ` ${ normalizedBase } / ${ normalizedChild } ` ;
}
function sessionRelativePath ( context : Record < string , unknown >, input : Record < string , unknown >, options ?: { preferFolder? : boolean }) : string {
const explicitPath = stringField ( input , "relativePath" )
|| stringField ( input , "relative_path" )
|| stringField ( input , "path" );
const folderPath = stringField ( input , "folderPath" )
|| stringField ( input , "folder_path" )
|| stringField ( selectedContextPart ( context , "currentFolder" ) || {}, "folderPath" );
if ( explicitPath ) {
return options ? . preferFolder ? joinRelativePath ( folderPath , explicitPath ) : explicitPath ;
}
return stringField ( input , "pagePath" )
|| stringField ( input , "page_path" )
|| stringField ( selectedContextPart ( context , "currentPage" ) || {}, "pagePath" )
|| folderPath
|| stringField ( context , "pagePath" );
}
function contextualToolParams ( toolName : string , params : unknown ) : Record < string , unknown > {
const nextParams = { ...(( params || {}) as Record < string , unknown >) };
const context = activeContextSnapshot ();
if ( ! context ) return nextParams ;
const currentPage = selectedContextPart ( context , "currentPage" );
const currentFolder = selectedContextPart ( context , "currentFolder" );
const selection = selectedContextPart ( context , "selection" );
if ( ! stringField ( nextParams , "rootUri" )) {
nextParams . rootUri = stringField ( currentPage || {}, "rootUri" )
|| stringField ( currentFolder || {}, "rootUri" )
|| stringField ( context , "rootUri" )
|| undefined ;
}
if ( ! stringField ( nextParams , "workspaceId" )) {
nextParams . workspaceId = stringField ( currentPage || {}, "workspaceId" )
|| stringField ( currentFolder || {}, "workspaceId" )
|| stringField ( context , "workspaceId" )
|| undefined ;
}
if ( ! stringField ( nextParams , "pagePath" )) {
nextParams . pagePath = stringField ( currentPage || {}, "pagePath" ) || undefined ;
}
if ( ! stringField ( nextParams , "folderPath" )) {
nextParams . folderPath = stringField ( currentFolder || {}, "folderPath" ) || undefined ;
}
if ( toolName === "mnote.selection.read" ) {
nextParams . selectionSource = "mnote_sidebar_host" ;
nextParams . selection = selection || null ;
}
return nextParams ;
}
2026-07-10 10:54:34 +08:00
function executeNativeCurrentPageRead ( params : unknown ) {
try {
const input = params && typeof params === "object" && ! Array . isArray ( params )
? params as Record < string , unknown >
: {};
const context = readContextSnapshot ();
2026-07-11 20:39:18 +08:00
const currentPage = selectedContextPart ( context , "currentPage" );
const rootUri = stringField ( currentPage || {}, "rootUri" )
2026-07-10 10:54:34 +08:00
|| stringField ( input , "rootUri" )
|| stringField ( input , "root_uri" );
2026-07-11 20:39:18 +08:00
const pagePath = stringField ( currentPage || {}, "pagePath" )
2026-07-10 10:54:34 +08:00
|| stringField ( input , "pagePath" )
|| stringField ( input , "page_path" )
|| stringField ( input , "path" );
if ( ! rootUri ) throw new Error ( "读取当前页缺少 rootUri" );
if ( ! pagePath ) throw new Error ( "读取当前页缺少 pagePath" );
const resolved = resolveAllowedTarget ( context , rootUri , pagePath );
const content = fs . readFileSync ( resolved . readPath , "utf8" );
const stat = fs . statSync ( resolved . readPath );
return toolResult ({
ok : true ,
rootUri ,
pagePath ,
path : resolved.target ,
content ,
contentLength : content.length ,
format : "markdown" ,
fileVersion : `pi-rust-native- ${ Math . trunc ( stat . mtimeMs ) } - ${ stat . size } ` ,
transport : "pi-rust-native-fs" ,
contextFile : CONTEXT_FILE ,
2026-07-11 20:39:18 +08:00
contextRefs : contextRefs ( context ),
selectedContextCurrentPage : currentPage ?? null ,
2026-07-10 10:54:34 +08:00
});
} catch ( error ) {
return toolResult ({
ok : false ,
code : "mnote_pi_rust_current_page_read_failed" ,
message : error instanceof Error ? error.message : String ( error ),
transport : "pi-rust-native-fs" ,
contextFile : CONTEXT_FILE ,
}, true );
}
}
2026-07-11 20:39:18 +08:00
function executeNativeLocalFileRead ( params : unknown ) {
try {
const input = params && typeof params === "object" && ! Array . isArray ( params )
? params as Record < string , unknown >
: {};
const context = readContextSnapshot ();
const rootUri = sessionRootUri ( context , input );
const relativePath = sessionRelativePath ( context , input , { preferFolder : true });
if ( ! rootUri ) throw new Error ( "读取文件缺少 rootUri" );
if ( ! relativePath ) throw new Error ( "读取文件缺少 relativePath/path" );
const resolved = resolveAllowedTarget ( context , rootUri , relativePath );
const content = fs . readFileSync ( resolved . readPath , "utf8" );
const stat = fs . statSync ( resolved . readPath );
return toolResult ({
ok : true ,
rootUri ,
relativePath ,
path : resolved.target ,
content ,
contentLength : content.length ,
fileVersion : `pi-rust-native- ${ Math . trunc ( stat . mtimeMs ) } - ${ stat . size } ` ,
transport : "pi-rust-native-fs" ,
contextFile : CONTEXT_FILE ,
});
} catch ( error ) {
return toolResult ({
ok : false ,
code : "mnote_pi_rust_local_file_read_failed" ,
message : error instanceof Error ? error.message : String ( error ),
transport : "pi-rust-native-fs" ,
contextFile : CONTEXT_FILE ,
}, true );
}
}
function applyTextOperations ( current : string , operations : unknown ) : string {
if ( ! Array . isArray ( operations )) throw new Error ( "operations 必须是数组" );
let next = current ;
for ( const operation of operations ) {
if ( ! operation || typeof operation !== "object" || Array . isArray ( operation )) {
throw new Error ( "operation 格式无效" );
}
const op = operation as Record < string , unknown >;
const type = stringField ( op , "type" ) || stringField ( op , "op" );
if ( type === "replace" ) {
const oldText = typeof op . oldText === "string" ? op.oldText : typeof op . old_text === "string" ? op . old_text : "" ;
const newText = typeof op . newText === "string" ? op.newText : typeof op . new_text === "string" ? op . new_text : "" ;
if ( ! oldText ) throw new Error ( "replace operation 缺少 oldText" );
const index = next . indexOf ( oldText );
if ( index < 0 ) throw new Error ( "replace operation 未找到 oldText" );
next = next . slice ( 0 , index ) + newText + next . slice ( index + oldText . length );
} else if ( type === "append" ) {
const text = typeof op . text === "string" ? op . text : "" ;
next += text ;
} else {
throw new Error ( `不支持的 operation: ${ type || "unknown" } ` );
}
}
return next ;
}
function executeNativeLocalFilePatch ( params : unknown ) {
try {
const input = params && typeof params === "object" && ! Array . isArray ( params )
? params as Record < string , unknown >
: {};
const context = readContextSnapshot ();
const rootUri = sessionRootUri ( context , input );
const relativePath = sessionRelativePath ( context , input , { preferFolder : true });
if ( ! rootUri ) throw new Error ( "写入文件缺少 rootUri" );
if ( ! relativePath ) throw new Error ( "写入文件缺少 relativePath/path" );
const resolved = resolveAllowedTarget ( context , rootUri , relativePath , { allowMissing : true });
const before = fs . existsSync ( resolved . target ) ? fs . readFileSync ( resolved . target , "utf8" ) : "" ;
const next = typeof input . content === "string"
? input.content
: applyTextOperations ( before , input . operations );
fs . mkdirSync ( path . dirname ( resolved . target ), { recursive : true });
fs . writeFileSync ( resolved . target , next , "utf8" );
const stat = fs . statSync ( resolved . target );
return toolResult ({
ok : true ,
rootUri ,
relativePath ,
path : resolved.target ,
beforeFileVersion : `pi-rust-native-before- ${ before . length } ` ,
afterFileVersion : `pi-rust-native- ${ Math . trunc ( stat . mtimeMs ) } - ${ stat . size } ` ,
oldSize : before.length ,
newSize : next.length ,
diffSummary : before === next ? "no_changes" : `bytes_delta= ${ next . length - before . length } ` ,
refresh : "mnote local-folder watcher / document-session external refresh" ,
transport : "pi-rust-native-fs" ,
contextFile : CONTEXT_FILE ,
});
} catch ( error ) {
return toolResult ({
ok : false ,
code : "mnote_pi_rust_local_file_patch_failed" ,
message : error instanceof Error ? error.message : String ( error ),
transport : "pi-rust-native-fs" ,
contextFile : CONTEXT_FILE ,
}, true );
}
}
2026-07-10 10:54:34 +08:00
function executeNativeSelectionRead() {
try {
const context = readContextSnapshot ();
2026-07-11 20:39:18 +08:00
const selection = selectedContextPart ( context , "selection" );
2026-07-10 10:54:34 +08:00
return toolResult ({
ok : true ,
2026-07-11 20:39:18 +08:00
selection : selection ?? null ,
text : stringField ( selection || {}, "text" ),
2026-07-10 10:54:34 +08:00
selectionSource : "mnote_sidebar_host_snapshot" ,
rootUri : context.rootUri ?? null ,
pagePath : context.pagePath ?? null ,
contextRefs : context.contextRefs ?? [],
transport : "pi-rust-native-context-file" ,
});
} catch ( error ) {
return toolResult ({
ok : false ,
code : "mnote_pi_rust_selection_read_failed" ,
message : error instanceof Error ? error.message : String ( error ),
transport : "pi-rust-native-context-file" ,
}, true );
}
}
function executeNativeAllowedRootsDescribe() {
try {
const context = readContextSnapshot ();
return toolResult ({
ok : true ,
allowedRoots : context.allowedRoots ?? { roots : [] },
rootUri : context.rootUri ?? null ,
pagePath : context.pagePath ?? null ,
2026-07-11 20:39:18 +08:00
workspaceId : context.workspaceId ?? null ,
contextRefs : context.contextRefs ?? [],
selectedContext : context.selectedContext ?? null ,
2026-07-10 10:54:34 +08:00
primaryRootPath : context.primaryRootPath ?? process . cwd (),
transport : "pi-rust-native-context-file" ,
});
} catch ( error ) {
return toolResult ({
ok : false ,
code : "mnote_pi_rust_allowed_roots_read_failed" ,
message : error instanceof Error ? error.message : String ( error ),
transport : "pi-rust-native-context-file" ,
}, true );
}
}
2026-07-11 20:39:18 +08:00
function isPiRustNativeRuntime () : boolean {
return runtimeImplementation () === "pi-rust" || Boolean ( CONTEXT_FILE );
}
2026-07-10 10:54:34 +08:00
function executeNativeTool ( tool : MnoteToolManifestEntry , params : unknown ) {
2026-07-11 20:39:18 +08:00
if ( ! isPiRustNativeRuntime () || toolPolicy ( tool . mnoteName ) !== "allow" ) return undefined ;
2026-07-10 10:54:34 +08:00
if ( tool . mnoteName === "mnote.current_page.read" ) return executeNativeCurrentPageRead ( params );
if ( tool . mnoteName === "mnote.selection.read" ) return executeNativeSelectionRead ();
if ( tool . mnoteName === "mnote.allowed_roots.describe" ) return executeNativeAllowedRootsDescribe ();
2026-07-11 20:39:18 +08:00
if ( tool . mnoteName === "mnote.local_file.read" ) return executeNativeLocalFileRead ( params );
if ( tool . mnoteName === "mnote.local_file.patch" ) return executeNativeLocalFilePatch ( params );
2026-07-10 10:54:34 +08:00
return undefined ;
}
async function callMnote ( toolName : string , params : unknown ) {
2026-07-11 20:39:18 +08:00
const sessionId = bridgeSessionId ();
if ( ! sessionId ) {
return toolResult ({
ok : false ,
code : "mnote_pi_bridge_session_id_missing" ,
message : "MNote Pi bridge 缺少有效的 pi_lab sessionId;已忽略 Pi runtime provider session id。" ,
toolName ,
transport : "mnote-bridge-http" ,
}, true );
}
const response = await fetch ( ` ${ bridgeBaseUrl () } /api/page-ai/pi/tool-call-bridge` , {
2026-07-10 10:54:34 +08:00
method : "POST" ,
headers : {
"content-type" : "application/json" ,
2026-07-11 20:39:18 +08:00
"x-mnote-pi-lab-bridge-token" : bridgeToken (),
2026-07-10 10:54:34 +08:00
},
2026-07-11 20:39:18 +08:00
body : JSON.stringify ({ sessionId , toolName , params : params || {} }),
2026-07-10 10:54:34 +08:00
});
const payload = await response . json (). catch (() => ({ ok : false , code : "bad_json" }));
2026-07-11 20:39:18 +08:00
const result = ( payload as Record < string , unknown >). result || payload ;
const text = JSON . stringify ( modelSafeBridgeResult ( toolName , result ), null , 2 );
2026-07-10 10:54:34 +08:00
return {
content : [{ type : "text" , text }],
details : payload ,
};
}
2026-07-11 20:39:18 +08:00
function modelSafeBridgeResult ( toolName : string , value : unknown ) : unknown {
if ( toolName !== "mnote.knowledge_rag.query" || ! value || typeof value !== "object" || Array . isArray ( value )) {
return value ;
}
const result = { ...( value as Record < string , unknown >) };
delete result . uiCitations ;
delete result . citationMarkdowns ;
delete result . citationRendering ;
result . uiCitationCount = Array . isArray (( value as Record < string , unknown >). uiCitations )
? (( value as Record < string , unknown >). uiCitations as unknown []). length
: 0 ;
result . uiCitationDelivery = "Clickable citations are retained in tool details for the MNote UI and omitted from model context." ;
return result ;
}
2026-07-10 10:54:34 +08:00
function stableJson ( value : unknown ) : string {
if ( value === null || typeof value !== "object" ) return JSON . stringify ( value );
if ( Array . isArray ( value )) return `[ ${ value . map ( stableJson ). join ( "," ) } ]` ;
const record = value as Record < string , unknown >;
return `{ ${ Object . keys ( record ). sort (). map (( key ) => ` ${ JSON . stringify ( key ) } : ${ stableJson ( record [ key ]) } ` ). join ( "," ) } }` ;
}
function stableHash ( value : string ) : string {
let hash = 5381 >>> 0 ;
for ( let index = 0 ; index < value . length ; index += 1 ) {
hash = ((( hash << 5 ) + hash ) + value . charCodeAt ( index )) >>> 0 ;
}
return hash . toString ( 16 );
}
function paramsHash ( params : unknown ) : string {
const copy = { ...(( params || {}) as Record < string , unknown >) };
delete copy . mnoteApproval ;
delete copy . mnote_approval ;
return stableHash ( stableJson ( copy ));
}
function toolPolicy ( toolName : string ) : string {
2026-07-11 20:39:18 +08:00
const contextPolicies = activeContextSnapshot () ? . toolPolicies ;
let contextValue = contextPolicies && typeof contextPolicies === "object" && ! Array . isArray ( contextPolicies )
2026-07-10 10:54:34 +08:00
? ( contextPolicies as Record < string , unknown >)[ toolName ]
: undefined ;
const value = contextValue ?? TOOL_POLICIES [ toolName ];
return typeof value === "string" && value . trim () ? value . trim () : "allow" ;
}
function runtimeImplementation () : string {
2026-07-11 20:39:18 +08:00
const fromContext = activeContextSnapshot ();
return fromContext ? stringField ( fromContext , "runtimeImplementation" ) || RUNTIME_IMPL : RUNTIME_IMPL ;
2026-07-10 10:54:34 +08:00
}
async function requestMnoteApprovalViaBridge ( request : Record < string , unknown >) : Promise < Record < string , unknown >> {
2026-07-11 20:39:18 +08:00
const sessionId = bridgeSessionId ();
if ( ! sessionId ) {
return {
ok : false ,
cancelled : true ,
code : "mnote_pi_bridge_session_id_missing" ,
message : "MNote Pi bridge 缺少有效的 pi_lab sessionId" ,
};
}
const response = await fetch ( ` ${ bridgeBaseUrl () } /api/page-ai/pi/ui-request-bridge` , {
2026-07-10 10:54:34 +08:00
method : "POST" ,
headers : {
"content-type" : "application/json" ,
2026-07-11 20:39:18 +08:00
"x-mnote-pi-lab-bridge-token" : bridgeToken (),
2026-07-10 10:54:34 +08:00
},
2026-07-11 20:39:18 +08:00
body : JSON.stringify ({ sessionId , ... request }),
2026-07-10 10:54:34 +08:00
});
return await response . json (). catch (() => ({ ok : false , cancelled : true , code : "bad_json" }));
}
async function requestMnoteApproval (
ctx : ExtensionContext | undefined ,
toolCallId : string ,
toolName : string ,
label : string ,
params : unknown ,
) : Promise < null | Record < string , unknown >> {
if ( toolPolicy ( toolName ) !== "ask" ) return null ;
const approvalId = toolCallId || `approval_ ${ Date . now () } _ ${ Math . random (). toString ( 16 ). slice ( 2 ) } ` ;
const approval = { approvalId , toolName , paramsHash : paramsHash ( params ) };
const summary = JSON . stringify ( params || {}, null , 2 ). slice ( 0 , 1200 );
const message = ` ${ label } \ n ${ toolName } \ n \ n ${ summary } ` ;
if ( ctx ? . hasUI && ctx . ui && typeof ctx . ui . confirm === "function" ) {
const result = await ctx . ui . confirm ( "审批 Pi 工具调用" , message , { timeout : 60000 });
if ( result === true ) return approval ;
return { ... approval , denied : true };
}
const result = await requestMnoteApprovalViaBridge ({
id : approvalId ,
method : "confirm" ,
title : "审批 Pi 工具调用" ,
message ,
timeoutMs : 60000 ,
mnoteApproval : approval ,
});
if ( result && result . confirmed === true ) return approval ;
return { ... approval , denied : true };
}
function register ( pi : ExtensionAPI , tool : MnoteToolManifestEntry ) {
pi . registerTool ({
name : tool.piName ,
label : tool.label ,
description : tool.description ,
promptSnippet : ` ${ tool . label } : ${ tool . description } ` ,
parameters : { type : "object" , additionalProperties : true },
execute ( toolCallId : unknown , params : unknown , _signal : unknown , _onUpdate : unknown , ctx : ExtensionContext | undefined ) {
if ( toolPolicy ( tool . mnoteName ) === "deny" ) {
return toolResult ({
ok : false ,
code : "mnote_tool_denied_by_permission_mode" ,
message : `当前 Pi 模式禁止调用工具 ${ tool . mnoteName } ` ,
toolName : tool.mnoteName ,
}, true );
}
const nativeResult = executeNativeTool ( tool , params );
if ( nativeResult ) return nativeResult ;
2026-07-11 20:39:18 +08:00
if ( runtimeImplementation () === "pi-rust" && ! HTTP_BRIDGE_AVAILABLE && ! bridgeToken ()) {
2026-07-10 10:54:34 +08:00
return toolResult ({
ok : false ,
code : "mnote_pi_rust_service_bridge_unavailable" ,
2026-07-11 20:39:18 +08:00
message : ` ${ tool . mnoteName } 缺少 MNote Pi bridge token; Pi Rust 当前应通过原生文件工具或 MNote 受控 bridge 调用。` ,
2026-07-10 10:54:34 +08:00
toolName : tool.mnoteName ,
runtimeImplementation : runtimeImplementation (),
}, true );
}
return executeBridgeTool ( toolCallId , params , ctx , tool );
},
});
}
async function executeBridgeTool (
toolCallId : unknown ,
params : unknown ,
ctx : ExtensionContext | undefined ,
tool : MnoteToolManifestEntry ,
) {
2026-07-11 20:39:18 +08:00
const nextParams = contextualToolParams ( tool . mnoteName , params );
2026-07-10 10:54:34 +08:00
const approval = await requestMnoteApproval ( ctx , String ( toolCallId || "" ), tool . mnoteName , tool . label , nextParams );
if ( approval && approval . denied ) {
return {
content : [{ type : "text" , text : JSON.stringify ({ ok : false , code : "mnote_tool_approval_cancelled" , message : "用户拒绝或未完成 MNote 工具审批" , toolName : tool.mnoteName }, null , 2 ) }],
details : { ok : false , code : "mnote_tool_approval_cancelled" , toolName : tool.mnoteName },
};
}
if ( toolPolicy ( tool . mnoteName ) === "ask" ) {
nextParams . mnoteApproval = {
... approval ,
confirmed : true ,
method : "extension_ui_confirm" ,
toolCallId : String ( toolCallId || "" ),
toolName : tool.mnoteName ,
approvedAt : new Date (). toISOString (),
};
}
return callMnote ( tool . mnoteName , nextParams );
}
export default function mnotePi ( pi : ExtensionAPI ) {
pi . on ? .( "input" , captureInputContext );
for ( const tool of TOOLS ) register ( pi , tool );
}