naisys 1.0.2 → 1.1.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.
- package/README.md +23 -10
- package/dist/apps/llmail.js +7 -1
- package/dist/apps/llmynx.js +2 -2
- package/dist/command/commandHandler.js +91 -40
- package/dist/command/commandLoop.js +17 -15
- package/dist/command/commandProtection.js +49 -0
- package/dist/command/promptBuilder.js +8 -1
- package/dist/command/shellCommand.js +15 -1
- package/dist/command/shellWrapper.js +21 -18
- package/dist/config.js +35 -12
- package/dist/llm/contextManager.js +3 -3
- package/dist/utils/enums.js +9 -0
- package/dist/utils/utilities.js +3 -0
- package/package.json +10 -8
- package/dist/__tests__/utils/output.test.js +0 -22
- package/dist/__tests__/utils/utilities.test.js +0 -42
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
## NAISYS (Node.js Autonomous Intelligence System)
|
|
2
2
|
|
|
3
|
-
NAISYS
|
|
3
|
+
NAISYS acts as a proxy shell between LLM(s) and a real shell. The goal is to see how far a LLM can
|
|
4
4
|
get into writing a website from scratch as well as work with other LLM agents on the same project. Trying to figure
|
|
5
|
-
out what works and what doesn't when it comes to 'cognitive architectures'. NAISYS isn't
|
|
5
|
+
out what works and what doesn't when it comes to 'cognitive architectures' for autonomy. NAISYS isn't
|
|
6
6
|
limited to websites, but it seemed like a good place to start.
|
|
7
7
|
|
|
8
|
-
Since the LLM has a limited context, NAISYS
|
|
8
|
+
Since the LLM has a limited context, NAISYS takes this into account and helps the LLM
|
|
9
9
|
perform 'context friendly' operations. For example reading/writing a file can't use a typical editor like
|
|
10
10
|
vim or nano so point the LLM to use cat to read/write files in a single operation.
|
|
11
11
|
|
|
12
|
-
[NPM](https://www.npmjs.com/package/naisys) | [Website](https://naisys.org) | [Discord](https://discord.gg/JBUPWSbaEt)
|
|
12
|
+
[NPM](https://www.npmjs.com/package/naisys) | [Website](https://naisys.org) | [Discord](https://discord.gg/JBUPWSbaEt) | [Demo Video](https://www.youtube.com/watch?v=Ttya3ixjumo)
|
|
13
13
|
|
|
14
14
|
#### Node.js is used to create a simple proxy shell environment for the LLM that
|
|
15
15
|
|
|
@@ -61,7 +61,7 @@ title: Software Engineer
|
|
|
61
61
|
|
|
62
62
|
# The model to use for console interactions
|
|
63
63
|
# (gpt4turbo, gpt4turbo, gemini-pro, claude3sonnet, claude3opus, local)
|
|
64
|
-
|
|
64
|
+
shellModel: claude3sonnet
|
|
65
65
|
|
|
66
66
|
# The model to use for llmynx, pre-processing websites to fit into a smaller context
|
|
67
67
|
webModel: gpt3turbo
|
|
@@ -76,6 +76,11 @@ agentPrompt: |
|
|
|
76
76
|
You can use PHP as a way to share layout across pages and reduce duplication.
|
|
77
77
|
Careful when creating new files that what you are creating is not already there.
|
|
78
78
|
|
|
79
|
+
# The number of tokens you want to limit a session to, independent of the LLM token max itself
|
|
80
|
+
# A lower max relies more on the LLM ending the session with good enough notes to not get lost when the session restarts
|
|
81
|
+
# A higher max allows the LLM to do more without losing track, but is more expensive
|
|
82
|
+
tokenMax: 5000
|
|
83
|
+
|
|
79
84
|
# The number of seconds to pause after each console interaction for debugging and rate limiting
|
|
80
85
|
# No value or zero means wait indefinitely (debug driven)
|
|
81
86
|
debugPauseSeconds: 5
|
|
@@ -87,6 +92,12 @@ wakeOnMessage: false
|
|
|
87
92
|
# The maximum amount to spend on LLM interactions
|
|
88
93
|
# Once reached the agent will stop and this value will need to be increased to continue
|
|
89
94
|
spendLimitDollars: 2.00
|
|
95
|
+
|
|
96
|
+
# None: Commands from the LLM run automatically, this is the default setting as well if the value is not set
|
|
97
|
+
# Manual: Every command the LLM wants you run you have to approve [y/n]
|
|
98
|
+
# Auto: All commands are run through the separate LLM instace, commands that look like they'll modify the system are blocked
|
|
99
|
+
commandProtection: 'none'
|
|
100
|
+
|
|
90
101
|
# Additional custom variables can be defined here and/or in the .env file to be loaded into the agent prompt
|
|
91
102
|
```
|
|
92
103
|
|
|
@@ -94,14 +105,14 @@ spendLimitDollars: 2.00
|
|
|
94
105
|
- If a yaml file is passed, naisys will start a single agent
|
|
95
106
|
- If a directory is passed, naisys will start a tmux session with the screen split for each agent
|
|
96
107
|
|
|
97
|
-
|
|
108
|
+
#### Creating a persistent agent run website (on Digital Ocean for example)
|
|
98
109
|
|
|
99
110
|
- Create new VM using the [LAMP stack droplet template](https://marketplace.digitalocean.com/apps/lamp)
|
|
100
111
|
- Login to the droplet using the web console
|
|
101
112
|
- Run `apt install npm`
|
|
102
113
|
- Install `nvm` using the `curl` url from these [instructions](https://github.com/nvm-sh/nvm?tab=readme-ov-file#installing-and-updating)
|
|
103
114
|
- Run `nvm install/use 20` to set node version to 20
|
|
104
|
-
|
|
115
|
+
- Follow the general install instructions above
|
|
105
116
|
|
|
106
117
|
## Using NAISYS
|
|
107
118
|
|
|
@@ -128,12 +139,12 @@ spendLimitDollars: 2.00
|
|
|
128
139
|
- NAISYS tries to be light, acting as a helpful proxy between the LLM and a real shell, most commands should pass right though to the shell
|
|
129
140
|
- Debug Commands
|
|
130
141
|
- `cost` - Prints the current total LLM cost
|
|
131
|
-
- `context` - Prints the current context
|
|
142
|
+
- `context` - Prints the current context
|
|
132
143
|
- `exit` - Exits NAISYS. If the LLM tries to use `exit`, it is directed to use `endsession` instead
|
|
133
144
|
- `talk` - Communicate with the local agent to give hints or ask questions (the agent itself does not know about talk and is directed to use `comment` or `llmail` for communication)
|
|
134
145
|
- Special Commands usable by the LLM as well as by the debug prompt
|
|
135
|
-
- `comment <
|
|
136
|
-
- `endsession <
|
|
146
|
+
- `comment "<note>"` - The LLM is directed to use this for 'thinking out loud' which avoids 'invalid command' errors
|
|
147
|
+
- `endsession "<note>"` - Clear the context and start a new session.
|
|
137
148
|
- The LLM is directed to track it's context size and to end the session with a note before running over the context limit
|
|
138
149
|
- `pause <seconds>` - Can be used by the debug agent or the LLM to pause execution indefinitely, or until a new message is received from another agent, or for a set number of seconds
|
|
139
150
|
- NAISYS apps
|
|
@@ -154,6 +165,8 @@ spendLimitDollars: 2.00
|
|
|
154
165
|
|
|
155
166
|
#### Notes for Windows users
|
|
156
167
|
|
|
168
|
+
- To use NAISYS on Windows you need to run it locally from source (or from within WSL)
|
|
169
|
+
- Use the above instructions to install locally, and then continue with the instructions below
|
|
157
170
|
- Install WSL (Windows Subsystem for Linux)
|
|
158
171
|
- The `NAISYS_FOLDER` and `WEBSITE_FOLDER` should be set to the WSL path
|
|
159
172
|
- So `C:\var\naisys` should be `/mnt/c/var/naisys` in the `.env` file
|
package/dist/apps/llmail.js
CHANGED
|
@@ -7,7 +7,7 @@ import { naisysToHostPath } from "../utils/utilities.js";
|
|
|
7
7
|
const _dbFilePath = naisysToHostPath(`${config.naisysFolder}/lib/llmail.db`);
|
|
8
8
|
let _myUserId = -1;
|
|
9
9
|
// Implement maxes so that LLMs actively manage threads, archive, and create new ones
|
|
10
|
-
const _threadTokenMax = config.tokenMax / 2; // So 4000, would be 2000 thread max
|
|
10
|
+
const _threadTokenMax = config.agent.tokenMax / 2; // So 4000, would be 2000 thread max
|
|
11
11
|
const _messageTokenMax = _threadTokenMax / 5; // Given the above a 400 token max, and 5 big messages per thread
|
|
12
12
|
/** The 'non-simple' version of this is a thread first mail system. Where agents can create threads, add users, and reply to threads, etc..
|
|
13
13
|
* The problem with this was the agents were too chatty with so many mail commands, wasting context replying, reading threads, etc..
|
|
@@ -347,4 +347,10 @@ function validateMsgTokenCount(message) {
|
|
|
347
347
|
async function usingDatabase(run) {
|
|
348
348
|
return dbUtils.usingDatabase(_dbFilePath, run);
|
|
349
349
|
}
|
|
350
|
+
export async function hasMultipleUsers() {
|
|
351
|
+
return await usingDatabase(async (db) => {
|
|
352
|
+
const users = await db.all("SELECT * FROM Users");
|
|
353
|
+
return users.length > 1;
|
|
354
|
+
});
|
|
355
|
+
}
|
|
350
356
|
//# sourceMappingURL=llmail.js.map
|
package/dist/apps/llmynx.js
CHANGED
|
@@ -15,7 +15,7 @@ let _nextGlobalLinkNum = 1;
|
|
|
15
15
|
export async function handleCommand(cmdArgs) {
|
|
16
16
|
outputInDebugMode("LLMYNX DEBUG MODE IS ON");
|
|
17
17
|
const argParams = cmdArgs.split(" ");
|
|
18
|
-
const defualtTokenMax = config.tokenMax / 8;
|
|
18
|
+
const defualtTokenMax = config.agent.tokenMax / 8;
|
|
19
19
|
if (!argParams[0]) {
|
|
20
20
|
argParams[0] = "help";
|
|
21
21
|
}
|
|
@@ -28,7 +28,7 @@ export async function handleCommand(cmdArgs) {
|
|
|
28
28
|
links <url> <page>: Lists only the links for the given url. Use the page number to get more links`;
|
|
29
29
|
case "search": {
|
|
30
30
|
const query = argParams.slice(1).join(" ");
|
|
31
|
-
return await loadUrl("https://www.google.com/search?q=" + encodeURIComponent(query), config.tokenMax / 2, // Prevent form being reduced as google results are usually short anyways and we want to maintainq the links
|
|
31
|
+
return await loadUrl("https://www.google.com/search?q=" + encodeURIComponent(query), config.agent.tokenMax / 2, // Prevent form being reduced as google results are usually short anyways and we want to maintainq the links
|
|
32
32
|
true, true);
|
|
33
33
|
}
|
|
34
34
|
case "open": {
|
|
@@ -10,6 +10,8 @@ import { InputMode } from "../utils/inputMode.js";
|
|
|
10
10
|
import * as logService from "../utils/logService.js";
|
|
11
11
|
import * as output from "../utils/output.js";
|
|
12
12
|
import { OutputColor } from "../utils/output.js";
|
|
13
|
+
import * as utilities from "../utils/utilities.js";
|
|
14
|
+
import * as commandProtection from "./commandProtection.js";
|
|
13
15
|
import * as promptBuilder from "./promptBuilder.js";
|
|
14
16
|
import * as shellCommand from "./shellCommand.js";
|
|
15
17
|
export var NextCommandAction;
|
|
@@ -19,50 +21,23 @@ export var NextCommandAction;
|
|
|
19
21
|
NextCommandAction[NextCommandAction["ExitApplication"] = 2] = "ExitApplication";
|
|
20
22
|
})(NextCommandAction || (NextCommandAction = {}));
|
|
21
23
|
export let previousSessionNotes = await logService.getPreviousEndSessionNote();
|
|
22
|
-
export async function
|
|
24
|
+
export async function processCommand(prompt, consoleInput) {
|
|
23
25
|
// We process the lines one at a time so we can support multiple commands with line breaks
|
|
24
26
|
let firstLine = true;
|
|
25
27
|
let processNextLLMpromptBlock = true;
|
|
26
|
-
const userHostPrompt = promptBuilder.getUserHostPrompt();
|
|
27
28
|
let nextCommandAction = NextCommandAction.Continue;
|
|
28
|
-
|
|
29
|
-
while (processNextLLMpromptBlock &&
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (nextPromptPos == 0) {
|
|
35
|
-
const pathPrompt = await promptBuilder.getUserHostPathPrompt();
|
|
36
|
-
// check working directory is the same
|
|
37
|
-
if (nextInput.startsWith(pathPrompt)) {
|
|
38
|
-
// slice nextInput after $
|
|
39
|
-
const endPrompt = nextInput.indexOf("$", pathPrompt.length);
|
|
40
|
-
nextInput = nextInput.slice(endPrompt + 1).trim();
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
// else prompt did not match, stop processing input
|
|
44
|
-
else {
|
|
45
|
-
break;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
// we can't validate that the working directory in the prompt is good until the commands are processed
|
|
49
|
-
else if (nextPromptPos > 0) {
|
|
50
|
-
input = nextInput.slice(0, nextPromptPos);
|
|
51
|
-
nextInput = nextInput.slice(nextPromptPos).trim();
|
|
29
|
+
consoleInput = consoleInput.trim();
|
|
30
|
+
while (processNextLLMpromptBlock && consoleInput) {
|
|
31
|
+
const { input, nextInput, splitResult } = await splitMultipleInputCommands(consoleInput);
|
|
32
|
+
consoleInput = nextInput;
|
|
33
|
+
if (splitResult == SplitResult.InputIsPrompt) {
|
|
34
|
+
continue;
|
|
52
35
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
input = nextInput.slice(0, newLinePos);
|
|
56
|
-
nextInput = nextInput.slice(newLinePos).trim();
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
input = nextInput;
|
|
60
|
-
nextInput = "";
|
|
61
|
-
}
|
|
62
|
-
if (!input.trim()) {
|
|
36
|
+
else if (splitResult == SplitResult.InputPromptMismatch ||
|
|
37
|
+
!input.trim()) {
|
|
63
38
|
break;
|
|
64
39
|
}
|
|
65
|
-
//
|
|
40
|
+
// First line is special because we want to append the output to the context without a line break
|
|
66
41
|
if (inputMode.current == InputMode.LLM) {
|
|
67
42
|
if (firstLine) {
|
|
68
43
|
firstLine = false;
|
|
@@ -73,6 +48,13 @@ export async function consoleInput(prompt, consoleInput) {
|
|
|
73
48
|
await output.commentAndLog("Continuing with next command from same LLM response...");
|
|
74
49
|
await contextManager.append(input, ContentSource.LLM);
|
|
75
50
|
}
|
|
51
|
+
// Run write protection checks if enabled
|
|
52
|
+
const { commandAllowed, rejectReason } = await commandProtection.validateCommand(input);
|
|
53
|
+
if (!commandAllowed) {
|
|
54
|
+
await output.errorAndLog(`Write Protection Triggered`);
|
|
55
|
+
await contextManager.append(rejectReason || "Unknown");
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
76
58
|
}
|
|
77
59
|
const cmdParams = input.split(" ");
|
|
78
60
|
const cmdArgs = input.slice(cmdParams[0].length).trim();
|
|
@@ -83,7 +65,12 @@ export async function consoleInput(prompt, consoleInput) {
|
|
|
83
65
|
break;
|
|
84
66
|
}
|
|
85
67
|
case "endsession": {
|
|
86
|
-
|
|
68
|
+
// Don't need to check end line as this is the last command in the context, just read to the end
|
|
69
|
+
previousSessionNotes = utilities.trimChars(cmdArgs, '"');
|
|
70
|
+
if (!previousSessionNotes) {
|
|
71
|
+
await contextManager.append(`End session notes are required. Use endsession "<notes>"`);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
87
74
|
await output.commentAndLog("------------------------------------------------------");
|
|
88
75
|
nextCommandAction = NextCommandAction.EndSession;
|
|
89
76
|
processNextLLMpromptBlock = false;
|
|
@@ -144,8 +131,8 @@ export async function consoleInput(prompt, consoleInput) {
|
|
|
144
131
|
}
|
|
145
132
|
}
|
|
146
133
|
// display unprocessed lines to aid in debugging
|
|
147
|
-
if (
|
|
148
|
-
await output.errorAndLog(`Unprocessed LLM response:\n${
|
|
134
|
+
if (consoleInput.trim()) {
|
|
135
|
+
await output.errorAndLog(`Unprocessed LLM response:\n${consoleInput}`);
|
|
149
136
|
}
|
|
150
137
|
return {
|
|
151
138
|
nextCommandAction,
|
|
@@ -153,4 +140,68 @@ export async function consoleInput(prompt, consoleInput) {
|
|
|
153
140
|
wakeOnMessage: config.agent.wakeOnMessage,
|
|
154
141
|
};
|
|
155
142
|
}
|
|
143
|
+
var SplitResult;
|
|
144
|
+
(function (SplitResult) {
|
|
145
|
+
SplitResult[SplitResult["InputIsPrompt"] = 0] = "InputIsPrompt";
|
|
146
|
+
SplitResult[SplitResult["InputPromptMismatch"] = 1] = "InputPromptMismatch";
|
|
147
|
+
})(SplitResult || (SplitResult = {}));
|
|
148
|
+
async function splitMultipleInputCommands(nextInput) {
|
|
149
|
+
let input = "";
|
|
150
|
+
let splitResult;
|
|
151
|
+
// If the prompt exists in the input, save if for the next run
|
|
152
|
+
const userHostPrompt = promptBuilder.getUserHostPrompt();
|
|
153
|
+
const nextPromptPos = nextInput.indexOf(userHostPrompt);
|
|
154
|
+
const newLinePos = nextInput.indexOf("\n");
|
|
155
|
+
if (nextPromptPos == 0) {
|
|
156
|
+
const pathPrompt = await promptBuilder.getUserHostPathPrompt();
|
|
157
|
+
// Check working directory is the same
|
|
158
|
+
if (nextInput.startsWith(pathPrompt)) {
|
|
159
|
+
// Slice nextInput after $
|
|
160
|
+
const endPrompt = nextInput.indexOf("$", pathPrompt.length);
|
|
161
|
+
nextInput = nextInput.slice(endPrompt + 1).trim();
|
|
162
|
+
splitResult = SplitResult.InputIsPrompt;
|
|
163
|
+
}
|
|
164
|
+
// Else prompt did not match, stop processing input
|
|
165
|
+
else {
|
|
166
|
+
splitResult = SplitResult.InputPromptMismatch;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// We can't validate that the working directory in the prompt is good until the commands are processed
|
|
170
|
+
else if (nextPromptPos > 0) {
|
|
171
|
+
input = nextInput.slice(0, nextPromptPos);
|
|
172
|
+
nextInput = nextInput.slice(nextPromptPos).trim();
|
|
173
|
+
}
|
|
174
|
+
// Most custom NAISYS commands are single line, but comment in quotes can span multiple lines so we need to handle that
|
|
175
|
+
// because often the LLM puts shell commands after the comment
|
|
176
|
+
else if (nextInput.startsWith(`comment "`)) {
|
|
177
|
+
// Find next double quote in nextInput that isn't escaped
|
|
178
|
+
let endQuote = nextInput.indexOf(`"`, 9);
|
|
179
|
+
while (endQuote > 0 && nextInput[endQuote - 1] === "\\") {
|
|
180
|
+
endQuote = nextInput.indexOf(`"`, endQuote + 1);
|
|
181
|
+
}
|
|
182
|
+
if (endQuote > 0) {
|
|
183
|
+
input = nextInput.slice(0, endQuote + 1);
|
|
184
|
+
nextInput = nextInput.slice(endQuote + 1).trim();
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
input = nextInput;
|
|
188
|
+
nextInput = "";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// If the LLM forgets the quote on the comment, treat it as a single line comment
|
|
192
|
+
else if (newLinePos > 0 && nextInput.startsWith("comment ")) {
|
|
193
|
+
input = nextInput.slice(0, newLinePos);
|
|
194
|
+
nextInput = nextInput.slice(newLinePos).trim();
|
|
195
|
+
}
|
|
196
|
+
// Else process the entire input now
|
|
197
|
+
else {
|
|
198
|
+
input = nextInput;
|
|
199
|
+
nextInput = "";
|
|
200
|
+
}
|
|
201
|
+
return { input, nextInput, splitResult };
|
|
202
|
+
}
|
|
203
|
+
export const exportedForTesting = {
|
|
204
|
+
splitMultipleInputCommands,
|
|
205
|
+
SplitResult,
|
|
206
|
+
};
|
|
156
207
|
//# sourceMappingURL=commandHandler.js.map
|
|
@@ -18,7 +18,7 @@ import * as promptBuilder from "./promptBuilder.js";
|
|
|
18
18
|
const maxErrorCount = 5;
|
|
19
19
|
export async function run() {
|
|
20
20
|
// Show Agent Config exept the agent prompt
|
|
21
|
-
await output.commentAndLog(`Agent configured to use ${config.agent.
|
|
21
|
+
await output.commentAndLog(`Agent configured to use ${config.agent.shellModel} model`);
|
|
22
22
|
// Show System Message
|
|
23
23
|
await output.commentAndLog("System Message:");
|
|
24
24
|
const systemMessage = contextManager.getSystemMessage();
|
|
@@ -35,28 +35,30 @@ export async function run() {
|
|
|
35
35
|
await output.commentAndLog("Starting Context:");
|
|
36
36
|
await contextManager.append("Previous Session Note:");
|
|
37
37
|
await contextManager.append(commandHandler.previousSessionNotes || "None");
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
if (await llmail.hasMultipleUsers()) {
|
|
39
|
+
await commandHandler.processCommand(await promptBuilder.getPrompt(), "llmail help");
|
|
40
|
+
await commandHandler.processCommand(await promptBuilder.getPrompt(), "llmail users");
|
|
41
|
+
}
|
|
40
42
|
inputMode.toggle(InputMode.Debug);
|
|
41
43
|
let pauseSeconds = config.agent.debugPauseSeconds;
|
|
42
44
|
let wakeOnMessage = config.agent.wakeOnMessage;
|
|
43
45
|
while (nextCommandAction == NextCommandAction.Continue) {
|
|
44
46
|
const prompt = await promptBuilder.getPrompt(pauseSeconds, wakeOnMessage);
|
|
45
|
-
let
|
|
47
|
+
let consoleInput = "";
|
|
46
48
|
// Debug command prompt
|
|
47
49
|
if (inputMode.current === InputMode.Debug) {
|
|
48
|
-
|
|
50
|
+
consoleInput = await promptBuilder.getInput(`${prompt}`, pauseSeconds, wakeOnMessage);
|
|
49
51
|
}
|
|
50
52
|
// LLM command prompt
|
|
51
53
|
else if (inputMode.current === InputMode.LLM) {
|
|
52
54
|
const workingMsg = prompt +
|
|
53
|
-
chalk[output.OutputColor.loading](`LLM (${config.agent.
|
|
55
|
+
chalk[output.OutputColor.loading](`LLM (${config.agent.shellModel}) Working...`);
|
|
54
56
|
try {
|
|
55
|
-
await
|
|
56
|
-
await
|
|
57
|
+
await checkNewMailNotification();
|
|
58
|
+
await checkContextLimitWarning();
|
|
57
59
|
await contextManager.append(prompt, ContentSource.ConsolePrompt);
|
|
58
60
|
process.stdout.write(workingMsg);
|
|
59
|
-
|
|
61
|
+
consoleInput = await llmService.query(config.agent.shellModel, contextManager.getSystemMessage(), contextManager.messages, "console");
|
|
60
62
|
clearPromptMessage(workingMsg);
|
|
61
63
|
}
|
|
62
64
|
catch (e) {
|
|
@@ -73,7 +75,7 @@ export async function run() {
|
|
|
73
75
|
// Run the command
|
|
74
76
|
try {
|
|
75
77
|
({ nextCommandAction, pauseSeconds, wakeOnMessage } =
|
|
76
|
-
await commandHandler.
|
|
78
|
+
await commandHandler.processCommand(prompt, consoleInput));
|
|
77
79
|
if (inputMode.current == InputMode.LLM) {
|
|
78
80
|
llmErrorCount = 0;
|
|
79
81
|
}
|
|
@@ -85,7 +87,7 @@ export async function run() {
|
|
|
85
87
|
}
|
|
86
88
|
// If the user is in debug mode and they didn't enter anything, switch to LLM
|
|
87
89
|
// If in LLM mode, auto switch back to debug
|
|
88
|
-
if ((inputMode.current == InputMode.Debug && !
|
|
90
|
+
if ((inputMode.current == InputMode.Debug && !consoleInput) ||
|
|
89
91
|
inputMode.current == InputMode.LLM) {
|
|
90
92
|
inputMode.toggle();
|
|
91
93
|
}
|
|
@@ -136,7 +138,7 @@ async function handleErrorAndSwitchToDebugMode(e, llmErrorCount, addToContext) {
|
|
|
136
138
|
wakeOnMessage,
|
|
137
139
|
};
|
|
138
140
|
}
|
|
139
|
-
async function
|
|
141
|
+
async function checkNewMailNotification() {
|
|
140
142
|
// Check for unread threads
|
|
141
143
|
const unreadThreads = await llmail.getUnreadThreads();
|
|
142
144
|
if (!unreadThreads.length) {
|
|
@@ -150,7 +152,7 @@ async function displayNewMail() {
|
|
|
150
152
|
// Check that token max for session will not be exceeded
|
|
151
153
|
const newMsgTokenCount = newMessages.reduce((acc, msg) => acc + utilities.getTokenCount(msg), 0);
|
|
152
154
|
const sessionTokens = contextManager.getTokenCount();
|
|
153
|
-
const tokenMax = config.tokenMax;
|
|
155
|
+
const tokenMax = config.agent.tokenMax;
|
|
154
156
|
// Show full messages unless we are close to the token limit of the session
|
|
155
157
|
// or in simple mode, which means non-threaded messages
|
|
156
158
|
if (sessionTokens + newMsgTokenCount < tokenMax * 0.75) {
|
|
@@ -174,9 +176,9 @@ async function displayNewMail() {
|
|
|
174
176
|
`Use llmail read <id>' to read the thread, but be mindful you are close to the token limit for the session.`, ContentSource.Console);
|
|
175
177
|
}
|
|
176
178
|
}
|
|
177
|
-
async function
|
|
179
|
+
async function checkContextLimitWarning() {
|
|
178
180
|
const tokenCount = contextManager.getTokenCount();
|
|
179
|
-
const tokenMax = config.tokenMax;
|
|
181
|
+
const tokenMax = config.agent.tokenMax;
|
|
180
182
|
if (tokenCount > tokenMax) {
|
|
181
183
|
await contextManager.append(`The token limit for this session has been exceeded.
|
|
182
184
|
Use \`endsession <note>\` to clear the console and reset the session.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as config from "../config.js";
|
|
2
|
+
import { LlmRole } from "../llm/llmDtos.js";
|
|
3
|
+
import * as llmService from "../llm/llmService.js";
|
|
4
|
+
import { CommandProtection } from "../utils/enums.js";
|
|
5
|
+
import * as output from "../utils/output.js";
|
|
6
|
+
import * as promptBuilder from "./promptBuilder.js";
|
|
7
|
+
export async function validateCommand(command) {
|
|
8
|
+
switch (config.agent.commandProtection) {
|
|
9
|
+
case CommandProtection.None:
|
|
10
|
+
return {
|
|
11
|
+
commandAllowed: true,
|
|
12
|
+
};
|
|
13
|
+
case CommandProtection.Manual: {
|
|
14
|
+
const confirmation = await promptBuilder.getCommandConfirmation();
|
|
15
|
+
const commandAllowed = confirmation.toLowerCase() === "y";
|
|
16
|
+
return {
|
|
17
|
+
commandAllowed,
|
|
18
|
+
rejectReason: commandAllowed ? undefined : "Command denied by admin",
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
case CommandProtection.Auto:
|
|
22
|
+
return await autoValidateCommand(command);
|
|
23
|
+
default:
|
|
24
|
+
throw "Write protection not configured correctly";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function autoValidateCommand(command) {
|
|
28
|
+
output.comment("Checking if command is allowed...");
|
|
29
|
+
const systemMessage = `You are a command validator that checks if shell commands are ok to run.
|
|
30
|
+
The user is 'junior admin' allowed to move around the system, anywhere, and read anything, list anything.
|
|
31
|
+
They are not allowed to execute programs that could modify the system.
|
|
32
|
+
Programs that just give information responses are ok.
|
|
33
|
+
The user is allowed to write to their home directory in ${config.naisysFolder}/home/${config.agent.username}
|
|
34
|
+
In addition to the commands you know are ok, these additional commands are whitelisted:
|
|
35
|
+
llmail, llmynx, comment, endsession, and pause
|
|
36
|
+
Reply with 'allow' to allow the command, otherwise you can give a reason for your rejection.`;
|
|
37
|
+
const response = await llmService.query(config.agent.shellModel, systemMessage, [
|
|
38
|
+
{
|
|
39
|
+
role: LlmRole.User,
|
|
40
|
+
content: command,
|
|
41
|
+
},
|
|
42
|
+
], "write-protection");
|
|
43
|
+
const commandAllowed = response.toLocaleLowerCase().startsWith("allow");
|
|
44
|
+
return {
|
|
45
|
+
commandAllowed,
|
|
46
|
+
rejectReason: commandAllowed ? undefined : "Command Rejected: " + response,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=commandProtection.js.map
|
|
@@ -31,7 +31,7 @@ _readlineInterface.on("close", () => {
|
|
|
31
31
|
});
|
|
32
32
|
export async function getPrompt(pauseSeconds, wakeOnMessage) {
|
|
33
33
|
const promptSuffix = inputMode.current == InputMode.Debug ? "#" : "$";
|
|
34
|
-
const tokenMax = config.tokenMax;
|
|
34
|
+
const tokenMax = config.agent.tokenMax;
|
|
35
35
|
const usedTokens = contextManager.getTokenCount();
|
|
36
36
|
const tokenSuffix = ` [Tokens: ${usedTokens}/${tokenMax}]`;
|
|
37
37
|
let pause = "";
|
|
@@ -130,4 +130,11 @@ export function getInput(commandPrompt, pauseSeconds, wakeOnMessage) {
|
|
|
130
130
|
}
|
|
131
131
|
});
|
|
132
132
|
}
|
|
133
|
+
export function getCommandConfirmation() {
|
|
134
|
+
return new Promise((resolve) => {
|
|
135
|
+
_readlineInterface.question(chalk.greenBright("Allow command to run? [y/n] "), (answer) => {
|
|
136
|
+
resolve(answer);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
133
140
|
//# sourceMappingURL=promptBuilder.js.map
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import * as config from "../config.js";
|
|
1
2
|
import * as contextManager from "../llm/contextManager.js";
|
|
2
3
|
import * as inputMode from "../utils/inputMode.js";
|
|
3
4
|
import { InputMode } from "../utils/inputMode.js";
|
|
5
|
+
import * as utilities from "../utils/utilities.js";
|
|
4
6
|
import * as shellWrapper from "./shellWrapper.js";
|
|
5
7
|
export async function handleCommand(input) {
|
|
6
8
|
const cmdParams = input.split(" ");
|
|
@@ -29,7 +31,19 @@ export async function handleCommand(input) {
|
|
|
29
31
|
}
|
|
30
32
|
const output = await shellWrapper.executeCommand(input);
|
|
31
33
|
if (output.value) {
|
|
32
|
-
|
|
34
|
+
let text = output.value;
|
|
35
|
+
let outputLimitExceeded = false;
|
|
36
|
+
const tokenCount = utilities.getTokenCount(text);
|
|
37
|
+
// Prevent too much output from blowing up the context
|
|
38
|
+
if (tokenCount > config.shellOutputTokenMax) {
|
|
39
|
+
outputLimitExceeded = true;
|
|
40
|
+
const trimLength = (text.length * config.shellOutputTokenMax) / tokenCount;
|
|
41
|
+
text = text.slice(0, trimLength);
|
|
42
|
+
}
|
|
43
|
+
await contextManager.append(text);
|
|
44
|
+
if (outputLimitExceeded) {
|
|
45
|
+
await contextManager.append(`\nThe shell command generated too much output (${tokenCount} tokens). Only 2,000 tokens worth are shown above.`);
|
|
46
|
+
}
|
|
33
47
|
}
|
|
34
48
|
response.hasErrors = output.hasErrors;
|
|
35
49
|
return response;
|
|
@@ -39,12 +39,12 @@ async function ensureOpen() {
|
|
|
39
39
|
// Init users home dir on first run, on shell crash/rerun go back to the current path
|
|
40
40
|
if (!_currentPath) {
|
|
41
41
|
output.comment("NEW SHELL OPENED. PID: " + _process.pid);
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
errorIfNotEmpty(await executeCommand(`mkdir -p ${config.naisysFolder}/home/` + config.agent.username));
|
|
43
|
+
errorIfNotEmpty(await executeCommand(`cd ${config.naisysFolder}/home/` + config.agent.username));
|
|
44
44
|
}
|
|
45
45
|
else {
|
|
46
46
|
output.comment("SHELL RESTORED. PID: " + _process.pid);
|
|
47
|
-
|
|
47
|
+
errorIfNotEmpty(await executeCommand("cd " + _currentPath));
|
|
48
48
|
}
|
|
49
49
|
// Stop running commands if one fails
|
|
50
50
|
// Often the LLM will give us back all kinds of invalid commands, we want to break on the first one
|
|
@@ -52,9 +52,9 @@ async function ensureOpen() {
|
|
|
52
52
|
//commentIfNotEmpty(await executeCommand("set -e"));
|
|
53
53
|
}
|
|
54
54
|
/** Basically don't show anything in the console unless there is an error */
|
|
55
|
-
function
|
|
55
|
+
function errorIfNotEmpty(response) {
|
|
56
56
|
if (response.value) {
|
|
57
|
-
output.
|
|
57
|
+
output.error(response.value);
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
export function processOutput(dataStr, eventType) {
|
|
@@ -128,19 +128,22 @@ export async function executeCommand(command) {
|
|
|
128
128
|
const commandWithDelimiter = `${command.trim()}\necho "${_commandDelimiter} LINE:\${LINENO}"\n`;
|
|
129
129
|
//_log += "INPUT: " + commandWithDelimiter;
|
|
130
130
|
_process === null || _process === void 0 ? void 0 : _process.stdin.write(commandWithDelimiter);
|
|
131
|
-
// If no response
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
131
|
+
// If no response, kill and reset the shell, often hanging on some unescaped input
|
|
132
|
+
_currentCommandTimeout = setTimeout(resetShell, config.shellCommmandTimeoutSeconds * 1000);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function resetShell() {
|
|
136
|
+
if (!_resolveCurrentCommand) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
_process === null || _process === void 0 ? void 0 : _process.kill();
|
|
140
|
+
output.error("SHELL TIMEMOUT/KILLED. PID: " + (_process === null || _process === void 0 ? void 0 : _process.pid));
|
|
141
|
+
const outputWithError = _commandOutput.trim() +
|
|
142
|
+
`\nError: Command timed out after ${config.shellCommmandTimeoutSeconds} seconds.`;
|
|
143
|
+
resetProcess();
|
|
144
|
+
_resolveCurrentCommand({
|
|
145
|
+
value: outputWithError,
|
|
146
|
+
hasErrors: true,
|
|
144
147
|
});
|
|
145
148
|
}
|
|
146
149
|
export async function getCurrentPath() {
|
package/dist/config.js
CHANGED
|
@@ -2,14 +2,15 @@ import { program } from "commander";
|
|
|
2
2
|
import dotenv from "dotenv";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import yaml from "js-yaml";
|
|
5
|
+
import { CommandProtection } from "./utils/enums.js";
|
|
5
6
|
import { valueFromString } from "./utils/utilities.js";
|
|
6
7
|
program.argument("<agent-path>", "Path to agent configuration file").parse();
|
|
7
8
|
dotenv.config();
|
|
8
9
|
/** The system name that shows after the @ in the command prompt */
|
|
9
10
|
export const hostname = "naisys";
|
|
10
|
-
|
|
11
|
-
export const
|
|
12
|
-
/* .env is used for global configs across naisys, while agent configs for the specific agent */
|
|
11
|
+
export const shellOutputTokenMax = 2500;
|
|
12
|
+
export const shellCommmandTimeoutSeconds = 10;
|
|
13
|
+
/* .env is used for global configs across naisys, while agent configs are for the specific agent */
|
|
13
14
|
export const naisysFolder = getEnv("NAISYS_FOLDER", true);
|
|
14
15
|
export const websiteFolder = getEnv("WEBSITE_FOLDER");
|
|
15
16
|
export const localLlmUrl = getEnv("LOCAL_LLM_URL");
|
|
@@ -18,13 +19,6 @@ export const openaiApiKey = getEnv("OPENAI_API_KEY");
|
|
|
18
19
|
export const googleApiKey = getEnv("GOOGLE_API_KEY");
|
|
19
20
|
export const anthropicApiKey = getEnv("ANTHROPIC_API_KEY");
|
|
20
21
|
export const agent = loadAgentConfig();
|
|
21
|
-
function getEnv(key, required) {
|
|
22
|
-
const value = process.env[key];
|
|
23
|
-
if (!value && required) {
|
|
24
|
-
throw `Config: Error, .env ${key} is not defined`;
|
|
25
|
-
}
|
|
26
|
-
return value;
|
|
27
|
-
}
|
|
28
22
|
function loadAgentConfig() {
|
|
29
23
|
const agentPath = program.args[0];
|
|
30
24
|
const checkAgentConfig = yaml.load(fs.readFileSync(agentPath, "utf8"));
|
|
@@ -32,16 +26,45 @@ function loadAgentConfig() {
|
|
|
32
26
|
for (const key of [
|
|
33
27
|
"username",
|
|
34
28
|
"title",
|
|
35
|
-
"
|
|
29
|
+
"shellModel",
|
|
36
30
|
"webModel",
|
|
37
31
|
"agentPrompt",
|
|
38
32
|
"spendLimitDollars",
|
|
39
|
-
|
|
33
|
+
"tokenMax",
|
|
34
|
+
// other properties can be undefined
|
|
40
35
|
]) {
|
|
41
36
|
if (!valueFromString(checkAgentConfig, key)) {
|
|
42
37
|
throw `Agent config: Error, ${key} is not defined`;
|
|
43
38
|
}
|
|
44
39
|
}
|
|
40
|
+
if (!checkAgentConfig.commandProtection) {
|
|
41
|
+
checkAgentConfig.commandProtection = CommandProtection.None;
|
|
42
|
+
}
|
|
43
|
+
if (!Object.values(CommandProtection).includes(checkAgentConfig.commandProtection)) {
|
|
44
|
+
throw `Agent config: Error, 'commandProtection' is not a valid value`;
|
|
45
|
+
}
|
|
45
46
|
return checkAgentConfig;
|
|
46
47
|
}
|
|
48
|
+
export const packageVersion = await getVersion();
|
|
49
|
+
/** Can only get version from env variable when naisys is started with npm,
|
|
50
|
+
* otherwise need to rip it from the package ourselves relative to where this file is located */
|
|
51
|
+
async function getVersion() {
|
|
52
|
+
try {
|
|
53
|
+
const packageJsonPath = new URL("../package.json", import.meta.url);
|
|
54
|
+
const packageJson = await import(packageJsonPath.href, {
|
|
55
|
+
assert: { type: "json" },
|
|
56
|
+
});
|
|
57
|
+
return packageJson.default.version;
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
return "0.1";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function getEnv(key, required) {
|
|
64
|
+
const value = process.env[key];
|
|
65
|
+
if (!value && required) {
|
|
66
|
+
throw `Config: Error, .env ${key} is not defined`;
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
47
70
|
//# sourceMappingURL=config.js.map
|
|
@@ -34,7 +34,7 @@ For example when you run 'cat' or 'ls', don't write what you think the output wi
|
|
|
34
34
|
Your role is that of the user. The system will provide responses and next command prompt. Don't output your own command prompt.
|
|
35
35
|
Be careful when writing files through the command prompt with cat. Make sure to close and escape quotes properly.
|
|
36
36
|
|
|
37
|
-
NAISYS ${
|
|
37
|
+
NAISYS ${config.packageVersion} Shell
|
|
38
38
|
Welcome back ${config.agent.username}!
|
|
39
39
|
MOTD:
|
|
40
40
|
Date: ${new Date().toLocaleString()}
|
|
@@ -46,9 +46,9 @@ Commands:
|
|
|
46
46
|
Special Commands: (Don't mix with standard commands on the same prompt)
|
|
47
47
|
llmail: A local mail system for communicating with your team
|
|
48
48
|
llmynx: A context optimized web browser. Enter 'llmynx help' to learn how to use it
|
|
49
|
-
comment <thought
|
|
49
|
+
comment "<thought>": Any non-command output like thinking out loud, prefix with the 'comment' command
|
|
50
50
|
pause <seconds>: Pause for <seconds> or indeterminite if no argument is provided. Auto wake up on new mail message
|
|
51
|
-
endsession <note
|
|
51
|
+
endsession "<note>": Ends this session, clears the console log and context.
|
|
52
52
|
The note should help you find your bearings in the next session.
|
|
53
53
|
The note should contain your next goal, and important things should you remember.
|
|
54
54
|
Try to keep the note around 400 tokens.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* To separate enums from services which is useful for mocking where the enum
|
|
2
|
+
is used across the code base, but the service it originates from is mocked. */
|
|
3
|
+
export var CommandProtection;
|
|
4
|
+
(function (CommandProtection) {
|
|
5
|
+
CommandProtection["None"] = "none";
|
|
6
|
+
CommandProtection["Manual"] = "manual";
|
|
7
|
+
CommandProtection["Auto"] = "auto";
|
|
8
|
+
})(CommandProtection || (CommandProtection = {}));
|
|
9
|
+
//# sourceMappingURL=enums.js.map
|
package/dist/utils/utilities.js
CHANGED
|
@@ -37,4 +37,7 @@ export function ensureFileDirExists(filePath) {
|
|
|
37
37
|
fs.mkdirSync(dir, { recursive: true });
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
export function trimChars(text, charList) {
|
|
41
|
+
return text.replace(new RegExp(`^[${charList}]+|[${charList}]+$`, "g"), "");
|
|
42
|
+
}
|
|
40
43
|
//# sourceMappingURL=utilities.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naisys",
|
|
3
3
|
"description": "Node.js Autonomous Intelligence System",
|
|
4
|
-
"version": "1.0
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/naisys.js",
|
|
7
7
|
"preferGlobal": true,
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
"naisys": "naisys.sh"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"compile/run/attachable": "tsc && node --inspect dist/naisys.js ./agents/
|
|
13
|
-
"run agent:dev": "node dist/naisys.js ./agents/
|
|
14
|
-
"run agent:admin": "node dist/naisys.js ./agents/
|
|
12
|
+
"compile/run/attachable": "tsc && node --inspect dist/naisys.js ./agents/webdev-fansite.yaml",
|
|
13
|
+
"run agent:dev": "node dist/naisys.js ./agents/2-team/dev.yaml",
|
|
14
|
+
"run agent:admin": "node dist/naisys.js ./agents/2-team/admin.yaml",
|
|
15
15
|
"clean": "rm -rf dist",
|
|
16
16
|
"clean:win": "wsl rm -rf dist",
|
|
17
17
|
"compile": "tsc",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"dependency-graph": "madge --image dependency-graph.png dist",
|
|
22
22
|
"detect-cycles": "madge --circular dist",
|
|
23
23
|
"updates:check": "npm-check-updates",
|
|
24
|
-
"updates:apply": "npm-check-updates -u && npm update"
|
|
24
|
+
"updates:apply": "npm-check-updates -u && npm update",
|
|
25
|
+
"npm:publish:dryrun": "npm run clean && npm run compile && npm publish --dry-run"
|
|
25
26
|
},
|
|
26
27
|
"repository": {
|
|
27
28
|
"type": "git",
|
|
@@ -37,13 +38,14 @@
|
|
|
37
38
|
],
|
|
38
39
|
"author": "John Marshall",
|
|
39
40
|
"license": "MIT",
|
|
41
|
+
"homepage": "https://naisys.org",
|
|
40
42
|
"devDependencies": {
|
|
41
43
|
"@types/escape-html": "1.0.4",
|
|
42
44
|
"@types/js-yaml": "4.0.9",
|
|
43
|
-
"@types/node": "20.11.
|
|
45
|
+
"@types/node": "20.11.25",
|
|
44
46
|
"@types/text-table": "0.2.5",
|
|
45
|
-
"@typescript-eslint/eslint-plugin": "7.1.
|
|
46
|
-
"@typescript-eslint/parser": "7.1.
|
|
47
|
+
"@typescript-eslint/eslint-plugin": "7.1.1",
|
|
48
|
+
"@typescript-eslint/parser": "7.1.1",
|
|
47
49
|
"eslint": "8.57.0",
|
|
48
50
|
"jest": "29.7.0",
|
|
49
51
|
"prettier": "3.2.5",
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, jest } from "@jest/globals";
|
|
2
|
-
jest.unstable_mockModule("../../config.js", () => ({}));
|
|
3
|
-
const mockLogServiceWrite = jest
|
|
4
|
-
.fn()
|
|
5
|
-
.mockResolvedValue(1);
|
|
6
|
-
jest.unstable_mockModule("../../utils/logService.js", () => ({
|
|
7
|
-
write: mockLogServiceWrite,
|
|
8
|
-
}));
|
|
9
|
-
const output = await import("../../utils/output.js");
|
|
10
|
-
describe("commentAndLog function", () => {
|
|
11
|
-
it("should call writeDbLog with the correct arguments", async () => {
|
|
12
|
-
// Assuming you've refactored commentAndLog to take logService or its functionality as a parameter
|
|
13
|
-
await output.commentAndLog("Test message");
|
|
14
|
-
// Verify the mock was called correctly
|
|
15
|
-
expect(mockLogServiceWrite).toHaveBeenCalledWith({
|
|
16
|
-
content: "Test message",
|
|
17
|
-
role: "user",
|
|
18
|
-
type: "comment",
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
//# sourceMappingURL=output.test.js.map
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "@jest/globals";
|
|
2
|
-
import { valueFromString } from "../../utils/utilities.js";
|
|
3
|
-
describe("valueFromString", () => {
|
|
4
|
-
const obj = {
|
|
5
|
-
user: {
|
|
6
|
-
name: "John Doe",
|
|
7
|
-
contact: {
|
|
8
|
-
email: "john@example.com",
|
|
9
|
-
phone: {
|
|
10
|
-
home: "123456",
|
|
11
|
-
work: "789101",
|
|
12
|
-
},
|
|
13
|
-
},
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
test("retrieves a nested value successfully", () => {
|
|
17
|
-
expect(valueFromString(obj, "user.name")).toBe("John Doe");
|
|
18
|
-
expect(valueFromString(obj, "user.contact.email")).toBe("john@example.com");
|
|
19
|
-
expect(valueFromString(obj, "user.contact.phone.home")).toBe("123456");
|
|
20
|
-
});
|
|
21
|
-
test("returns undefined for non-existent path", () => {
|
|
22
|
-
expect(valueFromString(obj, "user.address")).toBeUndefined();
|
|
23
|
-
});
|
|
24
|
-
test("returns default value for non-existent path when specified", () => {
|
|
25
|
-
const defaultValue = "N/A";
|
|
26
|
-
expect(valueFromString(obj, "user.age", defaultValue)).toBe(defaultValue);
|
|
27
|
-
});
|
|
28
|
-
test("handles non-object inputs gracefully", () => {
|
|
29
|
-
expect(valueFromString(null, "user.name")).toBeUndefined();
|
|
30
|
-
expect(valueFromString(undefined, "user.name")).toBeUndefined();
|
|
31
|
-
expect(valueFromString("not-an-object", "user.name")).toBeUndefined();
|
|
32
|
-
});
|
|
33
|
-
test("deals with edge cases for paths", () => {
|
|
34
|
-
expect(valueFromString(obj, "")).toEqual(obj);
|
|
35
|
-
expect(valueFromString(obj, ".", "default")).toBe("default");
|
|
36
|
-
});
|
|
37
|
-
test("handles empty object and non-matching paths", () => {
|
|
38
|
-
expect(valueFromString({}, "user.name")).toBeUndefined();
|
|
39
|
-
expect(valueFromString(obj, "user.nonexistent.prop", "default")).toBe("default");
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
//# sourceMappingURL=utilities.test.js.map
|