runline 0.1.0 → 0.2.0

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/README.md CHANGED
@@ -1,273 +1,20 @@
1
- # runline
1
+ # runline
2
2
 
3
- Code mode for agents.
3
+ The runline library and CLI. See the [monorepo README](../../README.md) for the full story, quickstart, and plugin catalog.
4
4
 
5
- Turn any API into a callable action. Install a plugin, write JavaScript, call actions. The code runs in a QuickJS WASM sandbox — no filesystem, no network, just plugin actions via a proxy.
5
+ ## What lives here
6
6
 
7
- ```bash
8
- npm install -g runline
9
- ```
10
-
11
- ## Quick Start
12
-
13
- ```bash
14
- runline init
15
- runline plugin install git:github.com/Michaelliv/runline#plugins/brandfetch
16
- runline connection add bf --plugin brandfetch --set apiKey=xxx
17
-
18
- runline exec 'return await brandfetch.brand.getColors({ domain: "nike.com" })'
19
- # => [{ hex: "#E5E5E5", type: "accent" }, { hex: "#111111", type: "dark" }, ...]
20
- ```
21
-
22
- Agent code runs in a QuickJS sandbox. Each installed plugin is a top-level global — no bracket notation, just dot-chain into resource and action. Plugins execute outside the sandbox with full network access; the agent can only reach APIs through the actions you've installed.
23
-
24
- ```js
25
- // agent writes this
26
- const company = await brandfetch.brand.getCompany({ domain: "stripe.com" });
27
- const deals = await pipedrive.deal.list({ limit: 10 });
28
- const issue = await github.issue.create({
29
- owner: "acme", repo: "api",
30
- title: `New lead: ${company.name}`,
31
- body: `${deals.length} open deals`
32
- });
33
- return { company: company.name, issue: issue.number };
34
- ```
35
-
36
- ## Plugins
37
-
38
- 188 plugins covering popular SaaS, DevOps, and productivity APIs. Each wraps a single service's REST/GraphQL API with typed actions.
39
-
40
- All plugins install via `runline plugin install git:github.com/Michaelliv/runline#plugins/<name>`.
41
-
42
- | Plugin | Actions | Auth |
43
- |--------|---------|------|
44
- | **github** | file/issue/pr/release/repo/review/user CRUD, search | Bearer token |
45
- | **gitlab** | issue/merge request/repo/user CRUD | Bearer token |
46
- | **jira** | issue/project/user CRUD, transitions | Basic auth |
47
- | **slack** | channel/message/user/reaction/star/file ops | Bearer token |
48
- | **discord** | channel/message/member CRUD, reactions | Bot token |
49
- | **notion** | block/database/page/user CRUD, search | Bearer token |
50
- | **todoist** | task/project/section/comment/label CRUD | Bearer token |
51
- | **linear** | issue/project/team/comment CRUD (GraphQL) | Bearer token |
52
- | **hubspot** | contact/company/deal/ticket/engagement CRUD | Bearer token |
53
- | **pipedrive** | deal/person/org/activity/lead/note/product CRUD, search | API token |
54
- | **salesforce** | account/contact/lead/opportunity/case/task CRUD | OAuth2 |
55
- | **shopify** | order/product/customer CRUD | API key |
56
- | **stripe** | charge/customer/source/coupon CRUD | Bearer token |
57
- | **airtable** | base/record CRUD, search, upsert | Bearer token |
58
- | **supabase** | row CRUD | API key |
59
- | **docker** | container/image/volume/network ops | Unix socket |
60
- | **telegram** | message/chat/callback/pin ops | Bot token in URL |
61
- | **twitter** | tweet/user/dm/list ops | OAuth2 Bearer |
62
- | **clickup** | task/list/folder/space/comment/checklist/team CRUD | Bearer token |
63
- | **asana** | task/project/section/subtask/tag/user CRUD | Bearer token |
64
- | **trello** | board/list/card/checklist/attachment/label/member CRUD | API key |
65
- | **monday** | board/group/item/column/update (GraphQL) | Bearer token |
66
- | **mailchimp** | list/member/campaign/tag ops | Bearer token |
67
- | **sendgrid** | contact/list/email ops | Bearer token |
68
- | **elasticsearch** | document/index CRUD | Basic auth |
69
- | **cloudflare** | zone/dns/worker/kv/r2/d1/pages/queue CRUD | Bearer token |
70
- | **databricks** | sql/files/genie/catalog/table/volume/function/vector search | Bearer token |
71
- | **splunk** | search/alert/report/user CRUD | Bearer token |
72
- | **home-assistant** | state/service/history/config/template/event ops | Bearer token |
73
- | **openweathermap** | current/5-day forecast | API key |
74
- | **brandfetch** | logos/colors/fonts/company/industry lookup | Bearer token |
75
-
76
- <details>
77
- <summary>All 188 plugins</summary>
78
-
79
- actionNetwork, activeCampaign, adalo, affinity, agileCrm, airtable, airtop, apiTemplateIo, asana, autopilot, bambooHr, bannerbear, baserow, beeminder, bitly, bitwarden, box, brandfetch, brevo, bubble, chargebee, circleci, ciscoWebex, clearbit, clickup, clockify, cloudflare, cockpit, coda, coingecko, contentful, convertkit, copper, cortex, currents, customerIo, databricks, deepl, demio, dhl, discord, discourse, disqus, docker, drift, dropbox, dropcontact, egoi, elasticsearch, emelia, erpnext, facebookGraph, freshdesk, freshservice, freshworksCrm, getresponse, ghost, github, gitlab, gong, gotify, gotowebinar, grafana, graphql, grist, hackernews, halopsa, harvest, helpscout, highlevel, homeAssistant, hubspot, humanticAi, hunter, intercom, iterable, jenkins, jira, keap, kobotoolbox, lemlist, line, linear, lingvanex, linkedin, lonescale, magento, mailcheck, mailchimp, mailerlite, mailgun, mailjet, mandrill, marketstack, matrix, mattermost, mautic, medium, messagebird, metabase, misp, mocean, monday, monicaCrm, msg91, nasa, netlify, netscalerAdc, nextcloud, nocodb, notion, npm, odoo, okta, oneSimpleApi, onfleet, openThesaurus, openweathermap, oura, paddle, pagerduty, paypal, peekalink, phantombuster, philipsHue, pipedrive, plivo, postbin, posthog, profitwell, pushbullet, pushcut, pushover, quickbase, quickbooks, quickchart, raindrop, reddit, rocketchat, rundeck, salesforce, salesmate, securityScorecard, segment, sendgrid, sendy, sentry, servicenow, shopify, signl4, slack, sms77, splunk, spotify, stackby, storyblok, strapi, strava, stripe, supabase, syncromsp, tapfiliate, telegram, thehive, thehiveProject, todoist, travisci, trello, twake, twilio, twist, twitter, unleashedSoftware, uplead, uproc, uptimerobot, urlscanio, vero, vonage, wekan, woocommerce, wordpress, xero, yourls, zammad, zendesk, zoho, zoom, zulip
80
-
81
- </details>
82
-
83
- ## Examples
84
-
85
- ```bash
86
- # List all available actions
87
- runline actions
88
-
89
- # Get Nike's brand colors
90
- runline exec 'return await brandfetch.brand.getColors({ domain: "nike.com" })'
91
-
92
- # Create a GitHub issue
93
- runline exec '
94
- return await github.issue.create({
95
- owner: "acme", repo: "api",
96
- title: "Bug: login broken",
97
- labels: ["bug", "urgent"]
98
- })
99
- '
100
-
101
- # Search Pipedrive deals
102
- runline exec 'return await pipedrive.deal.search({ term: "Acme" })'
103
-
104
- # Chain actions together
105
- runline exec '
106
- const contact = await hubspot.contact.get({ id: "123" });
107
- const task = await todoist.task.create({
108
- content: `Follow up with ${contact.properties.firstname}`,
109
- priority: 4
110
- });
111
- return { contact: contact.properties.email, taskId: task.id };
112
- '
113
-
114
- # Discover actions from inside the sandbox
115
- runline exec 'return brandfetch.help()'
116
- runline exec 'return help()'
117
-
118
- # Output as JSON (for agents)
119
- runline exec 'return await github.repo.list({ owner: "torvalds" })' --json
120
- ```
121
-
122
- ## Writing a Plugin
123
-
124
- Plugins export a function that receives a `RunlinePluginAPI` and registers actions.
125
-
126
- ```typescript
127
- import type { RunlinePluginAPI } from "runline";
128
-
129
- export default function orders(rl: RunlinePluginAPI) {
130
- rl.setName("orders");
131
- rl.setVersion("1.0.0");
132
-
133
- // Connection config — env vars override config.json values
134
- rl.setConnectionSchema({
135
- apiKey: { type: "string", required: true, env: "ORDERS_API_KEY" },
136
- baseUrl: { type: "string", required: true, env: "ORDERS_BASE_URL" },
137
- });
7
+ - `src/` — the library source (SDK, engine, plugin API, loader, CLI commands)
8
+ - `scripts/` — tooling (e.g. `generate-plugin-table.js` for the root README)
9
+ - `dist/plugins/` — populated at build time by copying `packages/runline-plugins/dist`
138
10
 
139
- rl.registerAction("list", {
140
- description: "List orders for an organization",
141
- inputSchema: {
142
- orgId: { type: "string", required: true },
143
- status: { type: "string", required: false, description: "open, closed, or all" },
144
- limit: { type: "number", required: false },
145
- },
146
- async execute(input, ctx) {
147
- const { orgId, status, limit } = input as Record<string, unknown>;
148
- const url = new URL(`${ctx.connection.config.baseUrl}/orgs/${orgId}/orders`);
149
- if (status) url.searchParams.set("status", status as string);
150
- if (limit) url.searchParams.set("limit", String(limit));
151
-
152
- const res = await fetch(url.toString(), {
153
- headers: { Authorization: `Bearer ${ctx.connection.config.apiKey}` },
154
- });
155
- if (!res.ok) throw new Error(`Orders API ${res.status}: ${await res.text()}`);
156
- return res.json();
157
- },
158
- });
159
-
160
- rl.registerAction("create", {
161
- description: "Create a new order",
162
- inputSchema: {
163
- orgId: { type: "string", required: true },
164
- customer: { type: "string", required: true },
165
- total: { type: "number", required: true },
166
- },
167
- async execute(input, ctx) {
168
- const res = await fetch(`${ctx.connection.config.baseUrl}/orders`, {
169
- method: "POST",
170
- headers: {
171
- Authorization: `Bearer ${ctx.connection.config.apiKey}`,
172
- "Content-Type": "application/json",
173
- },
174
- body: JSON.stringify(input),
175
- });
176
- if (!res.ok) throw new Error(`Orders API ${res.status}: ${await res.text()}`);
177
- return res.json();
178
- },
179
- });
180
- }
181
- ```
182
-
183
- Key points: `execute` runs **outside** the sandbox with full Node.js access (fetch, fs, etc). The sandbox can only call your actions through the proxy. `ctx.connection.config` holds the resolved config with env var overrides applied.
184
-
185
- See [plugins/](plugins/) for 188 real-world examples.
186
-
187
- ## Sandbox
188
-
189
- Agent code runs in a [QuickJS](https://bellard.org/quickjs/) WASM sandbox:
190
-
191
- - **No `fetch`** — network access is only through plugin actions
192
- - **No `fs`** — no filesystem access
193
- - **Timeout** — configurable, kills infinite loops
194
- - **Memory limit** — configurable, prevents OOM
195
- - **`console.log`** — captured and returned in `result.logs`
196
- - **Plugin globals** — each installed plugin is a top-level proxy (e.g. `github`, `slack`, `brandfetch`). Dot-chain into resource and action: `github.issue.create(input)`
197
-
198
- ## For Agents
199
-
200
- Every command supports `--json`. Use `runline actions --json` for full schemas with input types.
201
-
202
- ```bash
203
- runline actions --json # all actions with schemas
204
- runline exec '<code>' --json # structured { result, logs } output
205
- ```
206
-
207
- ## SDK
208
-
209
- ```typescript
210
- import { Runline } from "runline";
211
- import brandfetch from "runline-plugin-brandfetch";
212
-
213
- const rl = Runline.create({
214
- plugins: [brandfetch],
215
- connections: [{ name: "bf", plugin: "brandfetch", config: { apiKey: "xxx" } }],
216
- });
217
-
218
- const result = await rl.execute(`
219
- const colors = await brandfetch.brand.getColors({ domain: "stripe.com" });
220
- return colors.filter(c => c.type === "accent");
221
- `);
222
-
223
- console.log(result.result); // [{ hex: "#635BFF", type: "accent", brightness: 116 }]
224
- ```
225
-
226
- ## CLI Reference
227
-
228
- ```bash
229
- runline exec "<code>" # execute JS in sandbox
230
- runline exec -f ./script.js # execute a file
231
- runline actions # list all actions
232
- runline plugin install <source> # install from git/npm/local
233
- runline plugin list # list installed plugins
234
- runline plugin remove <name> # remove a plugin
235
- runline connection add <n> -p <plugin> -s key=val # add connection
236
- runline connection list # list connections
237
- runline connection remove <name> # remove a connection
238
- runline init # create .runline/ directory
239
- ```
240
-
241
- ## Configuration
242
-
243
- `.runline/config.json`:
244
-
245
- ```json
246
- {
247
- "connections": [
248
- { "name": "gh", "plugin": "github", "config": { "token": "ghp_xxx" } },
249
- { "name": "bf", "plugin": "brandfetch", "config": { "apiKey": "xxx" } }
250
- ],
251
- "timeoutMs": 30000,
252
- "memoryLimitBytes": 67108864
253
- }
254
- ```
255
-
256
- Env vars override config values. Plugins declare env var names in their connection schema (e.g. `GITHUB_TOKEN`).
257
-
258
- ## Development
11
+ ## Scripts
259
12
 
260
13
  ```bash
261
- npm install
262
- npm run dev -- exec 'return 1 + 2'
263
- npm test
264
- npm run check
14
+ bun run dev -- exec 'return 1 + 2' # run the CLI from source
15
+ bun run build # compile + bundle built-in plugins
16
+ bun run test # bun test src/tests
17
+ bun run check # biome check
265
18
  ```
266
19
 
267
- ## How It Relates to dripline
268
-
269
- [dripline](https://github.com/Michaelliv/dripline) is **query mode** — SQL tables over live APIs. runline is **code mode** — JavaScript actions over the same APIs. Same plugin architecture, same connection config, different interface. Use dripline when you want to `SELECT` rows; use runline when you want to `create`, `update`, `delete`, or chain multiple API calls together.
270
-
271
- ## License
272
-
273
- MIT
20
+ `build` does two things: compiles `src/` with `tsc`, then invokes `bun --filter runline-plugins build` and copies the output into `dist/plugins/` so the published package ships with every built-in plugin.
package/dist/sdk.d.ts CHANGED
@@ -28,7 +28,10 @@ export declare class Runline {
28
28
  name: string;
29
29
  version: string;
30
30
  actions: string[];
31
+ connectionConfigSchema?: PluginDef["connectionConfigSchema"];
31
32
  }>;
33
+ /** Return all connections currently configured. */
34
+ connections(): ConnectionConfig[];
32
35
  /**
33
36
  * Load runline from a project directory.
34
37
  * Discovers .runline/ config and installed plugins, just like the CLI.
package/dist/sdk.js CHANGED
@@ -54,8 +54,13 @@ export class Runline {
54
54
  name: p.name,
55
55
  version: p.version,
56
56
  actions: p.actions.map((a) => a.name),
57
+ connectionConfigSchema: p.connectionConfigSchema,
57
58
  }));
58
59
  }
60
+ /** Return all connections currently configured. */
61
+ connections() {
62
+ return [...this._config.connections];
63
+ }
59
64
  /**
60
65
  * Load runline from a project directory.
61
66
  * Discovers .runline/ config and installed plugins, just like the CLI.
package/package.json CHANGED
@@ -1,41 +1,51 @@
1
1
  {
2
2
  "name": "runline",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Code mode for agents — turn any API or command into a callable action",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "exports": {
8
- ".": "./dist/index.js",
9
- "./utils/http": "./dist/utils/http.js",
10
- "./utils/cli": "./dist/utils/cli.js"
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./utils/http": {
14
+ "types": "./dist/utils/http.d.ts",
15
+ "import": "./dist/utils/http.js",
16
+ "default": "./dist/utils/http.js"
17
+ },
18
+ "./utils/cli": {
19
+ "types": "./dist/utils/cli.d.ts",
20
+ "import": "./dist/utils/cli.js",
21
+ "default": "./dist/utils/cli.js"
22
+ }
11
23
  },
12
24
  "bin": {
13
25
  "runline": "./dist/main.js"
14
26
  },
15
27
  "files": [
16
- "dist",
17
- ".pi/extensions"
28
+ "dist"
18
29
  ],
19
- "pi": {
20
- "extensions": [
21
- ".pi/extensions/runline-context"
22
- ]
23
- },
24
30
  "scripts": {
25
- "build": "tsc && tsc -p tsconfig.plugins.json --noCheck",
26
- "prepublishOnly": "npm run build",
27
- "dev": "npx tsx src/main.ts",
28
- "test": "npx tsx --test src/tests/**/*.test.ts",
29
- "format": "npx @biomejs/biome format --write src/",
30
- "lint": "npx @biomejs/biome lint src/",
31
- "check": "npx @biomejs/biome check src/"
31
+ "build": "tsc && bun --filter runline-plugins build && rm -rf dist/plugins && cp -R ../runline-plugins/dist dist/plugins",
32
+ "prepublishOnly": "bun run build",
33
+ "dev": "bun run src/main.ts",
34
+ "test": "bun test src/tests",
35
+ "format": "bunx @biomejs/biome format --write src/",
36
+ "lint": "bunx @biomejs/biome lint src/",
37
+ "check": "bunx @biomejs/biome check src/"
32
38
  },
33
39
  "repository": {
34
40
  "type": "git",
35
41
  "url": "git+https://github.com/Michaelliv/runline.git"
36
42
  },
37
43
  "homepage": "https://github.com/Michaelliv/runline#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/Michaelliv/runline/issues"
46
+ },
38
47
  "keywords": [
48
+ "pi-package",
39
49
  "agent",
40
50
  "tools",
41
51
  "mcp",
@@ -45,8 +55,8 @@
45
55
  "license": "MIT",
46
56
  "devDependencies": {
47
57
  "@biomejs/biome": "^2.3.14",
58
+ "@types/bun": "^1.2.17",
48
59
  "@types/node": "^25.5.0",
49
- "tsx": "^4.21.0",
50
60
  "typescript": "^5.8.0"
51
61
  },
52
62
  "dependencies": {
@@ -1,135 +0,0 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { Markdown, Text } from "@mariozechner/pi-tui";
3
- import { Runline } from "../../../src/sdk.js";
4
-
5
- function formatActions(
6
- actions: Array<{
7
- plugin: string;
8
- action: string;
9
- description?: string;
10
- inputSchema?: Record<string, { type: string; required?: boolean; description?: string }>;
11
- }>,
12
- ): string {
13
- const grouped = new Map<string, typeof actions>();
14
- for (const a of actions) {
15
- const list = grouped.get(a.plugin) ?? [];
16
- list.push(a);
17
- grouped.set(a.plugin, list);
18
- }
19
-
20
- const lines: string[] = [];
21
- for (const [plugin, entries] of grouped) {
22
- lines.push(`### ${plugin}`);
23
- for (const a of entries) {
24
- const inputs = a.inputSchema
25
- ? Object.entries(a.inputSchema)
26
- .map(
27
- ([k, v]) =>
28
- `${k}: ${v.type}${v.required ? "" : "?"}`,
29
- )
30
- .join(", ")
31
- : "";
32
- const sig = inputs
33
- ? `\`${plugin}.${a.action}({ ${inputs} })\``
34
- : `\`${plugin}.${a.action}()\``;
35
- const desc = a.description ? ` — ${a.description}` : "";
36
- lines.push(`- ${sig}${desc}`);
37
- }
38
- lines.push("");
39
- }
40
-
41
- return lines.join("\n");
42
- }
43
-
44
- export default function (pi: ExtensionAPI) {
45
- pi.registerMessageRenderer(
46
- "runline-context",
47
- (message, { expanded }, theme) => {
48
- if (!expanded) {
49
- const label = theme.fg("customMessageLabel", "⚡ runline actions");
50
- const hint = theme.fg("dim", " — Ctrl+O to expand");
51
- return new Text(label + hint, 1, 0);
52
- }
53
- return new Markdown(
54
- message.content,
55
- 1,
56
- 0,
57
- {
58
- heading: (t) => theme.fg("mdHeading", t),
59
- link: (t) => theme.fg("mdLink", t),
60
- linkUrl: (t) => theme.fg("mdLinkUrl", t),
61
- code: (t) => theme.fg("mdCode", t),
62
- codeBlock: (t) => theme.fg("mdCodeBlock", t),
63
- codeBlockBorder: (t) => theme.fg("mdCodeBlockBorder", t),
64
- quote: (t) => theme.fg("mdQuote", t),
65
- quoteBorder: (t) => theme.fg("mdQuoteBorder", t),
66
- hr: (t) => theme.fg("mdHr", t),
67
- listBullet: (t) => theme.fg("mdListBullet", t),
68
- bold: (t) => theme.bold(t),
69
- italic: (t) => theme.italic(t),
70
- strikethrough: (t) => theme.strikethrough(t),
71
- underline: (t) => theme.underline(t),
72
- },
73
- { color: (t) => theme.fg("customMessageText", t) },
74
- );
75
- },
76
- );
77
-
78
- pi.on("session_start", async (_event, ctx) => {
79
- const rl = await Runline.fromProject(ctx.cwd);
80
-
81
- if (!rl) {
82
- if (ctx.hasUI) {
83
- ctx.ui.setStatus("runline", ctx.ui.theme.fg("dim", "runline: no .runline/"));
84
- }
85
- return;
86
- }
87
-
88
- const actions = rl.actions();
89
- const plugins = rl.plugins();
90
-
91
- if (actions.length === 0) {
92
- if (ctx.hasUI) {
93
- ctx.ui.setStatus("runline", ctx.ui.theme.fg("dim", "runline: no plugins"));
94
- }
95
- return;
96
- }
97
-
98
- // Check if already injected
99
- const alreadyInjected = ctx.sessionManager
100
- .getEntries()
101
- .some(
102
- (e: any) =>
103
- e.type === "message" &&
104
- e.message.role === "custom" &&
105
- e.message.customType === "runline-context",
106
- );
107
-
108
- if (!alreadyInjected) {
109
- const header =
110
- "## Runline actions\n\n" +
111
- "This project has runline installed. You can execute JavaScript in a sandbox " +
112
- "where each installed plugin is a top-level global. Chain actions together, " +
113
- "call `help()` or `pluginName.help()` inside the sandbox for discovery.\n\n" +
114
- `**${plugins.length} plugins, ${actions.length} actions available.**\n\n` +
115
- "Use `runline exec '<code>'` to run code. Examples:\n" +
116
- "```js\n" +
117
- "return await github.issue.create({ owner: \"acme\", repo: \"api\", title: \"Bug\" })\n" +
118
- "```\n\n";
119
-
120
- ctx.sessionManager.appendCustomMessageEntry(
121
- "runline-context",
122
- header + formatActions(actions),
123
- true,
124
- );
125
- }
126
-
127
- if (ctx.hasUI) {
128
- const theme = ctx.ui.theme;
129
- ctx.ui.setStatus(
130
- "runline",
131
- `⚡${theme.fg("dim", ` runline: ${plugins.length} plugins, ${actions.length} actions`)}`,
132
- );
133
- }
134
- });
135
- }
@@ -1,17 +0,0 @@
1
- {
2
- "name": "@miclivs/pi-runline-context",
3
- "version": "0.1.0",
4
- "description": "Pi extension — injects runline actions into agent context",
5
- "type": "commonjs",
6
- "keywords": ["pi-package"],
7
- "files": ["index.ts", "README.md"],
8
- "pi": {
9
- "extensions": ["."]
10
- },
11
- "peerDependencies": {
12
- "@mariozechner/pi-coding-agent": "*",
13
- "@mariozechner/pi-tui": "*"
14
- },
15
- "author": "michaelliv",
16
- "license": "MIT"
17
- }