{"id":48297235,"url":"https://github.com/tschiemer/at-commander","last_synced_at":"2026-04-04T23:38:16.013Z","repository":{"id":57185987,"uuid":"62704059","full_name":"tschiemer/at-commander","owner":"tschiemer","description":"AT Command handler for serial ports (NodeJS)","archived":false,"fork":false,"pushed_at":"2018-02-08T11:24:21.000Z","size":28,"stargazers_count":15,"open_issues_count":1,"forks_count":7,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-10-03T19:58:32.952Z","etag":null,"topics":["at","command-handler","modem","serial"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tschiemer.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-07-06T08:21:20.000Z","updated_at":"2024-02-13T04:26:12.000Z","dependencies_parsed_at":"2022-09-06T04:02:07.617Z","dependency_job_id":null,"html_url":"https://github.com/tschiemer/at-commander","commit_stats":null,"previous_names":["smart-home-technology/at-commander"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/tschiemer/at-commander","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tschiemer%2Fat-commander","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tschiemer%2Fat-commander/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tschiemer%2Fat-commander/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tschiemer%2Fat-commander/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tschiemer","download_url":"https://codeload.github.com/tschiemer/at-commander/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tschiemer%2Fat-commander/sbom","scorecard":{"id":900573,"data":{"date":"2025-08-11","repo":{"name":"github.com/tschiemer/at-commander","commit":"2dabd621e0f8cf42c3575532cf86afef4155a1d0"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.6,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":0,"reason":"license file not detected","details":["Warn: project does not have a license file"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}}]},"last_synced_at":"2025-08-24T15:24:38.391Z","repository_id":57185987,"created_at":"2025-08-24T15:24:38.391Z","updated_at":"2025-08-24T15:24:38.391Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31419538,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T20:09:54.854Z","status":"ssl_error","status_checked_at":"2026-04-04T20:09:44.350Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["at","command-handler","modem","serial"],"created_at":"2026-04-04T23:38:15.356Z","updated_at":"2026-04-04T23:38:15.973Z","avatar_url":"https://github.com/tschiemer.png","language":"JavaScript","readme":"# AT Commander\n\n__Please note that this is still a beta version__\n\nPromise based AT(tention) command handler for serial ports (typically for use with external modem components and the like).\n\nThis module is ment to serve only as a basis for your specific device implementations - it is rather device agnostic, so to speak.\n\nFor a sample (beta) implementation of a real device see [telit-modem](https://www.npmjs.com/package/telit-modem)\n\nFeatures:\n\n- Send simple commands and receive boolean success/failure responses\n- Catch complex responses and preprocess\n- Set up notifications / event handlers for unsolicited messages\n- Command queue\n\nThis module uses the npm https://www.npmjs.com/package/serialport for serial communication.\n\n\n## Todos\n\n* Complete documentation..\n* Add more serialport configuration options (modem options are passthrough anyway, so just note that in the documentation...)\n* Add tests\n* Generic refactoring\n* Rethink timeout principle - is it ok like this or should it be remodelled? (timeout not absolute to actual command start but relative to last incoming data) (-\u003e process.nextTick ??)\n\n## Possible issues\n\nIn case something doesn't work as expected, please first look here.\n\n* After an inbuffer handling change (auto-discard of CR/NL prefixes) reading a specific number of bytes might have an unexpected behaviour. Well, changing this ment a simplification of usage but also a change in semantics as incoming data is being interpreted.\n\n## Overview\n\n* [Usage](#usage)\n  * [Example](#example)\n  * [Promise based commands](#promise-based-commands)\n* [Classes](#classes)\n  * [Modem](#modem)\n    * [Modem(options)](#modem-options)\n    * [getConfig()](#getConfig)\n    * [setConfig(options)](#setConfig-options)\n    * [open(path)](#open-path)\n    * [isOpen()](#isOpen)\n    * [pause()](#pause)\n    * [close(callback)](#close-callback)\n    * [closeGracefully(callback)](#closeGracefully-callback)\n    * [on(event, callback)](#on-event-callback)\n    * [isProcessingCommands()](#isProcessingCommands)\n    * [startProcessing()](#startProcessing)\n    * [stopProcessing(abortCurrent, callback)](#stopProcessing-abortCurrent-callback)\n    * [getPendingCommands()](#getPendingsCommands)\n    * [clearPendingCommands()](#clearPendingCommands)\n    * [getCurrentCommand()](#getCurrentCommand)\n    * [abortCurrentCommand()](#abortCurrentCommand)\n    * [run(command, expected, options)](#run-command-expected-options)\n    * [addCommand(command, expected, options)](#addCommand-command-expected-options)\n    * [read(n)](#read-n)\n    * [write(buf)](#write-buffer)\n    * [getInBuffer()](#)\n    * [clearInBuffer()](#)\n    * [getNotifications()](#)\n    * [clearNotifications()](#)\n    * [addNotification(name, regex, handler)](#)\n    * [removeNotification(name)](#)\n  * [Command](#command)\n  * [Notification](#notification)\n* [Events](#events)\n\n## Usage\n\n### Example\n\n    var ATCommander = require('at-commander');\n    var Command = ATCommander.Command;\n\n    // all options are optional, these are the default options\n    var opts = {\n        // the following options define the options used by serialport\n        parser: serialport.parsers.raw,\n        baudRate: 115200,\n        dataBits: 8,\n        stopBits: 1,\n\n        // command termination string (is added to every normal string type command)\n        EOL: \"\\r\\n\",\n\n        // this regex is used by default to detect one-line responses\n        lineRegex: /^\\r\\n(.+)\\r\\n/,\n\n        // (default) command timeout\n        timeout: 500\n    };\n\n    var modem = new ATCommander.Modem(opts);\n\n    var port = 'COM4'; // on Windows\n    var port = '/tty/serial/by-id/blabalbla'; // linux based machines\n\n    modem.open(port).catch((err) =\u003e {\n        console.log(\"Failed to open serial\", err);\n    }).then(function(){\n\n        // check if a response is coming\n        // NOTE: run(command) bypasses the command queue and is executed immediatly (unless another command is being executed already)\n        modem.run('AT').then((success) =\u003e {\n\n            modem.startProcessing();\n\n        });\n\n        // fill up command queue\n        // queue is only processed it modem.startProcessing() is called.\n        modem.addCommand('AT+CMG=1');\n\n        // identical to previous command\n        modem.addCommand('AT+CMG=1', undefined);\n\n        // with expected result 'OK' and command specific timeout\n        modem.addCommand('AT+FOOO', 'OK', {\n            timeout: 10000\n        }).then(function(){\n            // command got expected response\n        }).catch(function(command){\n            // some error occurred\n        });\n\n        // consider the next incoming 6 bytes as the wanted response\n        modem.addCommand('AT+FOOO', 6).then(function(buffer){\n            // buffer contains the next 6 incoming bytes (please note, that beginning CR + NL characters are trimmed automatically, thus (at the moment) if you expect to be reading only these characters your logic will fail)\n        }).catch(function(command){\n            // most likely to fail only if there is a timeout\n        });\n\n        modem.addCommand('AT+CREG=?', /\\+CREG=(.*),(.*)/).then((matches) =\u003e {\n            // matches contains the response's string matches according to the given regex\n        });\n\n        modem.addCommand('AT+FOOO',  function(buffer){\n            // complex response detectors are passed the updated response buffer contents whenever there is new data arriving\n            var str = buffer.toString();\n            if (str.matches(/^OK/r/n/){\n                return 4; // return the byte count the response (these many bytes will be consumed from the buffer)\n            }\n            return 0; // return 0 if expected response not received yet\n        }).then((buffer) =\u003e {\n            // complex response detectors receive the whole (consumed) buffer as argument\n        });\n\n\n        // add a notification\n        modem.addNotification('myEventName', /^+CMI=(.*),(.*)/, function(buffer, matches) {\n            modem.addCommand(\"AT+CMR=\"+matches[1], parseInt(matches[2])).then((buf) =\u003e {\n                // buf contains my wanted result\n            });\n        });\n\n\n        modem.addNotification('shutdown', /SHUTDOWN/, function(){\n            modem.close();\n        });\n    });\n\n### Promise based commands\n\nThe `Modem` methods `run`, `addCommand` return a promise that will be resolved/rejected with variable parameters that depend on the (Command)[#command] options.\n\nThe following setup illustrates the differences\n\n    var CommandStates = require('at-commander').CommandStates;\n\n    // please note, it is also possible to call modem.run directly with the arguments as passed to the constructor of command\n    // modem.run thus is just a nice wrapper\n    var myCommand = new ATCommander.Command(cmd, expected);\n    modem.run(myCommand).then(function(result){\n        if (typeof expected === 'undefined' || typeof expected === 'string'){\n            // result is a boolean denoting wether the one-line response matched the expected value\n            // in case expected was undefined, the default response (OK) is assumed\n            // NOTE this will have to be refactored to make it configurable on the fly\n        }\n        if (typeof expected === 'number'){\n            // result will be of type Buffer container the number of bytes as denoted by expected\n        }\n        if (expected instanceof RegExp){\n            // result will be the return value of inBufferString.match(expected)\n        }\n        if (typeof expected === 'function'){\n            // result will be the relevant inBuffer part that was detected using expected\n        }\n\n\n\n    }).catch(function(command){\n        // in case of an error, the given object is an instance of Command\n        // command is the same object as myCommand\n\n        // furthermore several fields will be set:\n\n        switch (command.state){\n\n            case CommandStates.Init:\n                //this state should never occur in an error case\n                break;\n\n            case CommandStates.Rejected:\n                // this state only occurs when passing a command using .run() (or write(), read())\n                // and denotes the situation where the modem is already processing a command\n                // (this is because .run() bypasses the command queue)\n                break;\n\n            case CommandStates.Running:\n                // this state should never occur in an error/catch case\n                // it denotes that the command is being processed by the modem\n                break;\n\n            case CommandStates.Finished:\n                // this state should never occur in an error/catch case\n                // it denotes that the command terminated as configured\n\n                // command.result.buf -\u003e read buffer that satisfied the expected result requirements\n\n                break;\n\n            case CommandStates.Failed:\n                // this state occurs if the commands result processor function returns an undefined value\n                // by default this will also be the case if the expected result is a string type and the read in line\n                // did not match (thus causing a rejection)\n                // note that if you provide result processor functions yourself, you might want to be aware of this (or\n                // make use of it)\n\n                // command.result.buf -\u003e read line that did not match\n\n                break;\n\n            case CommandStates.Timeout:\n                // this state denotes that there was no reply from the attached serial device in the given time constraint\n                // also the contents of the inBuffer will be passed to the command (and consumed from the inBuffer)\n\n                // command.result.buf -\u003e will be a Buffer object\n\n                break;\n\n            case CommandStates.Aborted:\n                // this state denotes that the command was user aborted\n                break;\n        }\n\n\n    });\n\n\n## Classes\n\n### Modem\n\n#### Modem (options)\nSee [setConfig(options)](#setConfig-options).\n\n#### getConfig ()\nReturns config..\n\n#### setConfig (options)\n**_options (optional)_**\n* `parser`: See https://www.npmjs.com/package/serialport#serialport-path-options-opencallback (Note: likely you will never want to change this!)\n* `baudRate`: See https://www.npmjs.com/package/serialport#serialport-path-options-opencallback\n* `dataBits`: See https://www.npmjs.com/package/serialport#serialport-path-options-opencallback\n* `stopBits`: See https://www.npmjs.com/package/serialport#serialport-path-options-opencallback\n* `EOL`: (default: `\"\\r\\n\"`) Command termination string (is added to every normal string type command)\n* `lineRegex`: (default `\"^(.+)\\r\\n\"`) This RegExp is used to detect one-line responses and notifications.\n* `timeout`: (default: `500`) default command timeout in millisec as well as the unsolicited notification timeout\n* `defaultExpectdResult`: (default: `\"OK\"`) Expected result if none given (see run(), addCommand)\n\n#### open (path)\n\n**_path_**\n\nDenotes path to serial port (on linux typically something like `/tty/tty.serialXYZ`, on windows `COM4`)\n\nReturns a promise.\n\n#### isOpen ()\nFacade for https://www.npmjs.com/package/serialport#isopen\n\n#### pause ()\nFacade for https://www.npmjs.com/package/serialport#pause\n\n#### close (callback)\nForces serial shutdown. Facade for https://www.npmjs.com/package/serialport#close-callback\n\n#### closeGracefully (callback)\nIf tries to finish any pending commands before shutting down serial.\n\n\n#### on (event, callback)\nPlease refer to [Events](#events)\n\n#### isProcessingCommands ()\nIf set to true, command queue will be automatically processed.\n\n#### startProcessing ()\nStart automatic processing of command queue.\n\n#### stopProcessing (abortCurrent, callback)\nStop automatic processing of command queue.\n\n**_boolean abortCurrent (optional)_**\n\n**_function callback (optional)_**\n\nCallback to run once abortion completes.\n\n\n#### getPendingCommands ()\nReturns array of pending (Commands)[#command]\n\n#### clearPendingCommands ()\nCleats pending commands list.\n\n#### getCurrentCommand ()\nReturns false if no command is pending at the moment, (Command)[#command] otherwise.\n\n#### abortCurrentCommand ()\n\n#### run (command, expected, options)\n\nIf and only if no other command is currently being processed, runs the given command\n\n**_string|buffer|Command command (required)_**\n\nIf it is a (Command)[#command], any other parameters are ignored, otherwise the string|buffer is used as command to write to the serial.\n\n**_string|number|regex|function expected (optional, default: `OK`)_**\n\n**_object options (optional)_**\n\n* `timeout`: command timeout in msec (if not defined, default of modem is used, see setConfig())\n* `resultProcessor`: result preprocessor, it's result will be considered the processed and final result as passed to promise\n\nReturns a promise.\n\n\n#### addCommand (command, expected, options)\n\nAdds the given command to the pending commands list.\nThe calling semantics are identical to `run(command, expected, callback, processor)`\n\nReturns a promise.\n\n\n#### read (n)\nShortcut helper to `run` a command that just reads n bytes.\nNOTE: after some refactoring initial CR|NL are automatically discarded and will thus never be read. This will likely have to change..\n\n**_number n (required)_**\n\nNumber of bytes to read.\n\nReturns a promise.\n\n\n#### write (buffer)\nShortcut helper to `run` a command that just writes `buffer` to serial and does not wait for a response.\n\n**_Buffer buffer (required)_**\n\nBuffer to write to serial.\n\nReturns a promise.\n\n#### getInBuffer ()\nGet contents of serial in buffer.\n\n#### clearInBuffer ()\nClear contents of serial in buffer.\n\n#### getNotifications ()\nGet array of registered notifications.\n\n#### clearNotifications ()\nClear deregister all notifications.\n\n#### addNotification (notification, regex, handler)\nRegister a new notification.\n\n**_string|Notification notification (required)_**\n\nIn case a [Notification](#notification) is passed the remaining parameters are ignored.\nOtherwise a string to uniquely identify the notification is expected. Will overwrite any previsouly notifications with the same value.\n\n**_RegExp regex (optional)_**\n\nMatching expression that will be looked out for in the buffer to detect any unsolicited incoming data.\n\n**_function handler(Buffer buffer, Array matches) (optional)_**\n\nNotification handler that will be called once `regex` matches incoming data. Will be passed the whole matches buffer and corresponding matches as arguments.\n\n#### removeNotification (name)\nUnregister notification with given name.\n\n\n### Command\n\n    var Command = require('at-commander').Command;\n\n    var myCommand = new Command(command, expected, options);\n\n    modem.run(myCommand); // or\n    modem.addCommand(myCommand);\n\nThe constructor semantics are very much identical to the options of [run(command, expected, options)](#run-command-expected-options) which serves as shortcut.\n\n### Notification\n\n    var Notification = require('at-commander').Notification;\n\n    var myNotification = new Notification(name, regex, handler);\n\n    modem.addNotification(myNotification);\n\nPlease note that [addNotification(notification, regex, handler)](#addNotification-notification-regex-handler) is the friendly shortcut.\n\n## Events\n\nEvent handlers can be set using `Modem.on(eventName, callback)`\n\n### open\nPlease see https://www.npmjs.com/package/serialport#onopen-callback\n\n### close\nhttps://www.npmjs.com/package/serialport#onclose-callback\n\n### data\nPlease see https://www.npmjs.com/package/serialport#ondata-callback\n\n### disconnect\nPlease see https://www.npmjs.com/package/serialport#ondisconnect-callback\n\n### error\nPlease see https://www.npmjs.com/package/serialport#onerror-callback\n\n### notification\nWill be called if any registered notification matches incoming data.\nWARNING: currently disabled, will have to be refactored\n\n### command\nThe command event is triggered if a command _successfully_ completes.\n\n`function callback(Command command, result)`\n\nThe type/contents of `result` is according to the command operations (also see section [Promise based commands](#promise-based-commands)).\nThe most interesting thing about this callback is that it contains the used `Command` object which in particular also has the following interesting properties:\n\n    command.result.buf -\u003e complete accepted response of type Buffer\n    command.result.matches -\u003e if and only if an expected response using a matching mechanism is used: the resulting matches\n    command.result.processed -\u003e if and only if a (default or custom) processor function is passed to the command (will be the same as result)\n\n### discarding\nThe discarding event is triggered if the inBuffer discards data due to a timeout.\n\n`function callback(Buffer buffer)`","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftschiemer%2Fat-commander","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftschiemer%2Fat-commander","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftschiemer%2Fat-commander/lists"}