Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6f8cd5c
Prevent emitting null lines for JSON-RPC notifications in mcpStdio me…
nmaguiar Feb 5, 2026
51acd00
chore(deps): bump org.jetbrains.kotlin:kotlin-stdlib
dependabot[bot] Feb 5, 2026
7aa4688
chore(deps-dev): bump org.apache.maven.plugins:maven-compiler-plugin
dependabot[bot] Feb 5, 2026
0f1792f
Enhance $jsonrpc and $mcp functions to support optional per-call opti…
nmaguiar Feb 6, 2026
9637894
Merge pull request #1680 from OpenAF/dependabot/maven/t8/org.apache.m…
nmaguiar Feb 6, 2026
8675897
feat: add semantic version parsing and comparison functions
nmaguiar Feb 6, 2026
28df8d3
Merge pull request #1679 from OpenAF/dependabot/maven/t8/org.jetbrain…
nmaguiar Feb 6, 2026
e13ff7e
Add setTimeout, setInterval, and clearInterval functions with documen…
nmaguiar Feb 8, 2026
249c013
Merge branch 't8' of https://github.com/openaf/openaf into t8
nmaguiar Feb 8, 2026
143e499
fix: add sleep after starting process in restartOpenAF function
nmaguiar Feb 9, 2026
b1fac22
fix: update ojdbc17 dependency version and remove sleep in restartOpe…
nmaguiar Feb 11, 2026
409cf77
feat: add restart functionality and methods to manage OpenAF processes
nmaguiar Feb 11, 2026
79b34f5
Initial plan
Copilot Feb 11, 2026
0190cb5
Add OAF_PIDFILE environment variable support to ow.server.checkIn
Copilot Feb 11, 2026
37fe9e5
Update test to use realistic approach for OAF_PIDFILE testing
Copilot Feb 11, 2026
3f55671
Improve test comments to explain OAF_PIDFILE testing approach
Copilot Feb 11, 2026
8deb6b1
Fix documentation formatting and improve test filename clarity
Copilot Feb 11, 2026
1ce42fb
Simplify logic and enhance test coverage for OAF_PIDFILE
Copilot Feb 11, 2026
64b40b1
Add trim() to handle whitespace-only environment variable values
Copilot Feb 11, 2026
675294f
Fix spacing and improve test documentation clarity
Copilot Feb 11, 2026
0bad8d2
use modern functions
nmaguiar Feb 11, 2026
95a2332
Merge 0bad8d265414452fa4a2938dc9af7595f29f3174 into 409cf771b5790a1b7…
Copilot Feb 11, 2026
db8df9b
Test results badge
github-actions[bot] Feb 11, 2026
3f6a647
Test results badge
github-actions[bot] Feb 11, 2026
3a439fb
Test results badge
github-actions[bot] Feb 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified compiler.jar
Binary file not shown.
6 changes: 6 additions & 0 deletions docs/openaf-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ Central list of noteworthy runtime flags and environment variables.
| OAF_SIGNATURE_KEY | (unset) | Public key for signature validation |
| OAF_VALIDATION_STRICT | false | Require integrity + signature both |

## Server Management

| Env | Default | Purpose |
|-----|---------|---------|
| OAF_PIDFILE | (unset) | Override PID file path in ow.server.checkIn |

## Misc Performance / Behavior

| Flag | Purpose |
Expand Down
1 change: 1 addition & 0 deletions docs/openaf.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ ow.sec.closeMainSBuckets();
ow.loadServer();

