@scout9/app 1.0.0-alpha.0.5.7 → 1.0.0-alpha.0.5.9

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.
@@ -5,16 +5,18 @@ import bodyParser from 'body-parser';
5
5
  import { config as dotenv } from 'dotenv';
6
6
  import { Configuration, Scout9Api } from '@scout9/admin';
7
7
  import { EventResponse, ProgressLogger } from '@scout9/app';
8
- import { WorkflowEventSchema, WorkflowResponseSchema } from '@scout9/app/schemas';
8
+ import { WorkflowEventSchema, WorkflowResponseSchema, MessageSchema } from '@scout9/app/schemas';
9
9
  import { Spirits } from '@scout9/app/spirits';
10
10
  import path, { resolve } from 'node:path';
11
+ import { createServer } from 'node:http';
11
12
  import fs from 'node:fs';
12
13
  import https from 'node:https';
13
14
  import { fileURLToPath, pathToFileURL } from 'node:url';
14
15
  import { readdir } from 'fs/promises';
15
16
  import { ZodError } from 'zod';
16
17
  import { fromError } from 'zod-validation-error';
17
- import { bgBlack, blue, bold, cyan, green, grey, magenta, red, white } from 'kleur/colors';
18
+ import { bgBlack, blue, bold, cyan, green, grey, magenta, red, white, yellow } from 'kleur/colors';
19
+
18
20
  import projectApp from './src/app.js';
19
21
  import config from './config.js';
20
22
 
@@ -86,10 +88,10 @@ const handleError = (e, res = undefined, tag = undefined, body = undefined) => {
86
88
  let name = e?.name || 'Runtime Error';
87
89
  let message = e?.message || 'Unknown error';
88
90
  let code = typeof e?.code === 'number'
89
- ? e.code
90
- : typeof e?.status === 'number'
91
- ? e.status
92
- : 500;
91
+ ? e.code
92
+ : typeof e?.status === 'number'
93
+ ? e.status
94
+ : 500;
93
95
  if ('response' in e) {
94
96
  const response = e.response;
95
97
  if (response?.status) {
@@ -128,18 +130,19 @@ const handleError = (e, res = undefined, tag = undefined, body = undefined) => {
128
130
  code
129
131
  }));
130
132
  }
133
+ return {name, error: message, code};
131
134
  };
132
135
 
