diff --git a/exec/load/frame.js b/exec/load/frame.js
new file mode 100644
index 0000000000000000000000000000000000000000..3ea6a616a11a9b4daf807cf2a3b4b8668cb87a30
--- /dev/null
+++ b/exec/load/frame.js
@@ -0,0 +1,793 @@
+/* $Id$ */
+/**
+ 	Javascript Frame Library 					
+ 	for Synchronet v3.15a+ 
+ 	by Matt Johnson (2011)	
+
+ DESCRIPTION:
+
+ 	this library is meant to be used in conjunction with other libraries that
+ 	store display data in a Frame() object or objects
+ 	this allows for "windows" that can be hidden and closed without 
+ 	destroying the data behind them.
+
+ 	the object itself takes the following parameters:
+
+ 		x: 		the coordinate representing the top left corner of the frame (horiz)
+ 		y: 		the coordinate representing the top left corner of the frame (vert)
+ 		width: 	the horizontal width of the frame 
+ 		height: 	the vertical height of the frame
+ 		attr:		the background color of the frame (displayed when there are no other contents)
+		
+METHODS:
+
+	frame.open();				//populates frame contents in character map
+	frame.close();				//removes frame contents from character map
+	frame.draw();				//draws the characters occupied by 'frame' coords/dimensions 
+	frame.cycle();				//checks the display matrix for updated characters and displays them 
+	frame.load(filename):			//loads a binary graphic (.BIN) or ANSI graphic (.ANS) file into the frame
+	frame.bottom();				//push frame to bottom of display stack
+	frame.top();					//pull frame to top of display stack
+	frame.scroll(dir);				//scroll frame one line in either direction ***NOT YET IMPLEMENTED***
+	frame.move(x,y);				//move frame one space where x = -1,0,1 and y = -1,0,1 
+	frame.moveTo(x,y);				//move frame to absolute position
+	
+	frame.clearline(attr);			//see http://synchro.net/docs/jsobjs.html#console
+	frame.cleartoeol(attr);
+	frame.putmsg(str);
+	frame.clear(attr);
+	frame.home();
+	frame.center(str);
+	frame.crlf();
+	frame.getxy();
+	frame.gotoxy(x,y);
+
+ USAGE:
+
+	//create a new frame object at screen position 1,1. 80 characters wide by 24 tall
+	var frame = load("frame.js",1,1,80,24,BG_BLUE);
+	
+ 	//or it can be done this way.....
+ 	load("frame.js");
+ 	var frame = new Frame(1,1,80,24,BG_BLUE);
+ 
+	//add a new frame within the frame object that will display on top at position 10,10
+	var subframe = new Frame(10,10,10,10,BG_GREEN,frame);
+	
+	.//beware this sample infinite loop
+ 	while(!js.terminated) { 
+	
+		//on first call this will draw the entire initial frame
+		//on subsequent calls this will draw only characters that have changed
+		frame.cycle();
+	}
+	
+	//close out the entire frame tree
+	frame.close();
+	
+TODO:
+
+	add a named Queue() to optionally receive messages from outside of
+	the parent script
+	
+ */
+ 
+load("sbbsdefs.js");
+
+function Frame(x,y,width,height,attr,frame) {
+
+	/* matrix representing frame positional and dimensional limits */
+	function CharMap(x,y,width,height) {
+		/* private properties */
+		var properties = {
+			x:undefined,
+			y:undefined,
+			width:undefined,
+			height:undefined,
+			map:[],
+			update:{}
+		}
+
+		/* protected properties */
+		this.x getter = function() {
+			return properties.x;
+		}
+		this.x setter = function(x) {
+			if(x == undefined)
+				properties.x = 1;
+			else if(isNaN(x) || x < 1 || x > console.screen_columns)
+				throw("invalid x coordinate: " + x);
+			else 
+				properties.x = x;
+		}
+		this.y getter = function() {
+			return properties.y;
+		}
+		this.y setter = function(y) {
+			if(y == undefined)
+				properties.y = 1;
+			else if(isNaN(y) || y < 1 || y > console.screen_rows)
+				throw("invalid y coordinate: " + y);
+			else 
+				properties.y = y;
+		}
+		this.width getter = function() {
+			return properties.width;
+		}
+		this.width setter = function(width) {
+			if(width == undefined)
+				properties.width = console.screen_columns;
+			else if(isNaN(width) || (x + width - 1) > (console.screen_columns))
+				throw("invalid width: " + width);
+			else 
+				properties.width = width;
+		}
+		this.height getter = function() {
+			return properties.height;
+		}
+		this.height setter = function(height) {
+			if(height == undefined)
+				properties.height = console.screen_rows;
+			else if(isNaN(height) || (y + height - 1) > (console.screen_rows))
+				throw("invalid height: " + height);
+			else
+				properties.height = height;
+		}
+		
+		/* public methods */
+		this.cycle = function() {
+			for(var y in properties.update) {
+				var lastx = undefined;
+				for(var x in properties.update[y]) {
+					var posx = Number(x) + properties.x;
+					var posy = Number(y) + properties.y;
+					if(lastx == undefined || x - lastx !== 1) 
+						console.gotoxy(posx,posy);
+					drawChar(x,y,posx,posy);
+					lastx = Number(x);
+				}
+			}
+			properties.update = {};
+ 		}
+		this.draw = function(xpos,ypos,width,height) {
+			var xoff = xpos - properties.x;
+			var yoff = ypos - properties.y;
+			for(var y=0;y<height;y++) {
+				console.gotoxy(xpos,ypos + y);
+				for(var x=0;x<width;x++) {
+					drawChar(x+xoff,y+yoff,xpos,ypos);
+				}
+			}
+		}
+		this.add = function(frame) {
+			for(var xoff = 0;xoff<frame.width;xoff++) {
+				for(var yoff = 0;yoff<frame.height;yoff++) {
+					var x = frame.x - properties.x;
+					var y = frame.y - properties.y;
+					properties.map[xoff+x][yoff+y][frame.id] = 
+						new CharPointer(frame,xoff,yoff);
+					updateChar(xoff + x,yoff + y);
+				}
+			}
+		}
+		this.remove = function(frame) {
+			for(var xoff = 0;xoff<frame.width;xoff++) {
+				for(var yoff = 0;yoff<frame.height;yoff++) {
+					var x = frame.x - properties.x;
+					var y = frame.y - properties.y;
+					delete properties.map[xoff+x][yoff+y][frame.id];
+					updateChar(xoff + x,yoff + y);
+				}
+			}
+		}
+		this.top = function(frame) {
+			for(var xoff = 0;xoff<frame.width;xoff++) {
+				for(var yoff = 0;yoff<frame.height;yoff++) {
+					var x = frame.x - properties.x;
+					var y = frame.y - properties.y;
+					delete properties.map[xoff+x][yoff+y][frame.id];
+					properties.map[xoff+x][yoff+y][frame.id] = 
+						new CharPointer(frame,xoff,yoff);
+					updateChar(xoff + x,yoff + y);
+				}
+			}
+		}
+		this.bottom = function(frame) {
+			for(var xoff = 0;xoff<frame.width;xoff++) {
+				for(var yoff = 0;yoff<frame.height;yoff++) {
+					var x = frame.x - properties.x;
+					var y = frame.y - properties.y;
+					for(var f in properties.map[xoff+x][yoff+y]) {
+						var cp = properties.map[xoff+x][yoff+y][f];
+						if(cp.frame.id == frame.id)
+							continue;
+						delete properties.map[xoff+x][yoff+y][f];
+						properties.map[xoff+x][yoff+y][f] = 
+							cp;
+					}
+					updateChar(xoff + x,yoff + y);
+				}
+			}
+		}
+		this.update = function(frame,xoff,yoff) {
+			var x = frame.x - properties.x;
+			var y = frame.y - properties.y;
+			updateChar(xoff + x,yoff + y);
+		}
+		
+		/* private functions */
+		function updateChar(x,y) {
+			if(!properties.update[y])
+				properties.update[y] = {};
+			properties.update[y][x] = 1;
+		}
+		function drawChar(x,y,xpos,ypos) {
+			var sector = getLast(properties.map[x][y]);
+			var ch = undefined;
+			var attr = undefined;
+
+			if(sector instanceof CharPointer) {
+				ch = sector.frame.data[sector.x][sector.y].ch;
+				attr = sector.frame.data[sector.x][sector.y].attr;
+			}
+			console.attributes = attr;
+			if(xpos == console.screen_columns && ypos == console.screen_rows) 
+				console.cleartoeol();
+			else if(ch == undefined)
+				console.write(" ");
+			else 
+				console.write(ch);
+		}
+		function init(x,y,width,height) {
+			this.x = x;
+			this.y = y;
+			this.width = width;
+			this.height = height;
+			
+			for(var w = 0;w<this.width;w++) {
+				properties.map.push(new Array(height));
+				for(var h = 0;h<this.height;h++)
+					properties.map[w][h] = {}; 
+			}
+		}
+		init.apply(this,arguments);
+	}
+	
+	/* frame and coordinate reference */
+	function CharPointer(frame,x,y) {
+		this.frame = frame;
+		this.x = x;
+		this.y = y;
+	}
+	
+	/* character/attribute pair representing a screen position and its contents */
+	function Char(ch,attr) {
+		this.ch = ch;
+		this.attr = attr;
+	}
+	
+	/* private properties */
+	var properties = {
+		x:undefined,
+		y:undefined,
+		width:undefined,
+		height:undefined,
+		attr:undefined,
+		map:undefined,
+		data:[],
+		parent:undefined,
+		child:[],
+		cursor:{x:0,y:0},
+		id:0
+	}
+		
+	/* protected properties */
+	this.id getter = function() {
+		if(properties.parent)
+			return properties.parent.id+""+properties.id;
+		return properties.id;
+	}
+	this.parent getter = function() {
+		return properties.parent;
+	}
+	this.child getter = function() {
+		return properties.child;
+	}
+	this.child setter = function(frame) {
+		properties.child.push(frame);
+	}
+	this.data getter = function() {
+		return properties.data;
+	}
+	this.map getter = function() {
+		return properties.map;
+	}
+	this.attr getter = function() {
+		return properties.attr;
+	}
+	this.attr setter = function(attr) {
+		properties.attr = attr;
+	}
+	this.name getter = function() {
+		return properties.name;
+	}
+	this.name setter = function(name) {
+		properties.name = name;
+	}
+	this.x getter = function() { 
+		if(properties.x)
+			return properties.x;
+		return properties.map.x; 
+	}
+	this.x setter = function(x) {
+		if(x == undefined)
+			return;
+		if(x < 1 || isNaN(x))
+			throw("invalid x coordinate: " + x);
+		else if(x > (properties.map.x + properties.map.width - 1) || x < properties.map.x)
+			throw("invalid x coordinate: " + x);
+		properties.x = x;
+	}
+	this.y getter = function() { 
+		if(properties.y)
+			return properties.y;
+		return properties.map.y; 
+	}
+	this.y setter = function(y) {
+		if(y == undefined)
+			return;
+		if(y < 1 || isNaN(y))
+			throw("invalid y coordinate: " + y);
+		else if(y > (properties.map.y + properties.map.height - 1) || y < properties.map.y)
+			throw("invalid y coordinate: " + y);
+		properties.y = y;
+	}
+	this.width getter = function() {
+		if(properties.width)
+			return properties.width;
+		return properties.map.width;
+	}
+	this.width setter = function(width) {
+		if(width == undefined)
+			return;
+		else if(width < 1 || isNaN(width))
+			throw("invalid width: " + width);
+		else if((properties.x + width) > (properties.map.x + properties.map.width))
+			throw("invalid width: " + width);
+		properties.width = width;
+	}
+	this.height getter = function() {
+		if(properties.height)
+			return properties.height;
+		return properties.map.height;
+	}
+	this.height setter = function(height) {
+		if(height == undefined)
+			return;
+		else if(height < 1 || isNaN(height)) 
+			throw("invalid height: " + height);
+		else if((properties.y+ height) > (properties.map.y + properties.map.height))
+			throw("invalid height: " + height);
+		properties.height = height;
+	}
+
+	/* public methods */
+	this.bottom = function() {
+		properties.map.bottom(this);
+	}
+	this.top = function() {
+		properties.map.top(this);
+	}
+	this.open = function() {
+		properties.map.add(this);
+		for each(var c in properties.child) 
+			c.open();
+	}
+	this.close = function() {
+		for each(var c in properties.child) 
+			c.close();
+		properties.map.remove(this);
+	}
+	this.move = function(x,y) {
+		if(this.x+x < properties.map.x ||
+			this.x+x + this.width > properties.map.x + properties.map.width)
+			return false;
+		if(this.y+y < properties.map.y ||
+			this.y+y + this.height > properties.map.y + properties.map.height)
+			return false;
+		properties.map.remove(this);
+		this.x += x;
+		this.y += y;
+		properties.map.add(this);
+	}
+	this.moveTo = function(x,y) {
+		if(x < properties.map.x || x + this.width > properties.map.x + properties.map.width)
+			return false;
+		if(y < properties.map.y || y + this.height > properties.map.y + properties.map.height)
+			return false;
+		properties.map.remove(this);
+		this.x = x;
+		this.y = y;
+		properties.map.add(this);
+	}
+	this.draw = function() {
+		properties.map.draw(this.x,this.y,this.width,this.height);
+	}
+	this.cycle = function() {
+		return properties.map.cycle();
+	}
+	this.load = function(filename) {
+		var f=new File(filename);
+		switch(file_getext(filename).substr(1).toUpperCase()) {
+		case "ANS":
+			if(!(f.open("r",true,4096)))
+				return(false);
+			var lines=f.readAll();
+			f.close();
+			
+			var attr = this.attr;
+			var bg = BG_BLACK;
+			var fg = LIGHTGRAY;
+			var i = 0;
+
+			var y = 0;
+			while(lines.length > 0 && y < this.height) {	
+				var x = 0;
+				var line = lines.shift();
+				while(line.length > 0 && x < this.width) {
+					/* parse an attribute sequence*/
+					var m = line.match(/^\x1b\[(\d+);?(\d*);?(\d*)m/);
+					if(m !== null) {
+						line = line.substr(m.shift().length);
+						if(m[0] == 0) {
+							bg = BG_BLACK;
+							fg = LIGHTGRAY;
+							i = 0;
+							m.shift();
+						}
+						if(m[0] == 1) {
+							i = HIGH;
+							m.shift();
+						}
+						if(m[0] >= 40) {
+							switch(Number(m.shift())) {
+							case 40:
+								bg = BG_BLACK;
+								break;
+							case 41:
+								bg = BG_RED;
+								break;
+							case 42: 
+								bg = BG_GREEN;
+								break;
+							case 43:
+								bg = BG_BROWN;
+								break;
+							case 44:
+								bg = BG_BLUE;
+								break;
+							case 45:
+								bg = BG_MAGENTA;
+								break;
+							case 46:
+								bg = BG_CYAN;
+								break;
+							case 47:
+								bg = BG_LIGHTGRAY;
+								break;
+							}
+						}
+						if(m[0] >= 30) {
+							switch(Number(m.shift())) {
+							case 30:
+								fg = BLACK;
+								break;
+							case 31:
+								fg = RED;
+								break;
+							case 32:
+								fg = GREEN;
+								break;
+							case 33:
+								fg = BROWN;
+								break;
+							case 34:
+								fg = BLUE;
+								break;
+							case 35:
+								fg = MAGENTA;
+								break;
+							case 36:
+								fg = CYAN;
+								break;
+							case 37:
+								fg = LIGHTGRAY;
+								break;
+							}
+						}
+						attr = bg + fg + i;
+						continue;
+					}
+					/* parse a positional sequence */
+					var n = line.match(/^\x1b\[(\d+)C/);
+					if(n !== null) {
+						line = line.substr(n.shift().length);
+						x+=Number(n.shift());
+						continue;
+					}
+					/* set character and attribute */
+					var ch = line[0];
+					line = line.substr(1);
+					properties.data[x][y]=new Char(ch,attr);
+					x++;
+				}
+				y++;
+			}
+			break;
+		case "BIN":
+			if(!(f.open("rb",true,4096)))
+				return(false);
+			for(var y=0; y<this.height; y++) {
+				for(var x=0; x<this.width; x++) {
+					var c = new Char();
+					if(f.eof)
+						return(false);
+					c.ch = f.read(1);
+					if(f.eof)
+						return(false);
+					c.attr = f.readBin(1);
+					properties.data[x][y] = c;
+				}
+			}
+			f.close();
+			break;
+		default:
+			throw("unsupported filetype");
+			break;
+		}
+	}
+	this.scroll = function(dir) {
+		if(dir == undefined) {
+			for(var x = 0;x<this.width;x++) {
+				for(var y = 0;y<this.height;y++) {
+					if(properties.data[x][y+1]) {
+						properties.data[x][y].ch = properties.data[x][y+1].ch;
+						properties.data[x][y].attr = properties.data[x][y+1].attr;
+					}
+					else {
+						properties.data[x][y].ch = undefined;
+						properties.data[x][y].attr = this.attr;
+					}
+					properties.map.update(this,x,y);
+				}
+			}
+		}
+	}
+
+	/* console method emulation */
+	this.home = function() {
+		properties.cursor.x = 0;
+		properties.cursor.y = 0;
+	}
+	this.clear = function(attr) {
+		if(attr == undefined)
+			attr = this.attr;
+		for(var x = 0;x<this.width;x++) {
+			for(var y = 0;y<this.height;y++) {
+				properties.data[x][y].ch = undefined;
+				properties.data[x][y].attr = attr;
+				properties.map.update(this,x,y);
+			}
+		}
+		this.home();
+	}
+	this.clearline = function(attr) {
+		if(attr == undefined)
+			attr = this.attr;
+		for(var x = 0;x<this.width;x++) {
+			properties.map.update(this,x,y);
+			properties.data[x][y].ch = undefined;
+			properties.data[x][y].attr = attr;
+		}
+	}
+	this.cleartoeol = function(attr) {
+		if(attr == undefined)
+			attr = this.attr;
+		for(var x = properties.cursor.x;x<this.width;x++) {
+			properties.map.update(this,x,y);
+			properties.data[x][y].ch = undefined;
+			properties.data[x][y].attr = attr;
+		}
+	}
+	this.crlf = function() {
+		properties.cursor.x = 0;
+		if(properties.cursor.y < this.height-1) 
+			properties.cursor.y += 1;
+		else {}
+	}
+	this.putmsg = function(str) {
+		str = str.split('');
+		var control_a = false;
+		var curattr = this.attr;
+		var pos = properties.cursor;
+
+		while(str.length > 0) {
+			if(pos.x >= this.width) {
+				pos.x=0;
+				pos.y++;
+			}
+			
+			if(pos.y >= this.height) {	
+				this.scroll();
+				pos.y--;
+			}
+			var ch = str.shift();
+			if(control_a) {
+				switch(ch) {
+				case '\1':	/* A "real" ^A code */
+					properties.data[pos.x][pos.y].ch=ch;
+					properties.data[pos.x][pos.y].attr=curattr;
+					properties.map.update(this,pos.x,pos.y);
+					pos.x++;
+					break;
+				case 'K':	/* Black */
+					curattr=(curattr)&0xf8;
+					break;
+				case 'R':	/* Red */
+					curattr=((curattr)&0xf8)|RED;
+					break;
+				case 'G':	/* Green */
+					curattr=((curattr)&0xf8)|GREEN;
+					break;
+				case 'Y':	/* Yellow */
+					curattr=((curattr)&0xf8)|BROWN;
+					break;
+				case 'B':	/* Blue */
+					curattr=((curattr)&0xf8)|BLUE;
+					break;
+				case 'M':	/* Magenta */
+					curattr=((curattr)&0xf8)|MAGENTA;
+					break;
+				case 'C':	/* Cyan */
+					curattr=((curattr)&0xf8)|CYAN;
+					break;
+				case 'W':	/* White */
+					curattr=((curattr)&0xf8)|LIGHTGRAY;
+					break;
+				case '0':	/* Black */
+					curattr=(curattr)&0x8f;
+					break;
+				case '1':	/* Red */
+					curattr=((curattr)&0x8f)|(RED<<4);
+					break;
+				case '2':	/* Green */
+					curattr=((curattr)&0x8f)|(GREEN<<4);
+					break;
+				case '3':	/* Yellow */
+					curattr=((curattr)&0x8f)|(BROWN<<4);
+					break;
+				case '4':	/* Blue */
+					curattr=((curattr)&0x8f)|(BLUE<<4);
+					break;
+				case '5':	/* Magenta */
+					curattr=((curattr)&0x8f)|(MAGENTA<<4);
+					break;
+				case '6':	/* Cyan */
+					curattr=((curattr)&0x8f)|(CYAN<<4);
+					break;
+				case '7':	/* White */
+					curattr=((curattr)&0x8f)|(LIGHTGRAY<<4);
+					break;
+				case 'H':	/* High Intensity */
+					curattr|=HIGH;
+					break;
+				case 'I':	/* Blink */
+					curattr|=BLINK;
+					break;
+				case 'N':	/* Normal (ToDo: Does this do ESC[0?) */
+					curattr=7;
+					break;
+				case '-':	/* Normal if High, Blink, or BG */
+					if(curattr & 0xf8)
+						curattr=7;
+					break;
+				case '_':	/* Normal if blink/background */
+					if(curattr & 0xf0)
+						curattr=7;
+					break;
+				case '[':	/* CR */
+					pos.x=0;
+					break;
+				case ']':	/* LF */
+					pos.y++;
+					break;
+				default:	/* Other stuff... specifically, check for right movement */
+					if(ch.charCodeAt(0)>127) {
+						pos.x+=ch.charCodeAt(0)-127;
+						if(pos.x>=this.width)
+							pos.x=this.width-1;
+					}
+					break;
+				}
+				control_a = false;
+			}
+			else {
+				switch(ch) {
+				case '\1':		/* CTRL-A code */
+					control_a = true;
+					break;
+				case '\7':		/* Beep */
+					break;
+				case '\r':
+					pos.x=0;
+					break;
+				case '\n':
+					pos.y++;
+					break;
+				default:
+					properties.data[pos.x][pos.y].ch=ch;
+					properties.data[pos.x][pos.y].attr=curattr;
+					properties.map.update(this,pos.x,pos.y);
+					pos.x++;
+					break;
+				}
+			}
+		}
+	}
+	this.center = function(str) {
+		properties.cursor.x = Math.ceil(this.width/2) - Math.ceil(console.strlen(str)/2) + 1;
+		if(properties.cursor.x < 0)
+			properties.cursor.x = 0;
+		this.putmsg(str);
+	}
+	this.gotoxy = function(x,y) {
+		if(x <= this.width)
+			properties.cursor.x = x-1;
+		if(y <= this.height)
+			properties.cursor.y = y-1;
+	}
+	this.getxy = function() {
+		var xy = {
+			x:properties.cursor.x+1,
+			y:properties.cursor.y+1
+		}
+		return xy;
+	}
+	
+	/* private functions */
+	function getLast(obj) {
+		var last = undefined;
+		for each(var i in obj)
+			last = i;
+		return last;
+	}
+	function init(x,y,width,height,attr,frame) {
+		if(frame instanceof Frame) {
+			properties.id = frame.child.length;
+			properties.map = frame.map;
+			properties.parent = frame;
+			frame.child = this;
+		}
+		else {
+			properties.map = new CharMap(x,y,width,height);
+		}
+
+		this.transparent = false;
+		this.x = x;
+		this.y = y;
+		this.width = width;
+		this.height = height;
+		this.attr = attr;
+		
+		for(var w=0;w<this.width;w++) {
+			properties.data.push(new Array(this.height));
+			for(var h=0;h<this.height;h++) {
+				properties.data[w][h] = new Char(undefined,this.attr);
+			}
+		}
+	}
+	init.apply(this,arguments);
+}
+
+if(argc >= 4)
+	frame = new Frame(argv[0],argv[1],argv[2],argv[3],argv[4],argv[5]);
\ No newline at end of file