localsage 1.0.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -6
- package/bin/localsage +10 -2
- package/cli.js +6 -3
- package/index.js +7 -1
- package/package.json +1 -1
- package/src/knowledge.js +42 -8
package/README.md
CHANGED
|
@@ -21,19 +21,23 @@ npm install -g localsage
|
|
|
21
21
|
|
|
22
22
|
## Quick Start
|
|
23
23
|
|
|
24
|
-
1.
|
|
24
|
+
1. Go to any folder that contains your documents (PDF, TXT, MD) — or create a `knowledge` subfolder for them:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
|
|
28
|
-
cp your-document.pdf knowledge/
|
|
27
|
+
cd my-documents
|
|
29
28
|
```
|
|
30
29
|
|
|
31
|
-
2. Start LocalSage:
|
|
30
|
+
2. Start LocalSage right there:
|
|
32
31
|
|
|
33
32
|
```bash
|
|
34
33
|
localsage
|
|
35
34
|
```
|
|
36
35
|
|
|
36
|
+
LocalSage picks the knowledge source automatically:
|
|
37
|
+
1. the path you pass with `-k / --knowledge`, if given
|
|
38
|
+
2. otherwise a `knowledge/` subfolder in the current directory, if it exists
|
|
39
|
+
3. otherwise **the current directory itself** — so you can just `cd` into your docs folder and run `localsage`
|
|
40
|
+
|
|
37
41
|
The first run downloads the model (~500 MB) to your Hugging Face cache. After that, it works fully offline.
|
|
38
42
|
|
|
39
43
|
3. Ask questions about your documents!
|
|
@@ -50,7 +54,8 @@ The first run downloads the model (~500 MB) to your Hugging Face cache. After th
|
|
|
50
54
|
localsage [options]
|
|
51
55
|
|
|
52
56
|
-m, --model <model> Model to use (default: "onnx-community/Qwen2.5-0.5B-Instruct")
|
|
53
|
-
-k, --knowledge <path> Knowledge folder path (default:
|
|
57
|
+
-k, --knowledge <path> Knowledge folder path (default: ./knowledge if it
|
|
58
|
+
exists, else the current directory)
|
|
54
59
|
--no-pdf Disable PDF support
|
|
55
60
|
--max-tokens <number> Max tokens for response (default: 200)
|
|
56
61
|
--temperature <number> Temperature for generation (default: 0.7)
|
|
@@ -98,7 +103,7 @@ main();
|
|
|
98
103
|
|
|
99
104
|
## How It Works
|
|
100
105
|
|
|
101
|
-
1. **Knowledge loading** — reads all TXT, MD, and PDF files from the knowledge folder (plus an optional `knowledge.txt` in the working directory
|
|
106
|
+
1. **Knowledge loading** — reads all TXT, MD, and PDF files from the resolved knowledge folder (explicit `-k` path → `./knowledge` subfolder → current directory), plus an optional `knowledge.txt` in the working directory
|
|
102
107
|
2. **AI processing** — runs the Qwen model locally with `@huggingface/transformers`
|
|
103
108
|
3. **History** — saves the conversation to `history.json` in the working directory
|
|
104
109
|
|
package/bin/localsage
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
|
|
3
|
+
// LocalSage CLI entry point.
|
|
4
|
+
// cli.js exports runCLI and only auto-runs when it is the main module —
|
|
5
|
+
// which it is NOT when launched through this bin wrapper. So we must
|
|
6
|
+
// invoke it explicitly here.
|
|
7
|
+
const runCLI = require('../cli.js');
|
|
8
|
+
|
|
9
|
+
runCLI().catch((error) => {
|
|
10
|
+
console.error('Fatal Error:', error.message);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
package/cli.js
CHANGED
|
@@ -7,10 +7,13 @@ const LocalSage = require('./index');
|
|
|
7
7
|
|
|
8
8
|
program
|
|
9
9
|
.name('localsage')
|
|
10
|
-
.version('1.0.
|
|
10
|
+
.version('1.0.3')
|
|
11
11
|
.description('LocalSage — local, offline AI assistant with PDF & document knowledge')
|
|
12
12
|
.option('-m, --model <model>', 'Model name', 'onnx-community/Qwen2.5-0.5B-Instruct')
|
|
13
|
-
.option(
|
|
13
|
+
.option(
|
|
14
|
+
'-k, --knowledge <path>',
|
|
15
|
+
'Knowledge folder path (default: ./knowledge if it exists, else the current directory)'
|
|
16
|
+
)
|
|
14
17
|
.option('--no-pdf', 'Disable PDF support')
|
|
15
18
|
.option('--max-tokens <number>', 'Max tokens for response', '200')
|
|
16
19
|
.option('--temperature <number>', 'Temperature for generation', '0.7')
|
|
@@ -33,7 +36,7 @@ async function runCLI() {
|
|
|
33
36
|
console.log(chalk.green.bold(' 🧙 LocalSage — your local AI assistant '));
|
|
34
37
|
console.log(chalk.blue('='.repeat(60)));
|
|
35
38
|
console.log(chalk.gray(` Model: ${options.model}`));
|
|
36
|
-
console.log(chalk.gray(` Knowledge: ${
|
|
39
|
+
console.log(chalk.gray(` Knowledge: ${assistant.getResolvedKnowledgePath()}`));
|
|
37
40
|
console.log(chalk.gray(` PDF Support: ${options.pdf !== false ? 'Enabled' : 'Disabled'}`));
|
|
38
41
|
console.log(chalk.blue('='.repeat(60)));
|
|
39
42
|
console.log(chalk.gray(' Commands:'));
|
package/index.js
CHANGED
|
@@ -6,7 +6,9 @@ class LocalSage {
|
|
|
6
6
|
constructor(options = {}) {
|
|
7
7
|
this.options = {
|
|
8
8
|
model: options.model || 'onnx-community/Qwen2.5-0.5B-Instruct',
|
|
9
|
-
|
|
9
|
+
// null = auto-resolve: ./knowledge if it exists, else the current
|
|
10
|
+
// working directory the user ran the command from
|
|
11
|
+
knowledgePath: options.knowledgePath || null,
|
|
10
12
|
maxTokens: options.maxTokens || 200,
|
|
11
13
|
temperature: options.temperature || 0.7,
|
|
12
14
|
topP: options.topP || 0.9,
|
|
@@ -85,6 +87,10 @@ ${knowledge}`;
|
|
|
85
87
|
return this.knowledge.getKnowledge();
|
|
86
88
|
}
|
|
87
89
|
|
|
90
|
+
getResolvedKnowledgePath() {
|
|
91
|
+
return this.knowledge.getResolvedPath();
|
|
92
|
+
}
|
|
93
|
+
|
|
88
94
|
async reloadKnowledge() {
|
|
89
95
|
await this.knowledge.loadKnowledge();
|
|
90
96
|
}
|
package/package.json
CHANGED
package/src/knowledge.js
CHANGED
|
@@ -2,17 +2,41 @@ const fs = require('fs-extra');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
4
|
class KnowledgeManager {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* @param {string|null} knowledgePath - explicit path from the user, or null
|
|
7
|
+
* to auto-resolve: use ./knowledge if it exists, otherwise the
|
|
8
|
+
* current working directory itself.
|
|
9
|
+
*/
|
|
10
|
+
constructor(knowledgePath = null) {
|
|
11
|
+
this.requestedPath = knowledgePath;
|
|
12
|
+
this.knowledgePath = null; // resolved in loadKnowledge()
|
|
7
13
|
this.knowledge = '';
|
|
8
14
|
}
|
|
9
15
|
|
|
16
|
+
resolvePath() {
|
|
17
|
+
if (this.requestedPath) {
|
|
18
|
+
return path.resolve(this.requestedPath);
|
|
19
|
+
}
|
|
20
|
+
const sub = path.join(process.cwd(), 'knowledge');
|
|
21
|
+
if (fs.pathExistsSync(sub)) {
|
|
22
|
+
return sub;
|
|
23
|
+
}
|
|
24
|
+
// Fall back to the directory the user ran the command from
|
|
25
|
+
return process.cwd();
|
|
26
|
+
}
|
|
27
|
+
|
|
10
28
|
async loadKnowledge() {
|
|
29
|
+
this.knowledgePath = this.resolvePath();
|
|
11
30
|
let combinedKnowledge = '';
|
|
12
31
|
|
|
13
|
-
// Load
|
|
32
|
+
// Load a standalone knowledge.txt from the working directory —
|
|
33
|
+
// but only if it isn't already inside the folder we're about to scan
|
|
34
|
+
// (avoids loading it twice when knowledgePath === cwd).
|
|
14
35
|
const txtFile = path.join(process.cwd(), 'knowledge.txt');
|
|
15
|
-
if (
|
|
36
|
+
if (
|
|
37
|
+
path.dirname(txtFile) !== this.knowledgePath &&
|
|
38
|
+
(await fs.pathExists(txtFile))
|
|
39
|
+
) {
|
|
16
40
|
try {
|
|
17
41
|
combinedKnowledge += await fs.readFile(txtFile, 'utf-8');
|
|
18
42
|
console.log('Knowledge loaded from knowledge.txt');
|
|
@@ -21,9 +45,8 @@ class KnowledgeManager {
|
|
|
21
45
|
}
|
|
22
46
|
}
|
|
23
47
|
|
|
24
|
-
// Load from the knowledge folder
|
|
25
48
|
if (await fs.pathExists(this.knowledgePath)) {
|
|
26
|
-
console.log(`Loading knowledge from
|
|
49
|
+
console.log(`Loading knowledge from: ${this.knowledgePath}`);
|
|
27
50
|
const folderKnowledge = await this.loadFolder(this.knowledgePath);
|
|
28
51
|
if (folderKnowledge) {
|
|
29
52
|
if (combinedKnowledge) {
|
|
@@ -31,6 +54,8 @@ class KnowledgeManager {
|
|
|
31
54
|
}
|
|
32
55
|
combinedKnowledge += folderKnowledge;
|
|
33
56
|
}
|
|
57
|
+
} else {
|
|
58
|
+
console.log(`Knowledge path not found: ${this.knowledgePath}`);
|
|
34
59
|
}
|
|
35
60
|
|
|
36
61
|
this.knowledge = combinedKnowledge;
|
|
@@ -50,7 +75,13 @@ class KnowledgeManager {
|
|
|
50
75
|
|
|
51
76
|
for (const file of files) {
|
|
52
77
|
const filePath = path.join(folderPath, file);
|
|
53
|
-
|
|
78
|
+
|
|
79
|
+
let stat;
|
|
80
|
+
try {
|
|
81
|
+
stat = await fs.stat(filePath);
|
|
82
|
+
} catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
54
85
|
if (stat.isDirectory()) continue;
|
|
55
86
|
|
|
56
87
|
const ext = path.extname(file).toLowerCase();
|
|
@@ -80,7 +111,6 @@ class KnowledgeManager {
|
|
|
80
111
|
|
|
81
112
|
async extractPDF(filePath) {
|
|
82
113
|
try {
|
|
83
|
-
// Lazy-require so the app still starts if pdf-parse is missing/broken
|
|
84
114
|
const pdfParse = require('pdf-parse');
|
|
85
115
|
const dataBuffer = await fs.readFile(filePath);
|
|
86
116
|
const data = await pdfParse(dataBuffer);
|
|
@@ -91,6 +121,10 @@ class KnowledgeManager {
|
|
|
91
121
|
}
|
|
92
122
|
}
|
|
93
123
|
|
|
124
|
+
getResolvedPath() {
|
|
125
|
+
return this.knowledgePath || this.resolvePath();
|
|
126
|
+
}
|
|
127
|
+
|
|
94
128
|
getKnowledge(truncate = true, maxLength = 4000) {
|
|
95
129
|
if (!truncate || this.knowledge.length <= maxLength) {
|
|
96
130
|
return this.knowledge;
|