// Process management
// Note: The PID file path can be overridden by setting the OAF_PIDFILE environment variable
var isRunning = ow.server.checkIn("server.pid",
function(existingPid) {
log("Server already running with PID: " + existingPid);
Expand Down
72 changes: 72 additions & 0 deletions js/mdtablesort.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,68 @@ function parseNumberWithUnits(value) {
return numberPart * multiplier;
}

// Parse a semantic version string and return normalized components.
function parseSemVer(value) {
if (value === null || value === undefined) return null;
const cleaned = String(value).trim();
if (!cleaned) return null;

const match = cleaned.match(
/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/
);
if (!match) return null;

return {
major: parseInt(match[1], 10),
minor: parseInt(match[2], 10),
patch: parseInt(match[3], 10),
prerelease: match[4] ? match[4].split('.') : []
};
}

function isNumericIdentifier(value) {
return /^\d+$/.test(value);
}

// Compare semantic versions according to SemVer precedence rules.
function compareSemVer(a, b) {
if (a.major !== b.major) return a.major < b.major ? -1 : 1;
if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1;
if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1;

const aHasPre = a.prerelease.length > 0;
const bHasPre = b.prerelease.length > 0;
if (!aHasPre && !bHasPre) return 0;
if (!aHasPre) return 1;
if (!bHasPre) return -1;

const len = Math.max(a.prerelease.length, b.prerelease.length);
for (let i = 0; i < len; i++) {
const ai = a.prerelease[i];
const bi = b.prerelease[i];

if (ai === undefined) return -1;
if (bi === undefined) return 1;
if (ai === bi) continue;

const aiNum = isNumericIdentifier(ai);
const biNum = isNumericIdentifier(bi);

if (aiNum && biNum) {
const aNum = parseInt(ai, 10);
const bNum = parseInt(bi, 10);
if (aNum !== bNum) return aNum < bNum ? -1 : 1;
continue;
}

if (aiNum && !biNum) return -1;
if (!aiNum && biNum) return 1;
return ai < bi ? -1 : 1;
}

return 0;
}

// Add helper function to support "yyyy-MM-dd HH:mm:ss" format.
function parseDate(str) {
// If the string contains a space but no 'T', replace the first space with 'T'
Expand Down Expand Up @@ -262,6 +324,16 @@ function sortTable(table) {
let valA = cellA ? cellA.textContent.trim() : '';
let valB = cellB ? cellB.textContent.trim() : '';

// Attempt semantic version parsing before generic date/string checks
const semverA = parseSemVer(valA);
const semverB = parseSemVer(valB);
if (semverA && semverB) {
const semverCmp = compareSemVer(semverA, semverB);
if (semverCmp < 0) return crit.direction === 'asc' ? -1 : 1;
if (semverCmp > 0) return crit.direction === 'asc' ? 1 : -1;
continue;
}

// Attempt date parsing first
const timeA = parseDate(valA);
const timeB = parseDate(valB);
Expand Down
161 changes: 148 additions & 13 deletions js/openaf.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,8 @@ var __flags = ( typeof __flags != "undefined" && "[object Object]" == Object.pro
merge : true,
jsonParse: true,
listFilesRecursive: true,
colorify : true
colorify : true,
restart : true
},
WITHMD: {
htmlFilter: true
Expand Down Expand Up @@ -4504,8 +4505,13 @@ const stopOpenAFAndRun = function(aCommandLineArray, addCommand) {
* element will be use sequentially to build the command line to start a new OpenAF instance.
* preCommandLineArray can be used to provide java arguments if defined.
* </odoc>
*
*/
const restartOpenAF = function(aCommandLineArray, preLineArray, noStop) {
if (__flags.ALTERNATIVES.restart) {
endOpenAFAndStartOpenAF(aCommandLineArray, preLineArray, noStop)
return
}
var javaBin = java.lang.System.getProperty("java.home") + java.io.File.separator + "bin" + java.io.File.separator + "java";
var currentJar = getOpenAFJar();

Expand Down Expand Up @@ -4540,7 +4546,7 @@ const restartOpenAF = function(aCommandLineArray, preLineArray, noStop) {

var builder = new java.lang.ProcessBuilder(command);
builder.inheritIO();
builder.start();
builder.start()
if (!noStop) java.lang.System.exit(0);
}

Expand All @@ -4559,6 +4565,81 @@ const forkOpenAF = function(aCommandLineArray, preLineArray) {
});
}

const __quotePosixArg = aArg => "'" + String(aArg).replace(/'/g, "'\"'\"'") + "'"
const __quoteCmdArg = aArg => {
var s = String(aArg)
if (s.length == 0) return "\"\""
if (/[\s"&|<>^()%!]/.test(s)) return "\"" + s.replace(/(["^%!])/g, "^$1") + "\""
return s
}

/**
* <odoc>
* <key>endOpenAFAndStart(aCommandLineArray, noStop)</key>
* Terminates the current OpenAF execution while trying to execute the commands on the aCommandLineArray
* in detached mode. On Unix systems it uses nohup (and setsid if available). On Windows it uses cmd start.
* </odoc>
*/
const endOpenAFAndStart = function(aCommandLineArray, noStop) {
_$(aCommandLineArray).isArray().$_("Please provide a command line array.")
noStop = _$(noStop).isBoolean().default(false)
if (aCommandLineArray.length <= 0) throw "Please provide a non-empty command line array."

var osName = String(java.lang.System.getProperty("os.name")).toLowerCase()
var isWindows = osName.indexOf("windows") >= 0
var commandLine = []

if (isWindows) {
var cmd = "start \"\" /B " + aCommandLineArray.map(__quoteCmdArg).join(" ")
commandLine = [ "cmd", "/c", cmd ]
} else {
var cmd = aCommandLineArray.map(__quotePosixArg).join(" ")
var shellCmd = "(command -v setsid >/dev/null 2>&1 && nohup setsid " + cmd + " >/dev/null 2>&1 < /dev/null || nohup " + cmd + " >/dev/null 2>&1 < /dev/null) &"
commandLine = [ "/bin/sh", "-c", shellCmd ]
}

var builder = new java.lang.ProcessBuilder(commandLine.map(s => new java.lang.String(s)))
builder.start()
sleep(100, true)
if (!noStop) java.lang.System.exit(0)
}

/**
* <odoc>
* <key>endOpenAFAndStartOpenAF(aCommandLineArray, preCommandLineArray, noStop)</key>
* Terminates the current OpenAF execution and tries to start a detached OpenAF process with the current
* OpenAF command line plus an extra aCommandLineArray. preCommandLineArray can be used to provide java arguments.
* </odoc>
*/
const endOpenAFAndStartOpenAF = function(aCommandLineArray, preLineArray, noStop) {
if (isDef(aCommandLineArray)) _$(aCommandLineArray).isArray().$_("Please provide a command line array.");
noStop = _$(noStop).isBoolean().default(false);

var javaBin = java.lang.System.getProperty("java.home") + java.io.File.separator + "bin" + java.io.File.separator + "java";
var currentJar = getOpenAFJar();
if (!currentJar.endsWith(".jar")) return;

var command = [];
command.push(String(javaBin));

if (isDef(preLineArray)) {
for (var c in preLineArray) command.push(String(preLineArray[c]));
} else {
var ar = java.lang.management.ManagementFactory.getRuntimeMXBean().getInputArguments();
for (var ari = 0; ari < ar.size(); ari++) command.push(String(ar.get(ari)));
}

command.push("-jar");
command.push(String(currentJar));

for (var i in __args) command.push(String(__args[i]));
if (isDef(aCommandLineArray)) {
for (var ai in aCommandLineArray) command.push(String(aCommandLineArray[ai]));
}

return endOpenAFAndStart(command, noStop);
}

/**
* <odoc>
* <key>compare(X, Y) : Boolean</key>
Expand Down Expand Up @@ -8513,7 +8594,8 @@ const $jsonrpc = function (aOptions) {
_debug("jsonrpc command set to: " + cmd)
return _r
},
exec: (aMethod, aParams, aNotification) => {
exec: (aMethod, aParams, aNotification, aExecOptions) => {
aExecOptions = _$(aExecOptions, "aExecOptions").isMap().default({})
switch (aOptions.type) {
case "dummy":
aOptions.options = _$(aOptions.options, "aOptions.options").isMap().default({})
Expand Down Expand Up @@ -8566,12 +8648,14 @@ const $jsonrpc = function (aOptions) {
}
if (aMethod == "initialize" && !aNotification) _r._info = isDef(_res) && isDef(_res.result) ? _res.result : _res
return isDef(_res) && isDef(_res.result) ? _res.result : _res
case "remote":
default:
_$(aOptions.url, "aOptions.url").isString().$_()
aOptions.options = _$(aOptions.options, "aOptions.options").isMap().default({})
case "remote":
default:
_$(aOptions.url, "aOptions.url").isString().$_()
aOptions.options = _$(aOptions.options, "aOptions.options").isMap().default({})
aMethod = _$(aMethod, "aMethod").isString().$_()
aParams = _$(aParams, "aParams").isMap().default({})
var _restOptions = clone(aOptions.options)
if (isMap(aExecOptions.restOptions)) _restOptions = merge(_restOptions, aExecOptions.restOptions)

var _req = {
jsonrpc: "2.0",
Expand All @@ -8585,7 +8669,7 @@ const $jsonrpc = function (aOptions) {
delete _req.id
}
_debug("jsonrpc -> " + stringify(_req, __, ""))
var res = $rest(aOptions.options).post(aOptions.url, _req)
var res = $rest(_restOptions).post(aOptions.url, _req)
// Notifications do not expect a reply
if (!!aNotification) return
_debug("jsonrpc <- " + stringify(res, __, ""))
Expand Down Expand Up @@ -8631,7 +8715,7 @@ const $jsonrpc = function (aOptions) {
* \
* The aOptions parameter is a map with the following possible keys:\
* \
* - type (string): Connection type - "stdio" for local process, "remote" for HTTP server, "dummy" for local testing, or "ojob" for oJob-based server (default: "stdio")\
* - type (string): Connection type - "stdio" for local process, "remote"/"http" for HTTP server, "dummy" for local testing, or "ojob" for oJob-based server (default: "stdio")\
* - url (string): Required for remote servers - the MCP server endpoint URL\
* - timeout (number): Timeout in milliseconds for operations (default: 60000)\
* - cmd (string): Required for stdio type - the command to launch the MCP server\
Expand Down Expand Up @@ -8668,7 +8752,7 @@ const $jsonrpc = function (aOptions) {
* - sh(aCommand): Set the command and switch to stdio type\
* - initialize(clientInfo): Initialize the MCP connection and exchange capabilities\
* - listTools(): Get list of available tools from the MCP server\
* - callTool(toolName, toolArguments): Execute a specific tool with given arguments\
* - callTool(toolName, toolArguments, toolOptions): Execute a specific tool with given arguments and optional per-call options\
* - listPrompts(): Get list of available prompts from the MCP server\
* - getPrompt(promptName, promptArguments): Get a specific prompt with given arguments\
* - toGptTools(aGptInstance, aToolNames): Add MCP tools to a $gpt instance\
Expand Down Expand Up @@ -8696,6 +8780,8 @@ const $jsonrpc = function (aOptions) {
* clientInfo: {name: "MyApp", version: "2.0.0"}\
* });\
* remoteClient.initialize();\
* // Optional per-call HTTP options (only used for remote/http MCP clients)\
* var result2 = remoteClient.callTool("read_file", {path: "/tmp/example.txt"}, { requestHeaders: { Authorization: "Bearer ..." } });\
* var prompts = remoteClient.listPrompts();\
* \
* // Dummy mode for testing\
Expand Down Expand Up @@ -8950,12 +9036,13 @@ const $mcp = function(aOptions) {
}
return _jsonrpc.exec("tools/list", {})
},
callTool: (toolName, toolArguments) => {
callTool: (toolName, toolArguments, toolOptions) => {
if (!_r._initialized) {
throw new Error("MCP client not initialized. Call initialize() first.")
}
toolName = _$(toolName, "toolName").isString().$_()
toolArguments = _$(toolArguments, "toolArguments").isMap().default({})
toolOptions = _$(toolOptions, "toolOptions").isMap().default(__)

// Call pre-function if provided
if (aOptions.preFn) {
Expand All @@ -8965,7 +9052,7 @@ const $mcp = function(aOptions) {
var _res = _jsonrpc.exec("tools/call", {
name: toolName,
arguments: toolArguments
})
}, __, { restOptions: toolOptions })
// Call post-function if provided
if (aOptions.posFn) {
aOptions.posFn(toolName, toolArguments, _res)
Expand Down Expand Up @@ -9758,13 +9845,34 @@ if (isUnDef(alert)) alert = function(msg) {
};

var __timeout = {};
/**
* <odoc>
* <key>setTimeout(aFunction, aPeriod)</key>
* Tries to execute aFunction after aPeriod milliseconds. Note: this function will block the
* main thread, so use it with caution. If you need to execute aFunction in a different thread and avoid blocking the main thread, please use setInterval with a clearInterval after the first execution.
* \
* Example:\
* \
* setTimeout(() => { print("Hello after 2 seconds"); }, 2000);\
* \
* // or using setInterval to avoid blocking the main thread\
* var id = setInterval(() => { print("Hello after 2 seconds"); clearInterval(id); }, 2000);\
* \
* </odoc>
*/
const setTimeout = function(aFunction, aPeriod) {
sleep(aPeriod);
var args = [];
for(var i = 2; i <= arguments.length; i++) { args.push(arguments[i]); }
aFunction.apply(this, args);
}

/**
* <odoc>
* <key>setInterval(aFunction, aPeriod) : String</key>
* Tries to execute aFunction every aPeriod milliseconds. Returns an id that can be used to clear the interval with clearInterval. Note: this function uses Threads plugin, so it will execute aFunction in a different thread and it won't block the main thread. Also, do note that the aFunction execution time is not included in the aPeriod, so if aFunction takes 2 seconds to execute and aPeriod is 5 seconds, the next execution will be 5 seconds after aFunction finishes, so it will be 7 seconds after the previous execution start.
* </odoc>
*/
const setInterval = function(aFunction, aPeriod) {
plugin("Threads");
var t = new Threads();
Expand All @@ -9775,7 +9883,7 @@ const setInterval = function(aFunction, aPeriod) {
var parent = this;

var f = function(uuid) {
aFunction.apply(parent, args);
aFunction.apply(parent, args);
}

var uuid = t.addThread(f);
Expand All @@ -9784,6 +9892,12 @@ const setInterval = function(aFunction, aPeriod) {
return uuid;
}

/**
* <odoc>
* <key>clearInterval(uuid)</key>
* Clears the interval with the provided uuid returned by setInterval.
* </odoc>
*/
const clearInterval = function(uuid) {
var t = __timeout[uuid];
t.stop();
Expand Down Expand Up @@ -11277,6 +11391,27 @@ AF.prototype.fromObj2XML = function (obj, sanitize, aAttrKey, aPrefix, aSuffix)
*/
AF.prototype.encryptText = function() { plugin("Console"); print("Encrypted text: " + af.encrypt((new Console()).readLinePrompt("Enter text: ", "*"))); };

/**
* <odoc>
* <key>AF.endOpenAFAndStart(aCommandLineArray, noStop)</key>
* Calls endOpenAFAndStart to start a detached process and then end the current OpenAF execution.
* noStop=true can be used to avoid exiting the current OpenAF process.
* </odoc>
*/
AF.prototype.endOpenAFAndStart = function(aCommandLineArray, noStop) {
return endOpenAFAndStart(aCommandLineArray, noStop)
}

/**
* <odoc>
* <key>AF.endOpenAFAndStartOpenAF(aCommandLineArray, preCommandLineArray, noStop)</key>
* Calls endOpenAFAndStartOpenAF to detach and start another OpenAF with current arguments plus extras.
* </odoc>
*/
AF.prototype.endOpenAFAndStartOpenAF = function(aCommandLineArray, preLineArray, noStop) {
return endOpenAFAndStartOpenAF(aCommandLineArray, preLineArray, noStop)
}

/**
* <odoc>
* <key>AF.protectSystemExit(shouldProtect, aMessage)</key>
Expand Down
Loading