@scout9/app 1.0.0-alpha.0.0.1

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/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@scout9/app",
3
+ "version": "1.0.0-alpha.0.0.1",
4
+ "description": "Build and deploy your Scout9 app for SMS auto reply",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git://github.com/Scout9Official/scout9-nodejs.git"
11
+ },
12
+ "scripts": {
13
+ "postinstall": "node postinstall.js",
14
+ "types": "npx -p typescript tsc src/**/*.ts --declaration --allowJs --emitDeclarationOnly --esModuleInterop --outDir dist/types"
15
+ },
16
+ "type": "module",
17
+ "bin": {
18
+ "scout9-app": "./scout9-app.js"
19
+ },
20
+ "files": [
21
+ "src",
22
+ "!src/**/*.spec.js",
23
+ "!src/core/**/fixtures",
24
+ "!src/core/**/test",
25
+ "types",
26
+ "scout9-app.js",
27
+ "postinstall.js"
28
+ ],
29
+ "exports": {
30
+ "./package.json": "./package.json",
31
+ ".": {
32
+ "types": "./types/index.d.ts",
33
+ "import": "./src/exports/index.js"
34
+ }
35
+ },
36
+ "keywords": [],
37
+ "author": "",
38
+ "license": "ISC",
39
+ "devDependencies": {
40
+ "@types/archiver": "^6.0.2",
41
+ "@types/jest": "^29.5.11",
42
+ "ts-jest": "^29.1.1",
43
+ "typescript": "^5.3.3"
44
+ },
45
+ "dependencies": {
46
+ "archiver": "^6.0.1",
47
+ "decompress": "^4.2.1",
48
+ "dotenv": "^16.3.1",
49
+ "glob": "^10.3.10",
50
+ "kleur": "^4.1.5",
51
+ "moment": "^2.29.4",
52
+ "node-fetch": "^3.3.2",
53
+ "sade": "^1.8.1",
54
+ "zod": "^3.22.4"
55
+ },
56
+ "engines": {
57
+ "node": ">=18.13"
58
+ }
59
+ }
package/postinstall.js ADDED
@@ -0,0 +1,2 @@
1
+ console.log('@scout9/app installed');
2
+ // @TODO run any synchronous post-install processes here
package/scout9-app.js ADDED
@@ -0,0 +1 @@
1
+ import './src/cli';
package/src/cli.js ADDED
@@ -0,0 +1,35 @@
1
+ import fs from 'node:fs';
2
+ import sade from 'sade';
3
+ import path from 'node:path';
4
+ import { Scout9Platform } from './platform.js';
5
+
6
+ // const pkg = JSON.parse(fs.readFileSync(new URL('../package.json'), 'utf-8'));
7
+ // const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
8
+ const prog = sade('scout9-auto-reply').version('0.0.1');
9
+
10
+ prog
11
+ .command('build')
12
+ .describe('Builds your scout9 auto reply app')
13
+ .option('--mode', 'Specify a mode for loading environment variables', 'development')
14
+ .action(async ({ mode }) => {
15
+ if (!fs.existsSync('.env')) {
16
+ console.warn(`Missing ${path.resolve('.env')} — skipping`);
17
+ return;
18
+ }
19
+ return Scout9Platform.build({cwd: process.cwd(), mode});
20
+ });
21
+
22
+
23
+ prog
24
+ .command('deploy')
25
+ .describe('Deploy your scout9 auto reply app')
26
+ .option('--mode', 'Specify a mode for loading environment variables', 'development')
27
+ .action(async ({ mode }) => {
28
+ if (!fs.existsSync('.env')) {
29
+ console.warn(`Missing ${path.resolve('.env')} — skipping`);
30
+ return;
31
+ }
32
+ return Scout9Platform.build({cwd: process.cwd(), mode});
33
+ });
34
+
35
+ prog.parse(process.argv, { unknown: (arg) => `Unknown option: ${arg}` });
@@ -0,0 +1,5 @@
1
+
2
+ export default async function loadAgentConfig({cwd = process.cwd(), folder = 'src'} = {}) {
3
+ console.log('@TODO implement loadAgentConfig');
4
+ return [];
5
+ }
@@ -0,0 +1,110 @@
1
+ import { globSync } from 'glob';
2
+ import path from 'node:path';
3
+ import {
4
+ entitiesRootProjectConfigurationSchema,
5
+ entityApiConfigurationSchema,
6
+ entityConfigurationSchema,
7
+ entityRootProjectConfigurationSchema
8
+ } from '../../runtime/index.js';
9
+ import { checkVariableType, requireOptionalProjectFile, requireProjectFile } from '../../utils/index.js';
10
+
11
+ async function loadEntityApiConfig(cwd, filePath) {
12
+ const dir = path.dirname(filePath);
13
+ const extension = path.extname(filePath);
14
+ const apiFilePath = path.join(dir, `api${extension}`);
15
+ const root = cwd.split('/').pop();
16
+ const x = apiFilePath.replace(cwd, '').split('/').slice(1).join('/');
17
+
18
+ const mod = await requireOptionalProjectFile(x);
19
+ if (mod) {
20
+ const config = {};
21
+ const methods = ['GET', 'UPDATE', 'QUERY', 'PUT', 'PATCH', 'DELETE'];
22
+ for (const key in mod) {
23
+ if (methods.includes(key)) {
24
+ config[key] = true;
25
+ }
26
+ }
27
+ entityApiConfigurationSchema.parse(config);
28
+ return config;
29
+ } else {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ export default async function loadEntitiesConfig(
35
+ {cwd = process.cwd(), folder = 'src'} = {}
36
+ ) {
37
+ console.log('loadEntitiesConfig', {cwd, folder});
38
+ const config = [];
39
+ const paths = globSync(path.resolve(cwd, `${folder}/entities/**/{index,config,api}.{ts,js}`));
40
+ const data = [];
41
+ for (const path of paths) {
42
+ const segments = path.split('/');
43
+ const srcIndex = segments.findIndex((segment, index) => segment === folder && segments[index + 1] === 'entities');
44
+ const parents = segments.slice(srcIndex + 2, -1).reverse(); // +2 to skip "${folder}" and "entities"
45
+ if (parents.length > 0) {
46
+ const api = await loadEntityApiConfig(cwd, path);
47
+ data.push({path, parents, api});
48
+ } else {
49
+ console.log(`WARNING: "${path}" Is not a valid entity path, must be contained in a named folder under entities/`);
50
+ }
51
+ }
52
+
53
+ const specialEntities = ['agents'];
54
+
55
+ for (const {path, parents, api} of data) {
56
+ let entityConfig = {};
57
+ const fileName = path.split('/')?.pop()?.split('.')?.[0];
58
+ if (!fileName) throw new Error(`Invalid file name "${path}"`);
59
+ const isSpecial = specialEntities.some(se => parents.includes(se));
60
+ if ((fileName === 'index' || fileName === 'config') && !isSpecial) {
61
+ const entityConfigHandler = await requireProjectFile(path).then(mod => mod.default);
62
+ // Check if entityConfig is a function or object
63
+ const entityType = checkVariableType(entityConfigHandler);
64
+ switch (entityType) {
65
+ case 'async function':
66
+ case 'function':
67
+ entityConfig = await entityConfigHandler();
68
+ break;
69
+ case 'object':
70
+ entityConfig = entityConfigHandler;
71
+ break;
72
+ default:
73
+ throw new Error(`Invalid entity type (${entityType}) returned at "${path}"`);
74
+ }
75
+
76
+ // Validate entity configuration
77
+ const result = entityConfigurationSchema.safeParse(entityConfig, {path: ['entities', config.length]});
78
+ if (!result.success) {
79
+ throw result.error;
80
+ }
81
+ } else if (isSpecial && (fileName === 'index' || fileName === 'config')) {
82
+ // If this is a special entity file, then ignore as we will capture it another method
83
+ continue;
84
+ }
85
+
86
+ // Validate project configuration
87
+ const entityProjectConfig = {
88
+ ...entityConfig,
89
+ entity: parents[0],
90
+ entities: parents,
91
+ api
92
+ };
93
+ entityRootProjectConfigurationSchema.parse(entityProjectConfig);
94
+
95
+ config.push(entityProjectConfig);
96
+ }
97
+
98
+ if (!config.some(c => c.entity === 'customers')) {
99
+ throw new Error(`Missing required entity: "entities/customers"`);
100
+ }
101
+ if (!config.some(c => c.entity === '[customer]')) {
102
+ throw new Error(`Missing required entity: "entities/customers/[customer]"`);
103
+ }
104
+
105
+ // Validate the config
106
+ entitiesRootProjectConfigurationSchema.parse(config);
107
+
108
+ return config;
109
+ }
110
+
@@ -0,0 +1,31 @@
1
+ import { config } from 'dotenv';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import loadAgentConfig from './agents.js';
5
+ import loadEntitiesConfig from './entities.js';
6
+ import loadProjectConfig from './project.js';
7
+ import loadWorkflowsConfig from './workflow.js';
8
+
9
+
10
+ export function loadEnvConfig({ cwd = process.cwd()} = {}) {
11
+ const configFilePath = path.resolve(cwd, './.env');
12
+ config({path: configFilePath});
13
+ if (!process.env.SCOUT9_API_KEY) {
14
+ const exists = fs.existsSync(configFilePath);
15
+ if (!exists) {
16
+ throw new Error(`Missing .env file with SCOUT9_API_KEY`);
17
+ } else {
18
+ throw new Error('Missing SCOUT9_API_KEY within .env file');
19
+ }
20
+ }
21
+ }
22
+ export async function loadConfig({ cwd = process.cwd(), folder = 'src'} = {}) {
23
+ // Load globals
24
+ loadEnvConfig({cwd});
25
+ return {
26
+ ...await loadProjectConfig({cwd, folder}),
27
+ entities: await loadEntitiesConfig({cwd, folder}),
28
+ agents: await loadAgentConfig({cwd, folder}),
29
+ workflows: await loadWorkflowsConfig({cwd, folder})
30
+ }
31
+ }
@@ -0,0 +1,17 @@
1
+ import path from 'node:path';
2
+ import { requireProjectFile } from '../../utils/index.js';
3
+ import { globSync } from 'glob';
4
+
5
+ export default async function loadProjectConfig({cwd = process.cwd(), folder = 'src'} = {}) {
6
+ console.log('loadProjectConfig', {cwd, folder});
7
+ const paths = globSync(path.resolve(cwd, `${folder}/index.{ts,js}`));
8
+ if (paths.length === 0) {
9
+ throw new Error(`Missing main project entry file ${folder}/index.{js|ts}`);
10
+ } else if (paths.length > 1) {
11
+ throw new Error(`Multiple main project entry files found ${folder}/index.{js|ts}`);
12
+ }
13
+ const [filePath] = paths;
14
+ const project = await requireProjectFile(filePath).then(mod => mod.default);
15
+ // @TODO validation type check with zod
16
+ return project;
17
+ }
@@ -0,0 +1,46 @@
1
+ import { globSync } from 'glob';
2
+ import path from 'node:path';
3
+ import {
4
+ WorkflowConfigurationSchema,
5
+ WorkflowsConfigurationSchema
6
+ } from '../../runtime/index.js';
7
+
8
+
9
+ export default async function loadWorkflowsConfig(
10
+ {
11
+ cwd = process.cwd(),
12
+ folder = 'src'
13
+ } = {}
14
+ ) {
15
+ console.log('loadWorkflowsConfig', {cwd, folder});
16
+ const config = globSync(path.resolve(cwd, `${folder}/workflows/**/workflow.{ts,js}`))
17
+ .map((path) => {
18
+ const segments = path.split('/');
19
+ const srcIndex = segments.findIndex((segment, index) => segment === folder && segments[index + 1] === 'workflows');
20
+ const parents = segments.slice(srcIndex + 2, -1).reverse(); // +2 to skip "${folder}" and "workflows"
21
+ return {path, parents};
22
+ })
23
+ .filter(path => {
24
+ if (path.parents.length > 0) {
25
+ return true;
26
+ } else {
27
+ console.log(`WARNING: "${path}" Is not a valid entity path, must be contained in a named folder under workflows/`);
28
+ }
29
+ })
30
+ .map(({path, parents}) => {
31
+
32
+ // Validate project configuration
33
+ const workflowConfig = {
34
+ entity: parents[0],
35
+ entities: parents,
36
+ }
37
+ WorkflowConfigurationSchema.parse(workflowConfig);
38
+
39
+ return workflowConfig;
40
+ });
41
+
42
+ // Validate the config
43
+ WorkflowsConfigurationSchema.parse(config);
44
+
45
+ return config;
46
+ }
@@ -0,0 +1,167 @@
1
+ import archiver from 'archiver';
2
+ import { globSync } from 'glob';
3
+ import { exec } from 'node:child_process';
4
+ import fss from 'node:fs';
5
+ import fs from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import fetch, {FormData} from 'node-fetch';
8
+ import { runInVM } from '../runtime/index.js';
9
+ import { checkVariableType, requireProjectFile } from '../utils/index.js';
10
+
11
+
12
+
13
+ async function runNpmRunBuild({cwd = process.cwd()} = {}) {
14
+ return new Promise((resolve, reject) => {
15
+ exec('npm run build', {cwd}, (error, stdout, stderr) => {
16
+ if (error) {
17
+ console.error(`Build failed: ${error.message}`);
18
+ return reject(error);
19
+ }
20
+ console.log('Build successful');
21
+ return resolve(undefined);
22
+ });
23
+ });
24
+ }
25
+
26
+
27
+ function zipDirectory(source, out) {
28
+ const archive = archiver('zip', {zlib: {level: 9}});
29
+ const stream = fss.createWriteStream(out);
30
+
31
+ return new Promise((resolve, reject) => {
32
+ archive
33
+ .directory(source, false)
34
+ .on('error', err => reject(err))
35
+ .pipe(stream);
36
+
37
+ stream.on('close', () => resolve(undefined));
38
+ archive.finalize();
39
+ });
40
+ }
41
+
42
+ async function deployZipDirectory(zipFilePath) {
43
+
44
+
45
+ const form = new FormData();
46
+ const blob = new Blob([await fs.readFile(zipFilePath)], {type: 'application/zip'});
47
+ form.set("file", blob, path.basename(zipFilePath), {contentType: 'application/zip'});
48
+ form.set("foo", "bar");
49
+
50
+ // @TODO append signature secret header
51
+ const response = await fetch(`https://pocket-guide.vercel.app/api/b/platform/upload`, {
52
+ method: 'POST',
53
+ body: form,
54
+ headers: {
55
+ 'Authorization': process.env.SCOUT9_API_KEY || ''
56
+ }
57
+ });
58
+ if (!response.ok) {
59
+ throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
60
+ }
61
+
62
+ console.log('File sent successfully');
63
+ return true;
64
+ }
65
+
66
+ async function downloadAndUnpackZip(outputDir) {
67
+ const downloadLocalResponse = await fetch(
68
+ `https://pocket-guide.vercel.app/api/b/platform/download`,
69
+ {
70
+ headers: {
71
+ 'Authorization': process.env.SCOUT9_API_KEY || ''
72
+ }
73
+ }
74
+ );
75
+ if (!downloadLocalResponse.ok) {
76
+ throw new Error(`Error downloading project file ${downloadLocalResponse.statusText}`);
77
+ }
78
+
79
+ try {
80
+ const buffer = await downloadLocalResponse.arrayBuffer();
81
+ const decompress = require('decompress');
82
+ await decompress(buffer, outputDir + '/build');
83
+
84
+ console.log('Files unpacked successfully at ' + outputDir + '/build');
85
+
86
+ return outputDir + '/build';
87
+ } catch (error) {
88
+ console.error('Error unpacking file:', error);
89
+ throw error;
90
+ }
91
+ }
92
+
93
+ export async function getApp({cwd = process.cwd(), folder = 'src', ignoreAppRequire = false} = {}) {
94
+ const indexTsPath = path.resolve(cwd, folder, 'index.ts');
95
+ const indexJsPath = path.join(cwd, folder, 'index.js');
96
+ let exe = '';
97
+ if (fss.existsSync(indexTsPath)) {
98
+ exe = path.extname(indexTsPath);
99
+ } else if (fss.existsSync(indexJsPath)) {
100
+ exe = path.extname(indexJsPath);
101
+ } else {
102
+ throw new Error(`Missing main project entry file ${folder}/index.{js|ts}`);
103
+ }
104
+ const filePath = path.resolve(cwd, folder, `app${exe}`);
105
+ let app;
106
+ if (!ignoreAppRequire) {
107
+ app = await requireProjectFile(filePath).then(mod => mod.default);
108
+ const type = checkVariableType(app);
109
+ if (!(type === 'function' || type === 'async function')) {
110
+ throw new Error(`App must return a default function, received "${type}"`);
111
+ }
112
+ }
113
+ return {app, exe, filePath, fileName: `app${exe}`}
114
+ }
115
+
116
+
117
+ /**
118
+ * Runs a given project container from scout9 to given environment
119
+ */
120
+ export async function run(event, {cwd = process.cwd()} = {}) {
121
+
122
+ // @TODO use scout9/admin
123
+ await downloadAndUnpackZip(path.resolve(cwd, 'tmp'));
124
+
125
+ const {filePath, fileName} = await getApp({cwd, folder: 'tmp/build', ignoreAppRequire: true});
126
+
127
+ return runInVM(event, {folder: path.resolve(cwd, 'tmp/build'), filePath, fileName});
128
+ }
129
+
130
+ /**
131
+ * Builds a local project
132
+ */
133
+ export async function build({cwd = process.cwd()} = {}, config) {
134
+ // 1. Lint: Run validation checks
135
+
136
+ // Check if app looks good
137
+ await getApp({cwd, folder: 'src'});
138
+
139
+ // Check if workflows look good
140
+ console.log('@TODO check if workflows are properly written');
141
+
142
+ // 2. Build code in user's project
143
+ await runNpmRunBuild({cwd});
144
+
145
+
146
+ // 3. Remove unnecessary files
147
+ const files = globSync(path.resolve(cwd, 'build/**/*(*.test.*|*.spec.*)'))
148
+ for (const file of files) {
149
+ await fs.unlink(file);
150
+ }
151
+
152
+ // 3. Run tests
153
+ // console.log('@TODO run tests');
154
+ }
155
+
156
+ /**
157
+ * Deploys a local project to scout9
158
+ */
159
+ export async function deploy({cwd = process.cwd()}) {
160
+ const zipFilePath = path.join(cwd, 'build.zip');
161
+ await zipDirectory(path.resolve(cwd, 'build'), zipFilePath);
162
+
163
+ console.log('Project zipped successfully.');
164
+
165
+ const response = await deployZipDirectory(zipFilePath);
166
+ console.log('Response from Firebase Function:', response);
167
+ }
package/src/exports.js ADDED
@@ -0,0 +1,49 @@
1
+ import { Scout9Platform } from './platform.js';
2
+ import { EventResponse } from './runtime/index.js';
3
+
4
+ export { EventResponse } from './runtime/index.js';
5
+ // export * from './types';
6
+ export * from './testing-tools/index.js';
7
+
8
+ export async function run(
9
+ event,
10
+ {cwd = process.cwd()} = {},
11
+ ) {
12
+ return Scout9Platform.run(event, {cwd})
13
+ }
14
+ export async function sendEvent(
15
+ event,
16
+ {cwd = process.cwd()} = {},
17
+ ) {
18
+ return Scout9Platform.run(event, {cwd})
19
+ }
20
+
21
+ /**
22
+ * @param data {T}
23
+ * @param init {ResponseInit | undefined}
24
+ * @returns {EventResponse<T>}
25
+ */
26
+ export function json(data, init) {
27
+ // TODO deprecate this in favour of `Response.json` when it's
28
+ // more widely supported
29
+ const body = JSON.stringify(data);
30
+
31
+ // we can't just do `text(JSON.stringify(data), init)` because
32
+ // it will set a default `content-type` header. duplicated code
33
+ // means less duplicated work
34
+ const headers = new Headers(init?.headers);
35
+ if (!headers.has('content-length')) {
36
+ headers.set('content-length', encoder.encode(body).byteLength.toString());
37
+ }
38
+
39
+ if (!headers.has('content-type')) {
40
+ headers.set('content-type', 'application/json');
41
+ }
42
+
43
+ return new EventResponse(data, {
44
+ ...init,
45
+ headers
46
+ });
47
+ }
48
+
49
+ const encoder = new TextEncoder();
package/src/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from './exports.js';
@@ -0,0 +1,71 @@
1
+ import colors from 'kleur';
2
+ import { z } from 'zod';
3
+ import { build as _build, deploy as _deploy, run as _run } from './core/index.js';
4
+ import { loadConfig, loadEnvConfig } from './core/config/index.js';
5
+ import { coalesceToError } from './utils/index.js';
6
+
7
+ export const Scout9Platform = {
8
+
9
+ /**
10
+ * Builds & Deploys the project
11
+ */
12
+ deploy: async function ({cwd = process.cwd()} = {}) {
13
+ try {
14
+ const config = await loadConfig({cwd, folder: 'src'});
15
+ await _build({cwd}, config);
16
+ await _deploy({cwd});
17
+ return config;
18
+ } catch (e) {
19
+ this.handleError(e);
20
+ }
21
+ },
22
+
23
+ /**
24
+ * Builds the project
25
+ */
26
+ build: async function({cwd = process.cwd(), mode = 'development'} = {}) {
27
+ try {
28
+ const config = await loadConfig({cwd, folder: 'src'});
29
+ await _build({cwd}, config);
30
+ return config;
31
+ } catch (e) {
32
+ this.handleError(e);
33
+ }
34
+ },
35
+
36
+ /**
37
+ * Runs the project in a container
38
+ */
39
+ run: async function (
40
+ event,
41
+ {cwd = process.cwd(), mode = 'remote'}
42
+ ) {
43
+ if (mode !== 'remote') {
44
+ throw new Error(`Unimplemented mode "${mode}"`);
45
+ }
46
+ try {
47
+ loadEnvConfig({cwd});
48
+ return _run(event, {cwd});
49
+ } catch (e) {
50
+ this.handleError(e);
51
+ throw e;
52
+ }
53
+ },
54
+ handleError: function (e) {
55
+ const error = coalesceToError(e);
56
+
57
+ if (error.name === 'SyntaxError') throw error;
58
+
59
+ console.error(colors.bold().red(`> ${error.message}`));
60
+ if (error instanceof z.ZodError) {
61
+ console.error(error.issues.map(i => colors.red(`${colors.bold(`\tZod Error (${i.code}): `)}"${i.message}" ${JSON.stringify(i.path)}`)).join('\n'));
62
+ console.error(colors.gray(JSON.stringify(error.format(), null, 2)));
63
+ }
64
+
65
+ if (error.stack) {
66
+ console.error(colors.gray(error.stack.split('\n').slice(0).join('\n')));
67
+ }
68
+
69
+ process.exit(1);
70
+ }
71
+ }
@@ -0,0 +1,44 @@
1
+ import { z } from 'zod';
2
+ import { zId } from './utils.js';
3
+ import { MessageSchema } from './message.js';
4
+
5
+ export const customerValueSchema = z.union([z.boolean(), z.number(), z.string()]);
6
+
7
+ export const customerSchema = z.object({
8
+ firstName: z.string().optional(),
9
+ lastName: z.string().optional(),
10
+ name: z.string(),
11
+ email: z.string().nullable().optional(),
12
+ phone: z.string().nullable().optional(),
13
+ img: z.string().nullable().optional(),
14
+ neighborhood: z.string().nullable().optional(),
15
+ city: z.string().nullable().optional(),
16
+ country: z.string().nullable().optional(),
17
+ line1: z.string().nullable().optional(),
18
+ line2: z.string().nullable().optional(),
19
+ postal_code: z.string().nullable().optional(),
20
+ state: z.string().nullable().optional(),
21
+ town: z.string().nullable().optional(),
22
+ joined: z.string().nullable().optional(),
23
+ stripe: z.string().nullable().optional(),
24
+ stripeDev: z.string().nullable().optional()
25
+ }).catchall(customerValueSchema);
26
+
27
+ export const agentConfigurationSchema = z.object({
28
+ id: zId('Agent ID', z.string({description: 'Unique ID for agent'})),
29
+ firstName: z.string({description: 'Agent first name'}).optional(),
30
+ lastName: z.string({description: 'Agent last name'}).optional(),
31
+ inactive: z.boolean({description: 'Agent is inactive'}).optional(),
32
+ programmablePhoneNumber: z.string({description: 'Programmable phone number'}).optional(),
33
+ programmablePhoneNumberSid: z.string({description: 'Programmable phone number SID'}).optional(),
34
+ programmableEmail: z.string({description: 'Email address from Scout9 gmail subdomain'}).optional(),
35
+ forwardEmail: z.string({description: 'Email address to forward to'}).optional(),
36
+ forwardPhone: z.string({description: 'Phone number to forward to'}).optional(),
37
+ title: z.string({description: 'Agent title '}).optional().default('Agent'),
38
+ context: z.string({description: 'Context of the agent'}).optional().default('You represent the agent when they are away'),
39
+ includedLocations: z.array(z.string({description: 'Locations the agent is included in'})).optional(),
40
+ excludedLocations: z.array(z.string({description: 'Locations the agent is excluded from'})).optional(),
41
+ model: z.enum(['Scout9', 'bard', 'openai']).optional().default('openai'),
42
+ transcripts: z.array(z.array(MessageSchema)).optional(),
43
+ audioRef: z.array(z.any()).optional()
44
+ });