naisys 1.0.3 → 1.2.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 CHANGED
@@ -11,6 +11,10 @@ vim or nano so point the LLM to use cat to read/write files in a single operatio
11
11
 
12
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
+ ```bash
15
+ npm install -g naisys
16
+ ```
17
+
14
18
  #### Node.js is used to create a simple proxy shell environment for the LLM that
15
19
 
16
20
  - Helps the LLM keep track of its current context size
@@ -61,7 +65,7 @@ title: Software Engineer
61
65
 
62
66
  # The model to use for console interactions
63
67
  # (gpt4turbo, gpt4turbo, gemini-pro, claude3sonnet, claude3opus, local)
64
- consoleModel: claude3sonnet
68
+ shellModel: claude3sonnet
65
69
 
66
70
  # The model to use for llmynx, pre-processing websites to fit into a smaller context
67
71
  webModel: gpt3turbo
@@ -85,14 +89,26 @@ tokenMax: 5000
85
89
  # No value or zero means wait indefinitely (debug driven)
86
90
  debugPauseSeconds: 5
87
91
 
88
- # If true, regardless of the debugPauseSeconds, the agent will not wake up on messages
89
- # With lots of agents this could be costly if they all end up mailing/replying each other in quick succession
92
+ # If true, regardless of the debugPauseSeconds, the agent will wake up on messages
93
+ # Useful for agents with long debugPauseSeconds, so that they can wake up and reply quickly
90
94
  wakeOnMessage: false
91
95
 
92
96
  # The maximum amount to spend on LLM interactions
93
97
  # Once reached the agent will stop and this value will need to be increased to continue
94
98
  spendLimitDollars: 2.00
95
- # Additional custom variables can be defined here and/or in the .env file to be loaded into the agent prompt
99
+
100
+ # Command Protection: Useful for agents you want to restrict from modifying the system
101
+ # None: Commands from the LLM run automatically, this is the default setting as well if the value is not set
102
+ # Manual: Every command the LLM wants to run has to be approved [y/n]. Not very autonomous.
103
+ # Auto: All commands are run through the separate LLM instace that will check to see if the command is safe
104
+ commandProtection: "none"
105
+
106
+ # Run these commands on session start, in the example below the agent will see how to use mail and a list of other agents
107
+ initialCommands:
108
+ - llmail users
109
+ - llmail help
110
+ - cat ${env.NAISYS_FOLDER}/home/${agent.username}/PLAN.md
111
+ # Additional custom variables can be defined here and/or in the agent config to be loaded into the agent prompt
96
112
  ```
97
113
 
98
114
  - Run `naisys <path to yaml or directory>`
package/bin/comment ADDED
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+
3
+ # ./src/command/commandHandler.ts has the same message
4
+ echo "Comment noted. Try running commands now to achieve your goal. ."
package/bin/endsession ADDED
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ echo "'endsession' cannot be used with other commands on the same prompt."
package/bin/llmail ADDED
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ echo "'llmail' cannot be used with other commands on the same prompt."
package/bin/llmynx ADDED
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ echo "'llmynx' cannot be used with other commands on the same prompt."
@@ -12,7 +12,7 @@ fi
12
12
 
13
13
  # Resolves the location of naisys from the bin directory
14
14
  SCRIPT=$(readlink -f "$0" || echo "$0")
15
- SCRIPT_DIR=$(dirname "$SCRIPT")
15
+ SCRIPT_DIR=$(dirname "$SCRIPT")/..
16
16
 
17
17
  # if path is a yaml file then start a single agent
18
18
  if [ -f "$1" ]; then
package/bin/pause ADDED
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ echo "'pause' cannot be used with other commands on the same prompt."
@@ -6,9 +6,8 @@ import * as utilities from "../utils/utilities.js";
6
6
  import { naisysToHostPath } from "../utils/utilities.js";
7
7
  const _dbFilePath = naisysToHostPath(`${config.naisysFolder}/lib/llmail.db`);
8
8
  let _myUserId = -1;
9
- // Implement maxes so that LLMs actively manage threads, archive, and create new ones
10
- const _threadTokenMax = config.agent.tokenMax / 2; // So 4000, would be 2000 thread max
11
- const _messageTokenMax = _threadTokenMax / 5; // Given the above a 400 token max, and 5 big messages per thread
9
+ /** Threading is not currently used so this doesn't matter */
10
+ const _threadTokenMax = config.mailMessageTokenMax * 5;
12
11
  /** 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
12
  * The problem with this was the agents were too chatty with so many mail commands, wasting context replying, reading threads, etc..
14
13
  * Simple mode only has two commands. It still requires db persistance to support offline agents. */
@@ -85,7 +84,7 @@ export async function handleCommand(args) {
85
84
  if (simpleMode) {
86
85
  return `llmail <command>