133
136
  const handleZodError = ({
134
- error,
135
- res = undefined,
136
- code = 500,
137
- status,
138
- name,
139
- bodyLabel = 'Provided Input',
140
- body = undefined,
141
- action = ''
142
- }) => {
137
+ error,
138
+ res = undefined,
139
+ code = 500,
140
+ status,
141
+ name,
142
+ bodyLabel = 'Provided Input',
143
+ body = undefined,
144
+ action = ''
145
+ }) => {
143
146
  res?.writeHead?.(code, {'Content-Type': 'application/json'});
144
147
  if (error instanceof ZodError) {
145
148
  const formattedError = simplifyZodError(error);
@@ -170,13 +173,13 @@ const handleWorkflowResponse = async ({fun, workflowEvent, tag, expressRes: res,
170
173
  let response;
171
174
  try {
172
175
  response = await fun(workflowEvent)
173
- .then((slots) => {
174
- if ('toJSON' in slots) {
175
- return slots.toJSON();
176
- } else {
177
- return slots;
178
- }
179
- });
176
+ .then((slots) => {
177
+ if ('toJSON' in slots) {
178
+ return slots.toJSON();
179
+ } else {
180
+ return slots;
181
+ }
182
+ });
180
183
  } catch (error) {
181
184
  if (error instanceof ZodError) {
182
185
  handleZodError({
@@ -222,7 +225,7 @@ const handleWorkflowResponse = async ({fun, workflowEvent, tag, expressRes: res,
222
225
  }
223
226
  }
224
227
 
225
- }
228
+ };
226
229
 
227
230
  const makeRequest = async (options, maxRedirects = 10) => {
228
231
  return new Promise((resolve, reject) => {
@@ -279,6 +282,12 @@ app.use(bodyParser.json());
279
282
  if (dev) {
280
283
  app.use(compression());
281
284
  app.use(sirv(path.resolve(__dirname, 'public'), {dev: true}));
285
+ app.use((req, res, next) => {
286
+ res.setHeader('Access-Control-Allow-Origin', '*'); // Adjust origin as needed
287
+ res.setHeader('Access-Control-Allow-Methods', 'GET, PUT, POST, OPTIONS');
288
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
289
+ next();
290
+ });
282
291
  }
283
292
 
284
293
  function parseWorkflowEvent(req, res) {
@@ -323,9 +332,9 @@ app.post(dev ? '/dev/workflow' : '/', async (req, res) => {
323
332
  await handleWorkflowResponse({
324
333
  fun: projectApp,
325
334
  workflowEvent,
326
- tag: "PMTFlow",
335
+ tag: 'PMTFlow',
327
336
  expressRes: res,
328
- expressReq: req,
337
+ expressReq: req
329
338
  });
330
339
  return;
331
340
  });
@@ -361,17 +370,17 @@ async function resolveEntityApi(entity, method) {
361
370
  }
362
371
  const {api, entities} = resolveEntity(entity, method);
363
372
  const mod = await import(pathToFileURL(path.resolve(__dirname, `./src/entities/${entities.join('/')}/api.js`)).href)
364
- .catch((e) => {
365
- switch (e.code) {
366
- case 'ERR_MODULE_NOT_FOUND':
367
- case 'MODULE_NOT_FOUND':
368
- console.error(e);
369
- throw new Error(`Invalid entity: no API method`);
370
- default:
371
- console.error(e);
372
- throw new Error(`Invalid entity: Internal system error`);
373
- }
374
- });
373
+ .catch((e) => {
374
+ switch (e.code) {
375
+ case 'ERR_MODULE_NOT_FOUND':
376
+ case 'MODULE_NOT_FOUND':
377
+ console.error(e);
378
+ throw new Error(`Invalid entity: no API method`);
379
+ default:
380
+ console.error(e);
381
+ throw new Error(`Invalid entity: Internal system error`);
382
+ }
383
+ });
375
384
  if (mod[method]) {
376
385
  return mod[method];
377
386
  }
@@ -384,7 +393,6 @@ async function resolveEntityApi(entity, method) {
384
393
 
385
394
  }
386
395
 
387
-
388
396
  function extractParamsFromPath(path) {
389
397
  const segments = path.split('/').filter(Boolean); // Split and remove empty segments
390
398
  let params = {};
@@ -468,7 +476,7 @@ async function runCommandApi(req, res) {
468
476
  const params = url.split('/').slice(2).filter(Boolean);
469
477
  try {
470
478
  const files = await getFilesRecursive(commandsDir).then(files => files.map(file => file.replace(commandsDir, '.'))
471
- .filter(file => params.every(p => file.includes(p))));
479
+ .filter(file => params.every(p => file.includes(p))));
472
480
  file = files?.[0];
473
481
  } catch (e) {
474
482
  console.log('No commands found', e.message);
@@ -516,7 +524,13 @@ async function runCommandApi(req, res) {
516
524
  return;
517
525
  }
518
526
 
519
- await handleWorkflowResponse({fun: mod.default, workflowEvent, tag: `${params.join('_').toUpperCase()} Command`, expressReq: req, expressRes: res});
527
+ await handleWorkflowResponse({
528
+ fun: mod.default,
529
+ workflowEvent,
530
+ tag: `${params.join('_').toUpperCase()} Command`,
531
+ expressReq: req,
532
+ expressRes: res
533
+ });
520
534
  }
521
535
 
522
536
  app.post('/commands/:command', runCommandApi);
@@ -533,9 +547,11 @@ app.post('/entity/:entity/*', runEntityApi);
533
547
  app.delete('/entity/:entity/*', runEntityApi);
534
548
 
535
549
  // For local development: parse a message
536
- let devProgram;
550
+ let devReadlineProgram;
551
+ let devServer;
537
552
  if (dev) {
538
553
 
554
+
539
555
  app.get('/dev/config', async (req, res, next) => {
540
556
 
541
557
  // Retrieve auth token
@@ -638,7 +654,7 @@ if (dev) {
638
654
  pmt: config.pmt
639
655
  }).then((_res => _res.data));
640
656
  console.log(`\t${grey(`Response: ${green('"')}${bold(white(payload.message))}`)}${green(
641
- '"')} (elapsed ${payload.ms}ms)`);
657
+ '"')} (elapsed ${payload.ms}ms)`);
642
658
 
643
659
  return payload;
644
660
  };
@@ -655,33 +671,35 @@ if (dev) {
655
671
  }
656
672
  });
657
673
 
658
-
659
- // NOTE: This does not sync with localhost app, that uses its own state
660
- devProgram = async () => {
661
- // Start program where use can test via command console
662
- const {createInterface} = await import('node:readline');
663
- const rl = createInterface({
664
- input: process.stdin,
665
- output: process.stdout
666
- });
667
-
668
- const persona = (config.persona || config.agents)?.[0];
674
+ const devPersona = (config.persona || config.agents)?.[0];
675
+ /**
676
+ * @returns {Omit<WorkflowEvent, 'message'> & {command?: CommandConfiguration}}
677
+ */
678
+ const devCreateState = (persona = (config.persona || config.agents)?.[0]) => {
669
679
  if (!persona) {
670
680
  throw new Error(`A persona is required before processing`);
671
681
  }
672
-
673
- /**
674
- * @returns {Omit<WorkflowEvent, 'message'> & {command?: CommandConfiguration}}
675
- */
676
- const createState = () => ({
677
- messages: [],
682
+ return {
683
+ messages: config.initialContext?.map((_context, index) => ({
684
+ id: `ctx_${index}`,
685
+ time: new Date().toISOString(),
686
+ content: _context,
687
+ role: 'system'
688
+ })),
678
689
  conversation: {
679
690
  $id: 'dev_console_input',
680
691
  $agent: persona.id,
681
692
  $customer: 'temp',
682
693
  environment: 'web'
683
694
  },
684
- context: {},
695
+ context: {
696
+ agent: persona,
697
+ customer: {
698
+ firstName: 'test',
699
+ name: 'test'
700
+ },
701
+ organization: config.organization
702
+ },
685
703
  agent: persona,
686
704
  customer: {
687
705
  firstName: 'test',
@@ -689,37 +707,38 @@ if (dev) {
689
707
  },
690
708
  intent: {current: null, flow: [], initial: null},
691
709
  stagnationCount: 0
692
- });
693
-
694
- /** @type {Omit<WorkflowEvent, 'message'> & {command?: CommandConfiguration}} */
695
- let state = createState();
710
+ };
711
+ };
696
712
 
713
+ /** @type {Omit<WorkflowEvent, 'message'> & {command?: CommandConfiguration}} */
714
+ let devState = devCreateState(devPersona);
697
715
 
698
- async function processCustomerMessage(message, callback) {
699
- const messagePayload = {
700
- id: `user_test_${Date.now()}`,
701
- role: 'customer',
702
- content: message,
703
- time: new Date().toISOString()
704
- };
705
- const logger = new ProgressLogger('Processing...');
706
- if (state.conversation.locked) {
707
- logger.error(`Conversation locked - ${state.conversation.lockedReason ?? 'Unknown reason'}`);
716
+ async function devProcessCustomerMessage(message, callback, roleOverride = null) {
717
+ const messagePayload = {
718
+ id: `user_test_${Date.now()}`,
719
+ role: roleOverride ?? 'customer',
720
+ content: message,
721
+ time: new Date().toISOString()
722
+ };
723
+ const logger = new ProgressLogger('Processing...');
724
+ try {
725
+ if (devState.conversation.locked) {
726
+ logger.error(`Conversation locked - ${devState.conversation.lockedReason ?? 'Unknown reason'}`);
708
727
  return;
709
728
  }
710
729
  const addMessage = (payload) => {
711
- state.messages.push(payload);
730
+ devState.messages.push(payload);
712
731
  switch (payload.role) {
713
732
  case 'system':
714
733
  logger.write(magenta('system: ' + payload.content));
715
734
  break;
716
735
  case 'user':
717
736
  case 'customer':
718
- logger.write(green(`> ${state.agent.firstName ? state.agent.firstName + ': ' : ''}` + payload.content));
737
+ logger.write(green(`> ${devState.agent.firstName ? devState.agent.firstName + ': ' : ''}` + payload.content));
719
738
  break;
720
739
  case 'agent':
721
740
  case 'assistant':
722
- logger.write(blue(`> ${state.customer.name ? state.customer.name + ': ' : ''}` + payload.content));
741
+ logger.write(blue(`> ${devState.customer.name ? devState.customer.name + ': ' : ''}` + payload.content));
723
742
  break;
724
743
  default:
725
744
  logger.write(red(`UNKNOWN (${payload.role}) + ${payload.content}`));
@@ -727,35 +746,35 @@ if (dev) {
727
746
  };
728
747
 
729
748
  const updateMessage = (payload) => {
730
- const index = state.messages.findIndex(m => m.id === payload.id);
749
+ const index = devState.messages.findIndex(m => m.id === payload.id);
731
750
  if (index < 0) {
732
751
  throw new Error(`Cannot find message ${payload.id}`);
733
752
  }
734
- state.messages[index] = payload;
753
+ devState.messages[index] = payload;
735
754
  };
736
755
 
737
756
  const removeMessage = (payload) => {
738
757
  if (typeof payload !== 'string') {
739
758
  throw new Error(`Invalid payload`);
740
759
  }
741
- const index = state.messages.findIndex(m => m.id === payload.id);
760
+ const index = devState.messages.findIndex(m => m.id === payload.id);
742
761
  if (index < 0) {
743
762
  throw new Error(`Cannot find message ${payload.id}`);
744
763
  }
745
- state.messages.splice(index, 1);
764
+ devState.messages.splice(index, 1);
746
765
  };
747
766
 
748
767
  const updateConversation = (payload) => {
749
- Object.assign(state.conversation, payload);
768
+ Object.assign(devState.conversation, payload);
750
769
  };
751
770
 
752
771
  const updateContext = (payload) => {
753
- Object.assign(state.context, payload);
772
+ Object.assign(devState.context, payload);
754
773
  };
755
774
 
756
775
  addMessage(messagePayload);
757
776
  const result = await Spirits.customer({
758
- customer: state.customer,
777
+ customer: devState.customer,
759
778
  config,
760
779
  parser: async (_msg, _lng) => {
761
780
  logger.log(`Parsing...`);
@@ -765,43 +784,28 @@ if (dev) {
765
784
  // Set the global variables for the workflows/commands to run Scout9 Macros
766
785
  globalThis.SCOUT9 = {
767
786
  ...workflowEvent,
768
- $convo: state.conversation.$id ?? state.conversation.id
787
+ $convo: devState.conversation.$id ?? devState.conversation.id
769
788
  };
770
789
 
771
- logger.log(`Gathering ${state.command ? 'Command ' + state.command.entity + ' ' : ''}instructions...`);
772
- if (state.command) {
773
- const commandFilePath = resolve(commandsDir, state.command.path);
790
+ logger.log(`Gathering ${devState.command ? 'Command ' + devState.command.entity + ' ' : ''}instructions...`);
791
+ if (devState.command) {
792
+ const commandFilePath = resolve(commandsDir, devState.command.path);
774
793
  let mod;
775
794
  try {
776
795
  mod = await import(commandFilePath);
777
796
  } catch (e) {
778
- logger.error(`Unable to resolve command ${state.command.entity} at ${commandFilePath}`);
797
+ logger.error(`Unable to resolve command ${devState.command.entity} at ${commandFilePath}`);
779
798
  throw new Error('Failed to gather command instructions');
780
799
  }
781
800
 
782
801
  if (!mod || !mod.default) {
783
- logger.error(`Unable to run command ${state.command.entity} at ${commandFilePath} - must return a default function that returns a WorkflowEvent payload`);
802
+ logger.error(`Unable to run command ${devState.command.entity} at ${commandFilePath} - must return a default function that returns a WorkflowEvent payload`);
784
803
  throw new Error('Failed to run command instructions');
785
804
  }
786
805
 
787
806
  try {
788
807
 
789
808
  return mod.default(workflowEvent)
790
- .then((response) => {
791
- if ('toJSON' in response) {
792
- return response.toJSON();
793
- } else {
794
- return response;
795
- }
796
- })
797
- .then(WorkflowResponseSchema.parse);
798
- } catch (e) {
799
- logger.error(`Failed to run command - ${e.message}`);
800
- throw e;
801
- }
802
-
803
- } else {
804
- return projectApp(workflowEvent)
805
809
  .then((response) => {
806
810
  if ('toJSON' in response) {
807
811
  return response.toJSON();
@@ -810,6 +814,21 @@ if (dev) {
810
814
  }
811
815
  })
812
816
  .then(WorkflowResponseSchema.parse);
817
+ } catch (e) {
818
+ logger.error(`Failed to run command - ${e.message}`);
819
+ throw e;
820
+ }
821
+
822
+ } else {
823
+ return projectApp(workflowEvent)
824
+ .then((response) => {
825
+ if ('toJSON' in response) {
826
+ return response.toJSON();
827
+ } else {
828
+ return response;
829
+ }
830
+ })
831
+ .then(WorkflowResponseSchema.parse);
813
832
  }
814
833
  },
815
834
  generator: async (request) => {
@@ -819,10 +838,10 @@ if (dev) {
819
838
  },
820
839
  idGenerator: (prefix) => `${prefix}_test_${Date.now()}`,
821
840
  progress: (
822
- message,
823
- level,
824
- type,
825
- payload
841
+ message,
842
+ level,
843
+ type,
844
+ payload
826
845
  ) => {
827
846
  callback(message, level);
828
847
  if (type) {
@@ -850,16 +869,16 @@ if (dev) {
850
869
  }
851
870
  },
852
871
  message: messagePayload,
853
- context: state.context,
854
- messages: state.messages,
855
- conversation: state.conversation
872
+ context: devState.context,
873
+ messages: devState.messages,
874
+ conversation: devState.conversation
856
875
  });
857
876
 
858
877
  // If a forward happens (due to a lock or other reason)
859
878
  if (!!result.conversation.forward) {
860
- if (!state.conversation.locked) {
879
+ if (!devState.conversation.locked) {
861
880
  // Only forward if conversation is not already locked
862
- await devForward(state.conversation.$id);
881
+ await devForward(devState.conversation.$id);
863
882
  }
864
883
  updateConversation({locked: true});
865
884
  logger.error(`Conversation locked`);
@@ -900,68 +919,82 @@ if (dev) {
900
919
  }
901
920
  }
902
921
 
922
+ } catch (e) {
923
+ handleError(e);
924
+ } finally {
903
925
  logger.done();
904
926
  }
927
+ }
928
+
929
+ /**
930
+ * @param {CommandConfiguration} command
931
+ * @param {string} message
932
+ * @param callback
933
+ * @returns {Promise<void>}
934
+ */
935
+ async function devProcessCommand(command, message, callback) {
936
+ console.log(magenta(`> command <${command.entity}>`));
937
+ devState = devCreateState();
938
+ devState.command = command;
939
+ return devProcessCustomerMessage(`Assist me in this ${command.entity} flow`, callback);
940
+ }
905
941
 
906
- /**
907
- * @param {CommandConfiguration} command
908
- * @param {string} message
909
- * @param callback
910
- * @returns {Promise<void>}
911
- */
912
- async function processCommand(command, message, callback) {
913
- console.log(magenta(`> command <${command.entity}>`));
914
- state = createState();
915
- state.command = command;
916
- return processCustomerMessage(`Assist me in this ${command.entity} flow`, callback);
942
+ async function devProgramProcessInput(message, callback, roleOverride = null) {
943
+ // Check if internal command
944
+ switch (message.toLowerCase().trim()) {
945
+ case 'context':
946
+ console.log(white('> Current Conversation Context:'));
947
+ console.log(grey(JSON.stringify(devState.context)));
948
+ return;
949
+ case 'conversation':
950
+ case 'convo':
951
+ console.log(white('> Current Conversation State:'));
952
+ console.log(grey(JSON.stringify(devState.conversation)));
953
+ return;
954
+ case 'messages':
955
+ devState.messages.forEach((msg) => {
956
+ switch (msg.role) {
957
+ case 'system':
958
+ console.log(magenta('\t - ' + msg.content));
959
+ break;
960
+ case 'user':
961
+ case 'customer':
962
+ console.log(green('> ' + msg.content));
963
+ break;
964
+ case 'agent':
965
+ case 'assistant':
966
+ console.log(blue('> ' + msg.content));
967
+ break;
968
+ default:
969
+ console.log(red(`UNKNOWN (${msg.role}) + ${msg.content}`));
970
+ }
971
+ });
972
+ return;
917
973
  }
918
974
 
919
- async function devProgramProcessInput(message, callback) {
920
- // Check if internal command
921
- switch (message.toLowerCase().trim()) {
922
- case 'context':
923
- console.log(white('> Current Conversation Context:'));
924
- console.log(grey(JSON.stringify(state.context)));
925
- return;
926
- case 'conversation':
927
- case 'convo':
928
- console.log(white('> Current Conversation State:'));
929
- console.log(grey(JSON.stringify(state.conversation)));
930
- return;
931
- case 'messages':
932
- state.messages.forEach((msg) => {
933
- switch (msg.role) {
934
- case 'system':
935
- console.log(magenta('\t - ' + msg.content));
936
- break;
937
- case 'user':
938
- case 'customer':
939
- console.log(green('> ' + msg.content));
940
- break;
941
- case 'agent':
942
- case 'assistant':
943
- console.log(blue('> ' + msg.content));
944
- break;
945
- default:
946
- console.log(red(`UNKNOWN (${msg.role}) + ${msg.content}`));
947
- }
948
- });
949
- return;
950
- }
975
+ // Check if it's a command
976
+ const target = message.toLowerCase().trim();
977
+ const command = config?.commands?.find(command => {
978
+ return command.entity === target;
979
+ });
980
+ // Run the command
981
+ if (command) {
982
+ return devProcessCommand(command, message, callback);
983
+ }
951
984
 
952
- // Check if it's a command
953
- const target = message.toLowerCase().trim();
954
- const command = config.commands.find(command => {
955
- return command.entity === target;
956
- });
957
- // Run the command
958
- if (command) {
959
- return processCommand(command, message, callback);
960
- }
985
+ // Otherwise default to processing customer message
986
+ return devProcessCustomerMessage(message, callback, roleOverride);
987
+ }
988
+
989
+ // This is used for handling cli dev
990
+ devReadlineProgram = async () => {
991
+ // Start program where use can test via command console
992
+ const {createInterface} = await import('node:readline');
993
+ const rl = createInterface({
994
+ input: process.stdin,
995
+ output: process.stdout
996
+ });
961
997
 
962
- // Otherwise default to processing customer message
963
- return processCustomerMessage(message, callback);
964
- }
965
998
 
966
999
  // Function to ask for input, perform the task, and then ask again
967
1000
  function promptUser() {
@@ -980,16 +1013,16 @@ if (dev) {
980
1013
  }
981
1014
 
982
1015
 
983
-
984
1016
  console.log(grey(`\nThe following ${bold('commands')} are available...`));
985
- [['context', 'logs the state context inserted into the conversation'], ['conversation', 'logs conversation details'], ['messages', 'logs all message history']].forEach(([command, description]) => {
986
- console.log(`\t - ${magenta(command)} ${grey(description)}`);
987
- });
1017
+ [['context', 'logs the state context inserted into the conversation'], ['conversation', 'logs conversation details'], ['messages', 'logs all message history']].forEach(
1018
+ ([command, description]) => {
1019
+ console.log(`\t - ${magenta(command)} ${grey(description)}`);
1020
+ });
988
1021
 
989
- if (config.commands.length) {
1022
+ if (config?.commands?.length) {
990
1023
  console.log(grey(`\nThe following ${bold('custom commands')} are available...`));
991
1024
  }
992
- config.commands.forEach((command) => {
1025
+ config?.commands?.forEach((command) => {
993
1026
  console.log(magenta(`\t - ${command.entity}`));
994
1027
  });
995
1028
 
@@ -1007,11 +1040,77 @@ if (dev) {
1007
1040
 
1008
1041
  };
1009
1042
 
1043
+ // API routes for handling and receiving the message state within websocket
1044
+ const {WebSocketServer, WebSocket} = await import('ws');
1045
+ devServer = createServer();
1046
+ devServer.on('request', app.handler);
1047
+ const wss = new WebSocketServer({server: devServer});
1048
+ wss.on('connection', (ws) => {
1049
+
1050
+ const sendState = () => {
1051
+ const payload = JSON.stringify({state: devState});
1052
+ ws.send(payload);
1053
+ wss.clients.forEach((client) => {
1054
+ if (client.readyState === WebSocket.OPEN) {
1055
+ client.send(payload);
1056
+ }
1057
+ });
1058
+ };
1059
+
1060
+ console.log(grey('WebSocket client connected'));
1061
+ sendState();
1062
+
1063
+ // Handle incoming WebSocket messages
1064
+ ws.on('message', async (msg) => {
1065
+ let parsedMessage;
1066
+ if (Buffer.isBuffer(msg)) {
1067
+ parsedMessage = JSON.parse(msg.toString());
1068
+ } else if (typeof msg === 'string') {
1069
+ parsedMessage = JSON.parse(msg);
1070
+ } else {
1071
+ console.error('Unexpected message type:', typeof msg);
1072
+ return;
1073
+ }
1074
+ const message = MessageSchema.parse(parsedMessage);
1075
+ console.log(`${grey('> ')} "${cyan(message.content)}"`);
1076
+ try {
1077
+ await devProgramProcessInput(message.content, () => sendState(), message.role);
1078
+ sendState();
1079
+ ws.send(JSON.stringify({message: {id: message.id}}));
1080
+ } catch (e) {
1081
+ ws.send(JSON.stringify(handleError(e)));
1082
+ }
1083
+ });
1084
+
1085
+ ws.on('close', () => {
1086
+ console.log(yellow('WebSocket client disconnected'));
1087
+ });
1088
+
1089
+
1090
+ // CRUD REST calls to manipulate state
1091
+ app.get('/dev', async (req, res, next) => {
1092
+ res.writeHead(200, {'Content-Type': 'application/json'});
1093
+ res.end(JSON.stringify(devState));
1094
+ });
1095
+ app.put('/dev', async (req, res, next) => {
1096
+ Object.assign(devState, req.body ?? {});
1097
+ sendState();
1098
+ res.writeHead(200, {'Content-Type': 'application/json'});
1099
+ res.end(JSON.stringify(devState));
1100
+ });
1101
+ app.post('/dev/reset', async (req, res, next) => {
1102
+ devState = devCreateState(req?.body?.persona);
1103
+ sendState();
1104
+ res.writeHead(200, {'Content-Type': 'application/json'});
1105
+ res.end(JSON.stringify(devState));
1106
+ });
1107
+
1108
+ });
1010
1109
 
1011
1110
  }
1012
1111
 
1013
1112
 
1014
- app.listen(process.env.PORT || 8080, async (err) => {
1113
+ (dev ? devServer : app).listen(process.env.PORT || 8080, async (err) => {
1015
1114
  if (err) throw err;
1016
1115
 
1017
1116
  const art_scout9 = `
@@ -1046,7 +1145,7 @@ app.listen(process.env.PORT || 8080, async (err) => {
1046
1145
  const fullUrl = `${protocol}://${host}:${port}`;
1047
1146
  if (dev) {
1048
1147
  console.log(bold(green(art_scout9)));
1049
- console.log(bold(cyan(art_pmt)));
1148
+ // console.log(bold(cyan(art_pmt)));
1050
1149
  console.log(`${grey(`${cyan('>')} Running ${bold(white('Scout9'))}`)} ${grey('dev environment on')} ${fullUrl}`);
1051
1150
  } else {
1052
1151
  console.log(`Running Scout9 app on ${fullUrl}`);
@@ -1058,16 +1157,16 @@ app.listen(process.env.PORT || 8080, async (err) => {
1058
1157
 
1059
1158
  if (dev && !process.env.SCOUT9_API_KEY) {
1060
1159
  console.log(red(
1061
- 'Missing SCOUT9_API_KEY environment variable, your PMT application may not work without it.'));
1160
+ 'Missing SCOUT9_API_KEY environment variable, your PMT application may not work without it.'));
1062
1161
  }
1063
1162
 
1064
1163
  if (process.env.SCOUT9_API_KEY === '<insert-scout9-api-key>') {
1065
1164
  console.log(`${red('SCOUT9_API_KEY has not been set in your .env file.')} ${grey(
1066
- 'You can find your API key in the Scout9 dashboard.')} ${bold(cyan('https://scout9.com'))}`);
1165
+ 'You can find your API key in the Scout9 dashboard.')} ${bold(cyan('https://scout9.com'))}`);
1067
1166
  }
1068
1167
 
1069
1168
  if (dev) {
1070
- devProgram();
1169
+ devReadlineProgram();
1071
1170
  }
1072
1171
 
1073
1172
  });