Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ cd ../llamaindex-basic && npm install && npm run start
# 🔌 LangChain-Style Usage

```ts
import { mountPack } from '@knolo/core';
import { mountPack } from '@knolo/core/node';
import { KnoLoRetriever } from '@knolo/langchain';

const pack = await mountPack({ src: './dist/knowledge.knolo' });
Expand All @@ -149,7 +149,7 @@ for (const doc of docs) {
# 🦙 LlamaIndex-Style Usage

```ts
import { mountPack } from '@knolo/core';
import { mountPack } from '@knolo/core/node';
import { KnoLoRetriever } from '@knolo/llamaindex';

const pack = await mountPack({ src: './dist/knowledge.knolo' });
Expand All @@ -165,6 +165,28 @@ for (const hit of nodes) {

---

# 📱 Expo / React Native Mounting

Use the runtime-safe entrypoint (`@knolo/core`) and pass URL/bytes.
For local filesystem paths in Node.js, use `@knolo/core/node`.

```ts
import { mountPack } from '@knolo/core';

const ab = await (await fetch(PACK_URL)).arrayBuffer();
const pack = await mountPack({ src: new Uint8Array(ab) });
```

Node-only local path usage:

```ts
import { mountPack } from '@knolo/core/node';

const pack = await mountPack({ src: './dist/knowledge.knolo' });
```

---

# 🔀 Hybrid Retrieval (Optional)

Lexical-first. Semantic rerank second.
Expand Down
17 changes: 14 additions & 3 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,30 @@ You can write it to disk or store it in object storage.

## 2️⃣ Mount a Pack

### Node.js (local path convenience)

```ts
import { mountPack } from "@knolo/core";
import { mountPack } from "@knolo/core/node";

const pack = await mountPack({
src: "./dist/knowledge.knolo"
});
```

### React Native / Expo (URL or bytes)

```ts
import { mountPack } from "@knolo/core";

const ab = await (await fetch(PACK_URL)).arrayBuffer();
const pack = await mountPack({ src: new Uint8Array(ab) });
```

You can mount from:

* File path
* URL string (runtime-safe entry)
* Buffer / Uint8Array
* Remote fetch response
* Local file path in Node via `@knolo/core/node`
* Object storage download

Mount-time validation ensures:
Expand Down
13 changes: 10 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,25 @@
],
"exports": {
".": {
"react-native": "./dist/index.js",
"browser": "./dist/index.js",
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./node": {
"import": "./dist/node.js",
"types": "./dist/node.d.ts"
}
},
"sideEffects": false,
"scripts": {
"build": "tsc -p tsconfig.json",
"prepublishOnly": "npm run build",
"smoke": "node scripts/smoke.mjs",
"test": "npm run build && node scripts/test.mjs",
"format": "prettier --write src/agent.ts src/pack.ts src/builder.ts src/index.ts scripts/test.mjs ../../README.md",
"format:check": "prettier --check src/agent.ts src/pack.ts src/builder.ts src/index.ts scripts/test.mjs ../../README.md"
"test": "npm run build && node scripts/check-runtime-no-node.mjs && node scripts/test.mjs",
"format": "prettier --write src/agent.ts src/pack.ts src/pack.runtime.ts src/pack.node.ts src/node.ts src/builder.ts src/index.ts scripts/test.mjs scripts/check-runtime-no-node.mjs ../../README.md README.md",
"format:check": "prettier --check src/agent.ts src/pack.ts src/pack.runtime.ts src/pack.node.ts src/node.ts src/builder.ts src/index.ts scripts/test.mjs scripts/check-runtime-no-node.mjs ../../README.md README.md",
"check:runtime-no-node": "node scripts/check-runtime-no-node.mjs"
},
"devDependencies": {
"@types/node": "^20.11.0",
Expand Down
17 changes: 17 additions & 0 deletions packages/core/scripts/check-runtime-no-node.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';

const distIndexPath = fileURLToPath(new URL('../dist/index.js', import.meta.url));
const runtimeBundle = await readFile(distIndexPath, 'utf8');

const forbidden = ['node:fs', 'fs/promises', 'node:path'];
for (const token of forbidden) {
assert.equal(
runtimeBundle.includes(token),
false,
`Runtime entry must not include Node stdlib reference: ${token}`
);
}

console.log('Runtime bundle contains no Node stdlib specifiers.');
11 changes: 6 additions & 5 deletions packages/core/scripts/test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
validateRouteDecisionV1,
selectAgentIdFromRouteDecisionV1,
} from '../dist/index.js';
import { mountPack as mountPackNode } from '../dist/node.js';