87
86
  users: Get list of users on the system
88
- send "<users>" "subject" "message": Send a message. ${_messageTokenMax} token max.`;
87
+ send "<users>" "subject" "message": Send a message. ${config.mailMessageTokenMax} token max.`;
89
88
  }
90
89
  else {
91
90
  return `llmail <command>
@@ -138,7 +137,8 @@ export async function handleCommand(args) {
138
137
  await init();
139
138
  return "llmail database reset";
140
139
  default:
141
- return "Unknown llmail command: " + argParams[0];
140
+ return ("Error, unknown command. See valid commands below:\n" +
141
+ (await handleCommand("help")));
142
142
  }
143
143
  }
144
144
  export async function getUnreadThreads() {
@@ -339,12 +339,18 @@ async function getUser(db, username) {
339
339
  }
340
340
  function validateMsgTokenCount(message) {
341
341
  const msgTokenCount = utilities.getTokenCount(message);
342
- if (msgTokenCount > _messageTokenMax) {
343
- throw `Error: Message is ${msgTokenCount} tokens, exceeding the limit of ${_messageTokenMax} tokens`;
342
+ if (msgTokenCount > config.mailMessageTokenMax) {
343
+ throw `Error: Message is ${msgTokenCount} tokens, exceeding the limit of ${config.mailMessageTokenMax} tokens`;
344
344
  }
345
345
  return msgTokenCount;
346
346
  }
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
@@ -15,53 +15,50 @@ 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.agent.tokenMax / 8;
19
18
  if (!argParams[0]) {
20
19
  argParams[0] = "help";
21
20
  }
22
21
  switch (argParams[0]) {
23
22
  case "help":
24
- return `llmynx <command> (results will be reduced to around ${defualtTokenMax})
23
+ return `llmynx <command> (results will be reduced to around ${config.webTokenMax})
25
24
  search <query>: Search google for the given query
26
25
  open <url>: Opens the given url. Links are represented as numbers in brackets which prefix the word they are linking like [123]
27
26
  follow <link number>: Opens the given link number. Link numbers work across all previous outputs
28
- links <url> <page>: Lists only the links for the given url. Use the page number to get more links`;
27
+ links <url> <page>: Lists only the links for the given url. Use the page number to get more links
28
+
29
+ *llmynx does not support input. Use llmynx or curl to call APIs directly*`;
29
30
  case "search": {
30
31
  const query = argParams.slice(1).join(" ");
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
- true, true);
32
+ return await loadUrl("https://www.google.com/search?q=" + encodeURIComponent(query), true, true);
33
33
  }
34
34
  case "open": {
35
35
  const url = argParams[1];
36
- const isNumber = !isNaN(parseInt(argParams[2]));
37
- const tokenMax = isNumber ? parseInt(argParams[2]) : defualtTokenMax;
38
- return await loadUrl(url, tokenMax, false, true);
36
+ return await loadUrl(url, false, true);
39
37
  }
40
38
  case "follow": {
41
39
  const linkNum = parseInt(argParams[1]);
42
- const isNumber = !isNaN(parseInt(argParams[2]));
43
- const tokenMax = isNumber ? parseInt(argParams[2]) : defualtTokenMax;
44
40
  const linkUrl = _globalLinkMap.get(linkNum);
45
41
  if (!linkUrl) {
46
42
  return "Link number not found";
47
43
  }
48
- return await loadUrl(linkUrl, tokenMax, true, false);
44
+ return await loadUrl(linkUrl, true, false);
49
45
  }
50
46
  case "links": {
51
47
  const url = argParams[1];
52
48
  const isNumber = !isNaN(parseInt(argParams[2]));
53
49
  const pageNumber = isNumber ? parseInt(argParams[2]) : 1;
54
- return await loadUrl(url, 600, false, false, pageNumber);
50
+ return await loadUrl(url, false, false, pageNumber);
55
51
  }
56
52
  // Secret command to toggle debug mode
57
53
  case "debug":
58
54
  debugMode = !debugMode;
59
55
  return "Debug mode toggled " + (debugMode ? "on" : "off");
60
56
  default:
61
- return "Unknown llmynx command: " + argParams[0];
57
+ return ("Error, unknown command. See valid commands below:\n" +
58
+ (await handleCommand("help")));
62
59
  }
63
60
  }
64
- async function loadUrl(url, tokenMax, showUrl, showFollowHint, linkPageAsContent) {
61
+ async function loadUrl(url, showUrl, showFollowHint, linkPageAsContent) {
65
62
  let content = await runLynx(url);
66
63
  let links = "";
67
64
  // Reverse find 'References: ' and cut everything after it from the content
@@ -79,13 +76,13 @@ async function loadUrl(url, tokenMax, showUrl, showFollowHint, linkPageAsContent
79
76
  outputInDebugMode(`Content Token size: ${contentTokenSize}\n` +
80
77
  `Links Token size: ${linksTokenSize}`);
81
78
  // Reduce content using LLM if it's over the token max
82
- if (contentTokenSize > tokenMax) {
79
+ if (contentTokenSize > config.webTokenMax) {
83
80
  const model = getLLModel(config.agent.webModel);
84
81
  // For example if context is 16k, and max tokens is 2k, 3k with 1.5x overrun
85
82
  // That would be 3k for the current compressed content, 10k for the chunk, and 3k for the output
86
- let tokenChunkSize = model.maxTokens - tokenMax * 2 * 1.5;
83
+ let tokenChunkSize = model.maxTokens - config.webTokenMax * 2 * 1.5;
87
84
  if (linkPageAsContent) {
88
- tokenChunkSize = tokenMax;
85
+ tokenChunkSize = config.webTokenMax;
89
86
  }
90
87
  outputInDebugMode(`Token max chunk size: ${tokenChunkSize}`);
91
88
  const pieceCount = Math.ceil(contentTokenSize / tokenChunkSize);
@@ -100,10 +97,10 @@ async function loadUrl(url, tokenMax, showUrl, showFollowHint, linkPageAsContent
100
97
  }
101
98
  continue;
102
99
  }
103
- output.comment(`Processing Piece ${i + 1} of ${pieceCount}...`);
100
+ output.comment(`Processing Piece ${i + 1} of ${pieceCount} with ${model.key}...`);
104
101
  outputInDebugMode(` Reduced output tokens: ${utilities.getTokenCount(reducedOutput)}\n` +
105
102
  ` Current Piece tokens: ${utilities.getTokenCount(pieceStr)}`);
106
- reducedOutput = await llmReduce(url, reducedOutput, i + 1, pieceCount, pieceStr, tokenMax);
103
+ reducedOutput = await llmReduce(url, reducedOutput, i + 1, pieceCount, pieceStr);
107
104
  }
108
105
  if (linkPageAsContent) {
109
106
  return "";
@@ -113,7 +110,7 @@ async function loadUrl(url, tokenMax, showUrl, showFollowHint, linkPageAsContent
113
110
  output.comment(`Content reduced from ${contentTokenSize} to ${finalTokenSize} tokens`);
114
111
  }
115
112
  else {
116
- output.comment(`Content is already under ${tokenMax} tokens.`);
113
+ output.comment(`Content is already under ${config.webTokenMax} tokens.`);
117
114
  }
118
115
  // Prefix content with url if following as otherwise the url is never shown
119
116
  if (showUrl) {
@@ -132,22 +129,25 @@ async function runLynx(url) {
132
129
  const modeParams = "";
133
130
  const ifWindows = os.platform() === "win32" ? "wsl " : "";
134
131
  exec(`${ifWindows}lynx -dump ${modeParams} "${url}"`, (error, stdout, stderr) => {
135
- if (error) {
136
- resolve(`error: ${error.message}`);
137
- return;
132
+ let output = "";
133
+ if (stdout) {
134
+ output += stdout;
135
+ }
136
+ // I've only seen either/or, but just in case
137
+ if (stdout && stderr) {
138
+ output += "\nError:\n";
138
139
  }
139
140
  if (stderr) {
140
- resolve(`stderr: ${stderr}`);
141
- return;
141
+ output += stderr;
142
142
  }
143
- resolve(stdout);
143
+ resolve(output);
144
144
  });
145
145
  });
146
146
  }
147
- async function llmReduce(url, reducedOutput, pieceNumber, pieceTotal, pieceStr, tokenMax) {
147
+ async function llmReduce(url, reducedOutput, pieceNumber, pieceTotal, pieceStr) {
148
148
  const systemMessage = `You will be iteratively fed the web page ${url} broken into ${pieceTotal} sequential equally sized pieces.
149
149
  Each piece should be reduced into the final content in order to maintain the meaning of the page while reducing verbosity and duplication.
150
- The final output should be around ${tokenMax} tokens.
150
+ The final output should be around ${config.webTokenMax} tokens.
151
151
  Don't remove links which are represented as numbers in brackets which prefix the word they are linking like [123].
152
152
  Try to prioritize content of substance over advertising content.`;
153
153
  const content = `Web page piece ${pieceNumber} of ${pieceTotal}:
@@ -156,7 +156,7 @@ ${pieceStr}
156
156
  Current reduced content:
157
157
  ${reducedOutput}
158
158
 
159
- Please merge the new piece into the existing reduced content above while keeping the result to around ${tokenMax} tokens.
159
+ Please merge the new piece into the existing reduced content above while keeping the result to around ${config.webTokenMax} tokens.
160
160
 
161
161
  Merged reduced content:
162
162
  `;
@@ -11,6 +11,7 @@ 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
13
  import * as utilities from "../utils/utilities.js";
14
+ import * as commandProtection from "./commandProtection.js";
14
15
  import * as promptBuilder from "./promptBuilder.js";
15
16
  import * as shellCommand from "./shellCommand.js";
16
17
  export var NextCommandAction;
@@ -20,7 +21,7 @@ export var NextCommandAction;
20
21
  NextCommandAction[NextCommandAction["ExitApplication"] = 2] = "ExitApplication";
21
22
  })(NextCommandAction || (NextCommandAction = {}));
22
23
  export let previousSessionNotes = await logService.getPreviousEndSessionNote();
23
- export async function consoleInput(prompt, consoleInput) {
24
+ export async function processCommand(prompt, consoleInput) {
24
25
  // We process the lines one at a time so we can support multiple commands with line breaks
25
26
  let firstLine = true;
26
27
  let processNextLLMpromptBlock = true;
@@ -47,12 +48,20 @@ export async function consoleInput(prompt, consoleInput) {
47
48
  await output.commentAndLog("Continuing with next command from same LLM response...");
48
49
  await contextManager.append(input, ContentSource.LLM);
49
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
+ }
50
58
  }
51
59
  const cmdParams = input.split(" ");
52
60
  const cmdArgs = input.slice(cmdParams[0].length).trim();
53
61
  switch (cmdParams[0]) {
54
62
  case "comment": {
55
63
  // Important - Hint the LLM to turn their thoughts into accounts
64
+ // ./bin/comment shell script has the same message
56
65
  await contextManager.append("Comment noted. Try running commands now to achieve your goal.");
57
66
  break;
58
67
  }
@@ -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.consoleModel} model`);
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,29 @@ 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
- await commandHandler.consoleInput(await promptBuilder.getPrompt(), "llmail help");
39
- await commandHandler.consoleInput(await promptBuilder.getPrompt(), "llmail users");
38
+ for (const initialCommand of config.agent.initialCommands) {
39
+ await commandHandler.processCommand(await promptBuilder.getPrompt(0, false), config.resolveConfigVars(initialCommand));
40
+ }
40
41
  inputMode.toggle(InputMode.Debug);