const execFileAsync = promisify(execFile);

Expand Down Expand Up @@ -409,20 +410,20 @@ async function testMountPackFromLocalPathAndFileUrl() {
try {
await writeFile(packPath, bytes);

const fromPath = await mountPack({ src: packPath });
const fromPath = await mountPackNode({ src: packPath });
const pathHits = query(fromPath, 'local path loading', { topK: 1 });
assert.equal(
pathHits[0]?.source,
'local-doc',
'expected mountPack to load plain filesystem paths'
'expected @knolo/core/node mountPack to load plain filesystem paths'
);

const fromFileUrl = await mountPack({ src: pathToFileURL(packPath).href });
const fromFileUrl = await mountPackNode({ src: pathToFileURL(packPath).href });
const fileUrlHits = query(fromFileUrl, 'local path loading', { topK: 1 });
assert.equal(
fileUrlHits[0]?.source,
'local-doc',
'expected mountPack to load file:// URLs'
'expected @knolo/core/node mountPack to load file:// URLs'
);
} finally {
await rm(tmpDir, { recursive: true, force: true });
Expand Down Expand Up @@ -1251,7 +1252,7 @@ async function testCliEmbedsAgentsFromDirectory() {
agentsDir,
]);

const pack = await mountPack({ src: outPath });
const pack = await mountPackNode({ src: outPath });
assert.deepEqual(
listAgents(pack),
['backend.agent', 'mobile.agent'],
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// src/index.ts
export { mountPack, hasSemantic } from './pack.js';
export { mountPack, hasSemantic } from './pack.runtime.js';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve Node path mounting on default core entry

Switching @knolo/core to export mountPack from pack.runtime removes filesystem-path support for Node callers that still import the default entry, so existing flows now fail at runtime when they pass local paths. I checked packages/cli/bin/knolo.mjs (cmdQuery uses core.mountPack({ src: packPath })) and both adapter constructors in packages/langchain/src/index.js and packages/llamaindex/src/index.js (mountPack({ src: this.packPath })), and these now hit the React Native URL-only error path instead of loading local files.

Useful? React with 👍 / 👎.

export {
query,
lexConfidence,
Expand All @@ -23,7 +23,7 @@ export {
validateAgentRegistry,
validateAgentDefinition,
} from './agent.js';
export type { MountOptions, PackMeta, Pack } from './pack.js';
export type { MountOptions, PackMeta, Pack } from './pack.runtime.js';
export type { QueryOptions, Hit } from './query.js';
export type { ContextPatch } from './patch.js';
export type { BuildInputDoc, BuildPackOptions } from './builder.js';
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { mountPack, hasSemantic } from './pack.node.js';
export type { MountOptions, PackMeta, Pack } from './pack.node.js';
42 changes: 42 additions & 0 deletions packages/core/src/pack.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { mountPackFromBuffer, toArrayBuffer } from './pack.runtime.js';
import type { MountOptions, Pack } from './pack.runtime.js';
export { hasSemantic } from './pack.runtime.js';
export type { MountOptions, PackMeta, Pack } from './pack.runtime.js';

export async function mountPack(opts: MountOptions): Promise<Pack> {
const buf = await resolveToBuffer(opts.src);
return mountPackFromBuffer(buf);
}

async function resolveToBuffer(src: MountOptions['src']): Promise<ArrayBuffer> {
if (typeof src === 'string') {
if (isLikelyLocalPath(src)) {
const { readFile } = await import('node:fs/promises');
const filePath = src.startsWith('file://')
? decodeURIComponent(new URL(src).pathname)
: src;
const data = await readFile(filePath);
return data.buffer.slice(
data.byteOffset,
data.byteOffset + data.byteLength
);
}
const res = await fetch(src);
return await res.arrayBuffer();
}
return toArrayBuffer(src);
}

function isLikelyLocalPath(value: string): boolean {
if (value.startsWith('file://')) return true;
if (
value.startsWith('./') ||
value.startsWith('../') ||
value.startsWith('/') ||
value.startsWith('~')
)
return true;
if (/^[A-Za-z]:[\\/]/.test(value)) return true;
if (/^[A-Za-z][A-Za-z\d+.-]*:/.test(value)) return false;
return true;
}
Loading
Loading