41
42
  let pauseSeconds = config.agent.debugPauseSeconds;
42
43
  let wakeOnMessage = config.agent.wakeOnMessage;
43
44
  while (nextCommandAction == NextCommandAction.Continue) {
44
45
  const prompt = await promptBuilder.getPrompt(pauseSeconds, wakeOnMessage);
45
- let input = "";
46
+ let consoleInput = "";
46
47
  // Debug command prompt
47
48
  if (inputMode.current === InputMode.Debug) {
48
- input = await promptBuilder.getInput(`${prompt}`, pauseSeconds, wakeOnMessage);
49
+ consoleInput = await promptBuilder.getInput(`${prompt}`, pauseSeconds, wakeOnMessage);
49
50
  }
50
51
  // LLM command prompt
51
52
  else if (inputMode.current === InputMode.LLM) {
52
53
  const workingMsg = prompt +
53
- chalk[output.OutputColor.loading](`LLM (${config.agent.consoleModel}) Working...`);
54
+ chalk[output.OutputColor.loading](`LLM (${config.agent.shellModel}) Working...`);
54
55
  try {
55
- await displayNewMail();
56
- await displayContextWarning();
56
+ await checkNewMailNotification();
57
+ await checkContextLimitWarning();
57
58
  await contextManager.append(prompt, ContentSource.ConsolePrompt);
58
59
  process.stdout.write(workingMsg);
59
- input = await llmService.query(config.agent.consoleModel, contextManager.getSystemMessage(), contextManager.messages, "console");
60
+ consoleInput = await llmService.query(config.agent.shellModel, contextManager.getSystemMessage(), contextManager.messages, "console");
60
61
  clearPromptMessage(workingMsg);
61
62
  }
62
63
  catch (e) {
@@ -73,7 +74,7 @@ export async function run() {
73
74
  // Run the command
74
75
  try {
75
76
  ({ nextCommandAction, pauseSeconds, wakeOnMessage } =
76
- await commandHandler.consoleInput(prompt, input));
77
+ await commandHandler.processCommand(prompt, consoleInput));
77
78
  if (inputMode.current == InputMode.LLM) {
78
79
  llmErrorCount = 0;
79
80
  }
@@ -85,7 +86,7 @@ export async function run() {
85
86
  }
86
87
  // If the user is in debug mode and they didn't enter anything, switch to LLM
87
88
  // If in LLM mode, auto switch back to debug
88
- if ((inputMode.current == InputMode.Debug && !input) ||
89
+ if ((inputMode.current == InputMode.Debug && !consoleInput) ||
89
90
  inputMode.current == InputMode.LLM) {
90
91
  inputMode.toggle();
91
92
  }
@@ -136,7 +137,7 @@ async function handleErrorAndSwitchToDebugMode(e, llmErrorCount, addToContext) {
136
137
  wakeOnMessage,
137
138
  };
138
139
  }
139
- async function displayNewMail() {
140
+ async function checkNewMailNotification() {
140
141
  // Check for unread threads
141
142
  const unreadThreads = await llmail.getUnreadThreads();
142
143
  if (!unreadThreads.length) {
@@ -174,7 +175,7 @@ async function displayNewMail() {
174
175
  `Use llmail read <id>' to read the thread, but be mindful you are close to the token limit for the session.`, ContentSource.Console);
175
176
  }
176
177
  }
177
- async function displayContextWarning() {
178
+ async function checkContextLimitWarning() {
178
179
  const tokenCount = contextManager.getTokenCount();
179
180
  const tokenMax = config.agent.tokenMax;
180
181
  if (tokenCount > tokenMax) {
@@ -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
@@ -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,22 @@ export async function handleCommand(input) {
29
31
  }
30
32
  const output = await shellWrapper.executeCommand(input);
31
33
  if (output.value) {
32
- await contextManager.append(output.value);
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 =
42
+ text.slice(0, trimLength / 2) +
43
+ "\n\n...\n\n" +
44
+ text.slice(-trimLength / 2);
45
+ }
46
+ await contextManager.append(text);
47
+ if (outputLimitExceeded) {
48
+ await contextManager.append(`\nThe shell command generated too much output (${tokenCount} tokens). Only 2,000 tokens worth are shown above.`);
49
+ }
33
50
  }
34
51
  response.hasErrors = output.hasErrors;
35
52
  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
- commentIfNotEmpty(await executeCommand(`mkdir -p ${config.naisysFolder}/home/` + config.agent.username));
43
- commentIfNotEmpty(await executeCommand(`cd ${config.naisysFolder}/home/` + config.agent.username));
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
- commentIfNotEmpty(await executeCommand("cd " + _currentPath));
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 commentIfNotEmpty(response) {
55
+ function errorIfNotEmpty(response) {
56
56
  if (response.value) {
57
- output.comment(response.value);
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 after 5 seconds, kill and reset the shell, often hanging on some unescaped input
132
- const timeoutSeconds = 5;
133
- _currentCommandTimeout = setTimeout(() => {
134
- if (_resolveCurrentCommand) {
135
- _process === null || _process === void 0 ? void 0 : _process.kill();
136
- output.error("SHELL TIMEMOUT/KILLED. PID: " + (_process === null || _process === void 0 ? void 0 : _process.pid));
137
- resetProcess();
138
- _resolveCurrentCommand({
139
- value: `Error: Command timed out after ${timeoutSeconds} seconds.`,
140
- hasErrors: true,
141
- });
142
- }
143
- }, timeoutSeconds * 1000);
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() {
@@ -167,14 +170,16 @@ function resetProcess() {
167
170
  * May also help with common escaping errors */
168
171
  function runCommandFromScript(command) {
169
172
  const scriptPath = `${config.naisysFolder}/home/${config.agent.username}/.command.tmp.sh`;
170
- // set -e causes the script to exit on any error
173
+ // set -e causes the script to exit on the first error
171
174
  const scriptContent = `#!/bin/bash
172
175
  set -e
173
176
  cd ${_currentPath}
174
177
  ${command.trim()}`;
175
178
  // create/writewrite file
176
179
  fs.writeFileSync(naisysToHostPath(scriptPath), scriptContent);
177
- // Source will run the script in the current shell, so any change directories in the script should persist in the current shell
178
- return `source ${scriptPath}`;
180
+ // `Path` is set to the ./bin folder because custom NAISYS commands that follow shell commands will be handled by the shell, which will fail
181
+ // so we need to remind the LLM that 'naisys commands cannot be used with other commands on the same prompt'
182
+ // `source` will run the script in the current shell, so any change directories in the script will persist in the current shell
183
+ return `PATH=${config.binPath}:$PATH source ${scriptPath}`;
179
184
  }
180
185
  //# sourceMappingURL=shellWrapper.js.map
package/dist/config.js CHANGED
@@ -2,11 +2,16 @@ 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";
11
+ export const shellOutputTokenMax = 2500; // Limits the size of files that can be read/wrote
12
+ export const shellCommmandTimeoutSeconds = 15; // The number of seconds NAISYS will wait for a shell command to complete
13
+ export const webTokenMax = 2500;
14
+ export const mailMessageTokenMax = 400;
10
15
  /* .env is used for global configs across naisys, while agent configs are for the specific agent */
11
16
  export const naisysFolder = getEnv("NAISYS_FOLDER", true);
12
17
  export const websiteFolder = getEnv("WEBSITE_FOLDER");
@@ -16,34 +21,45 @@ export const openaiApiKey = getEnv("OPENAI_API_KEY");
16
21
  export const googleApiKey = getEnv("GOOGLE_API_KEY");
17
22
  export const anthropicApiKey = getEnv("ANTHROPIC_API_KEY");
18
23
  export const agent = loadAgentConfig();
19
- function getEnv(key, required) {
20
- const value = process.env[key];
21
- if (!value && required) {
22
- throw `Config: Error, .env ${key} is not defined`;
23
- }
24
- return value;
25
- }
26
24
  function loadAgentConfig() {
27
25
  const agentPath = program.args[0];
28
- const checkAgentConfig = yaml.load(fs.readFileSync(agentPath, "utf8"));
26
+ const config = yaml.load(fs.readFileSync(agentPath, "utf8"));
29
27
  // throw if any property is undefined
30
28
  for (const key of [
31
29
  "username",
32
30
  "title",
33
- "consoleModel",
31
+ "shellModel",
34
32
  "webModel",
35
33
  "agentPrompt",
36
34
  "spendLimitDollars",
37
35
  "tokenMax",
38
- // debugPauseSeconds and wakeOnMessage can be undefined
36
+ // other properties can be undefined
39
37
  ]) {
40
- if (!valueFromString(checkAgentConfig, key)) {
38
+ if (!valueFromString(config, key)) {
41
39
  throw `Agent config: Error, ${key} is not defined`;
42
40
  }
43
41
  }
44
- return checkAgentConfig;
42
+ // Sanitize input
43
+ if (!config.initialCommands) {
44
+ config.initialCommands = [];
45
+ }
46
+ else if (!Array.isArray(config.initialCommands)) {
47
+ throw `Agent config: Error, 'initialCommands' is not an array`;
48
+ }
49
+ config.debugPauseSeconds = config.debugPauseSeconds
50
+ ? Number(config.debugPauseSeconds)
51
+ : 0;
52
+ config.wakeOnMessage = Boolean(config.wakeOnMessage);
53
+ if (!config.commandProtection) {
54
+ config.commandProtection = CommandProtection.None;
55
+ }
56
+ if (!Object.values(CommandProtection).includes(config.commandProtection)) {
57
+ throw `Agent config: Error, 'commandProtection' is not a valid value`;
58
+ }
59
+ return config;
45
60
  }
46
61
  export const packageVersion = await getVersion();
62
+ export const binPath = getBinPath();
47
63
  /** Can only get version from env variable when naisys is started with npm,
48
64
  * otherwise need to rip it from the package ourselves relative to where this file is located */
49
65
  async function getVersion() {
@@ -58,4 +74,35 @@ async function getVersion() {
58
74
  return "0.1";
59
75
  }
60
76
  }
77
+ function getEnv(key, required) {
78
+ const value = process.env[key];
79
+ if (!value && required) {
80
+ throw `Config: Error, .env ${key} is not defined`;
81
+ }
82
+ return value;
83
+ }
84
+ export function resolveConfigVars(templateString) {
85
+ let resolvedString = templateString;
86
+ resolvedString = resolveTemplateVars(resolvedString, "agent", agent);
87
+ resolvedString = resolveTemplateVars(resolvedString, "env", process.env);
88
+ return resolvedString;
89
+ }
90
+ function resolveTemplateVars(templateString, allowedVarString, mappedVar) {
91
+ const pattern = new RegExp(`\\$\\{${allowedVarString}\\.([^}]+)\\}`, "g");
92
+ return templateString.replace(pattern, (match, key) => {
93
+ const value = valueFromString(mappedVar, key);
94
+ if (value === undefined) {
95
+ throw `Agent config: Error, ${key} is not defined`;
96
+ }
97
+ return value;
98
+ });
99
+ }
100
+ function getBinPath() {
101
+ // C:/git/naisys/dist/config.js
102
+ let binPath = new URL("../bin", import.meta.url).pathname;
103
+ if (binPath.startsWith("/C:")) {
104
+ binPath = "/mnt/c" + binPath.substring(3);
105
+ }
106
+ return binPath;
107
+ }
61
108
  //# sourceMappingURL=config.js.map
@@ -5,7 +5,6 @@ import * as logService from "../utils/logService.js";
5
5
  import * as output from "../utils/output.js";
6
6
  import { OutputColor } from "../utils/output.js";
7
7
  import * as utilities from "../utils/utilities.js";
8
- import { valueFromString } from "../utils/utilities.js";
9
8
  import { LlmRole } from "./llmDtos.js";
10
9
  export var ContentSource;
11
10
  (function (ContentSource) {
@@ -23,8 +22,7 @@ export function getSystemMessage() {
23
22
  // A lot of the stipulations in here are to prevent common LLM mistakes
24
23
  // Like we can't jump between standard and special commands in a single prompt, which the LLM will try to do if not warned
25
24
  let agentPrompt = config.agent.agentPrompt;
26
- agentPrompt = resolveTemplateVars(agentPrompt, "agent", config.agent);
27
- agentPrompt = resolveTemplateVars(agentPrompt, "env", process.env);
25
+ agentPrompt = config.resolveConfigVars(agentPrompt);
28
26
  const systemMessage = `${agentPrompt.trim()}
29
27
 
30
28
  This is a command line interface presenting you with the next command prompt.
@@ -38,12 +36,12 @@ NAISYS ${config.packageVersion} Shell
38
36
  Welcome back ${config.agent.username}!
39
37
  MOTD:
40
38
  Date: ${new Date().toLocaleString()}
41
- Commands:
42
- Standard Unix commands are available
39
+ LINUX Commands:
40
+ Standard Linux commands are available
43
41
  vi and nano are not supported
44
42
  Read files with cat. Write files with \`cat > filename << 'EOF'\`
45
43
  Do not input notes after the prompt. Only valid commands.
46
- Special Commands: (Don't mix with standard commands on the same prompt)
44
+ NAISYS Commands: (cannot be used with other commands on the same prompt)
47
45
  llmail: A local mail system for communicating with your team
48
46
  llmynx: A context optimized web browser. Enter 'llmynx help' to learn how to use it
49
47
  comment "<thought>": Any non-command output like thinking out loud, prefix with the 'comment' command
@@ -51,23 +49,12 @@ Special Commands: (Don't mix with standard commands on the same prompt)
51
49
  endsession "<note>": Ends this session, clears the console log and context.
52
50
  The note should help you find your bearings in the next session.
53
51
  The note should contain your next goal, and important things should you remember.
54
- Try to keep the note around 400 tokens.
55
52
  Tokens:
56
53
  The console log can only hold a certain number of 'tokens' that is specified in the prompt
57
54
  Make sure to call endsession before the limit is hit so you can continue your work with a fresh console`;
58
55
  _cachedSystemMessage = systemMessage;
59
56
  return systemMessage;
60
57
  }
61
- function resolveTemplateVars(templateString, allowedVarString, mappedVar) {
62
- const pattern = new RegExp(`\\$\\{${allowedVarString}\\.([^}]+)\\}`, "g");
63
- return templateString.replace(pattern, (match, key) => {
64
- const value = valueFromString(mappedVar, key);
65
- if (value === undefined) {
66
- throw `Agent config: Error, ${key} is not defined`;
67
- }
68
- return value;
69
- });
70
- }
71
58
  export let messages = [];
72
59
  export async function append(text, source = ContentSource.Console) {
73
60
  // Debug runs in a shadow mode where their activity is not recorded in the context
@@ -80,24 +80,37 @@ async function sendWithGoogle(modelKey, systemMessage, context, source) {
80
80
  if (lastMessage.role !== LlmRole.User) {
81
81
  throw "Error, last message on context is not a user message";
82
82
  }
83
+ const contextHistory = context
84
+ .filter((m) => m != lastMessage)
85
+ .map((m) => ({
86
+ role: m.role == LlmRole.Assistant ? "model" : "user",
87
+ parts: [
88
+ {
89
+ text: m.content,
90
+ },
91
+ ],
92
+ }));
83
93
  const history = [
84
94
  {
85
95
  role: LlmRole.User, // System role is not supported by Google API
86
- parts: systemMessage,
96
+ parts: [
97
+ {
98
+ text: systemMessage,
99
+ },
100
+ ],
87
101
  },
88
102
  {
89
103
  role: "model",
90
- parts: "Understood",
104
+ parts: [
105
+ {
106
+ text: "Understood",
107
+ },
108
+ ],
91
109
  },
92
- ...context
93
- .filter((m) => m != lastMessage)
94
- .map((m) => ({
95
- role: m.role == LlmRole.Assistant ? "model" : LlmRole.User,
96
- parts: m.content,
97
- })),
110
+ ...contextHistory,
98
111
  ];
99
112
  const chat = googleModel.startChat({
100
- history: history,
113
+ history,
101
114
  generationConfig: {
102
115
  maxOutputTokens: 2000,
103
116
  },
@@ -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/package.json CHANGED
@@ -1,27 +1,28 @@
1
1
  {
2
2
  "name": "naisys",
3
3
  "description": "Node.js Autonomous Intelligence System",
4
- "version": "1.0.3",
4
+ "version": "1.2.0",
5
5
  "type": "module",
6
6
  "main": "dist/naisys.js",
7
7
  "preferGlobal": true,
8
8
  "bin": {
9
- "naisys": "naisys.sh"
9
+ "naisys": "./bin/naisys"
10
10
  },
11
11
  "scripts": {
12
- "compile/run/attachable": "tsc && node --inspect dist/naisys.js ./agents/example.yaml",
13
- "run agent:dev": "node dist/naisys.js ./agents/eva-site-2-team/dev.yaml",
14
- "run agent:admin": "node dist/naisys.js ./agents/eva-site-2-team/admin.yaml",
12
+ "compile/run/attachable": "tsc && node --inspect dist/naisys.js ./agents/webdev-fansite.yaml",
13
+ "run agent:p1": "node dist/naisys.js ./agents/webdev-battle/player1.yaml",
14
+ "run agent:p2": "node dist/naisys.js ./agents/webdev-battle/player2.yaml",
15
15
  "clean": "rm -rf dist",
16
- "clean:win": "wsl rm -rf dist",
17
- "compile": "tsc",
16
+ "compile": "tsc --build --verbose",
18
17
  "eslint": "npx eslint --rulesdir eslint-rules src",
19
18
  "test": "tsc && node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=dist/__tests__",
20
19
  "prettier": "npx prettier --write .",
21
20
  "dependency-graph": "madge --image dependency-graph.png dist",
22
21
  "detect-cycles": "madge --circular dist",
23
22
  "updates:check": "npm-check-updates",
24
- "updates:apply": "npm-check-updates -u && npm update"
23
+ "updates:apply": "npm-check-updates -u && npm update",
24
+ "npm:publish:dryrun": "npm run clean && npm run compile && npm publish --dry-run",
25
+ "postinstall": "chmod +x ./bin/*"
25
26
  },
26
27
  "repository": {
27
28
  "type": "git",
@@ -37,22 +38,23 @@
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.25",
45
+ "@types/node": "20.11.26",
44
46
  "@types/text-table": "0.2.5",
45
- "@typescript-eslint/eslint-plugin": "7.1.1",
46
- "@typescript-eslint/parser": "7.1.1",
47
+ "@typescript-eslint/eslint-plugin": "7.2.0",
48
+ "@typescript-eslint/parser": "7.2.0",
47
49
  "eslint": "8.57.0",
48
50
  "jest": "29.7.0",
49
51
  "prettier": "3.2.5",
50
52
  "ts-node": "10.9.2",
51
- "typescript": "5.3.3"
53
+ "typescript": "5.4.2"
52
54
  },
53
55
  "dependencies": {
54
- "@anthropic-ai/sdk": "0.16.1",
55
- "@google/generative-ai": "0.2.1",
56
+ "@anthropic-ai/sdk": "0.17.2",
57
+ "@google/generative-ai": "0.3.0",
56
58
  "chalk": "5.3.0",
57
59
  "commander": "12.0.0",
58
60
  "dotenv": "16.4.5",