gumdrops-agents 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/Readme.md +61 -0
- package/docs/agent-definitions.md +171 -0
- package/host/dist/agentfile.js +418 -0
- package/host/dist/api.js +3 -0
- package/host/dist/avatar.js +182 -0
- package/host/dist/cli.js +594 -0
- package/host/dist/cron.js +131 -0
- package/host/dist/db.js +666 -0
- package/host/dist/definition.js +114 -0
- package/host/dist/diff.js +49 -0
- package/host/dist/discovery.js +100 -0
- package/host/dist/events.js +139 -0
- package/host/dist/extension.js +151 -0
- package/host/dist/folders.js +27 -0
- package/host/dist/history.js +478 -0
- package/host/dist/host.js +1161 -0
- package/host/dist/mcp.js +387 -0
- package/host/dist/migrate.js +289 -0
- package/host/dist/models.js +58 -0
- package/host/dist/network.js +52 -0
- package/host/dist/plugins.js +229 -0
- package/host/dist/processes.js +366 -0
- package/host/dist/requests.js +358 -0
- package/host/dist/routes.js +328 -0
- package/host/dist/rpc.js +243 -0
- package/host/dist/runners.js +139 -0
- package/host/dist/schedules.js +122 -0
- package/host/dist/secrets.js +70 -0
- package/host/dist/selfmanage.js +106 -0
- package/host/dist/service.js +231 -0
- package/host/dist/session.js +239 -0
- package/host/dist/settings.js +41 -0
- package/host/dist/templates.js +133 -0
- package/host/dist/tools.js +136 -0
- package/host/dist/transcript.js +240 -0
- package/host/dist/types.js +2 -0
- package/host/dist/util.js +27 -0
- package/host/dist/webui.js +82 -0
- package/host/package.json +3 -0
- package/host/plugins/gumdrops/.claude-plugin/plugin.json +8 -0
- package/host/plugins/gumdrops/AGENT.md +20 -0
- package/host/plugins/gumdrops/avatars/bean.svg +28 -0
- package/host/plugins/gumdrops/avatars/cherries.svg +29 -0
- package/host/plugins/gumdrops/avatars/cookie.svg +27 -0
- package/host/plugins/gumdrops/avatars/cushion.svg +29 -0
- package/host/plugins/gumdrops/avatars/floss.svg +28 -0
- package/host/plugins/gumdrops/avatars/gumdrop.svg +29 -0
- package/host/plugins/gumdrops/avatars/icecream.svg +29 -0
- package/host/plugins/gumdrops/avatars/index.json +86 -0
- package/host/plugins/gumdrops/avatars/lollipop.svg +29 -0
- package/host/plugins/gumdrops/avatars/matte.svg +27 -0
- package/host/plugins/gumdrops/avatars/redbean.svg +28 -0
- package/host/plugins/gumdrops/avatars/round.svg +30 -0
- package/host/plugins/gumdrops/avatars/softserve.svg +29 -0
- package/host/plugins/gumdrops/avatars/sprinkle.svg +28 -0
- package/host/plugins/gumdrops/avatars/striped.svg +28 -0
- package/host/plugins/gumdrops/skills/avatar-design/SKILL.md +71 -0
- package/host/plugins/gumdrops/skills/create-agent/SKILL.md +100 -0
- package/host/plugins/gumdrops/skills/plugins/SKILL.md +73 -0
- package/package.json +70 -4
- package/web/dist/assets/Markdown-BXlyLSg3.js +30 -0
- package/web/dist/assets/bean-CoCOAJYg.js +30 -0
- package/web/dist/assets/cherries-RYwOI5KA.js +31 -0
- package/web/dist/assets/cookie-Bjr8423M.js +29 -0
- package/web/dist/assets/cushion-DaZMCcdE.js +31 -0
- package/web/dist/assets/floss-B4EXnD8R.js +30 -0
- package/web/dist/assets/gumdrop-B4lGdWXD.js +31 -0
- package/web/dist/assets/hljs-ClMkEmJs.js +4 -0
- package/web/dist/assets/icecream-B12iGUNf.js +31 -0
- package/web/dist/assets/index-BATPLBVX.css +1 -0
- package/web/dist/assets/index-LYfD_sqJ.js +34 -0
- package/web/dist/assets/lollipop-GTYPG8gC.js +31 -0
- package/web/dist/assets/matte-D-K2uety.js +29 -0
- package/web/dist/assets/redbean-OdJ2m0uu.js +30 -0
- package/web/dist/assets/rolldown-runtime-QTnfLwEv.js +1 -0
- package/web/dist/assets/round-D_Dzgq51.js +32 -0
- package/web/dist/assets/softserve-Cy3qze6G.js +31 -0
- package/web/dist/assets/sprinkle-BOLQPON-.js +30 -0
- package/web/dist/assets/striped-Bo0Ks1TI.js +30 -0
- package/web/dist/index.html +16 -0
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrew
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/Readme.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Gum Drops
|
|
2
|
+
|
|
3
|
+
Gum Drops runs persistent [Pi](https://pi.dev/) coding agents on your computer and gives you a browser UI for them.
|
|
4
|
+
|
|
5
|
+
An agent is a folder with an `AGENT.md` and a `memory.md`. It keeps one ongoing conversation, splits focused work into threads, remembers what it learns, and can run on a schedule. A small host process runs the agents and serves the UI. It keeps running when you close the browser.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Requires macOS or Linux and Node.js 22.19 or later.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm i -g gumdrops-agents
|
|
13
|
+
gumdrops setup
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`setup` registers the host as a background service (launchd on macOS, a systemd user unit on Linux), starts it, helps you sign Pi in to a model provider, and opens the UI. Run `gumdrops open` to open the UI again.
|
|
17
|
+
|
|
18
|
+
Manage the service:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
gumdrops service status
|
|
22
|
+
gumdrops service stop
|
|
23
|
+
gumdrops service start
|
|
24
|
+
gumdrops service uninstall # stop running at login
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`setup` and `service` take `--dry-run` to print the files and commands without running them. The host log is `host.log` in the host data folder (`~/Library/Application Support/GumDrops/host` on macOS, `~/.local/share/gumdrops/host` on Linux).
|
|
28
|
+
|
|
29
|
+
## Agents
|
|
30
|
+
|
|
31
|
+
Agents live in `~/.gumdrops/agents/<name>/` or in a project's `.gumdrops/agents/<name>/`. Create one in the UI, or in a project with:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
gumdrops init
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
An agent edits its own folder and commits its changes with Git. Every run is snapshotted on a hidden Git ref, so you can inspect and revert changes without touching your branch. See [agent definitions](docs/agent-definitions.md) and [host commands](host/README.md).
|
|
38
|
+
|
|
39
|
+
## Remote access
|
|
40
|
+
|
|
41
|
+
The host only listens on this computer by default. To use it from your phone or another machine, put both on a private network such as [Tailscale](https://tailscale.com/) and bind the host to the Tailscale address:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
gumdrops setup --bind <tailscale ip> --allow-host <machine>.<tailnet>.ts.net
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The same flags work with `gumdrops serve` in the foreground. Then run `gumdrops open --host <machine>.<tailnet>.ts.net` and open the printed URL on the other device. The host can run commands on this machine, so never expose it to the public internet.
|
|
48
|
+
|
|
49
|
+
Want this deployed in your own environment? Get in touch.
|
|
50
|
+
|
|
51
|
+
## Development
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
npm install
|
|
55
|
+
./run.sh # build host and web, run the host in the foreground
|
|
56
|
+
node host/dist/cli.js open # in another terminal
|
|
57
|
+
npm run check # host and web checks
|
|
58
|
+
npm run web:e2e # browser smoke test against a built host and a fake Pi
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`npm run web:build` builds the UI into `web/dist`, which the host serves. Set `GUMDROPS_HOST_DATA_DIR`, `GUMDROPS_CONFIG_DIR` and `GUMDROPS_HOST_PORT` to run a separate host for testing. The e2e test needs Playwright's Chromium once: `npx playwright install chromium`.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Agent definitions
|
|
2
|
+
|
|
3
|
+
An agent is a folder. The folder is the source of truth.
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
~/.gumdrops/agents/<name>/ global
|
|
7
|
+
<project>/.gumdrops/agents/<name>/ project
|
|
8
|
+
├── AGENT.md settings and role
|
|
9
|
+
├── memory.md memory between runs
|
|
10
|
+
├── mcp.json optional MCP servers
|
|
11
|
+
├── skills/<name>/SKILL.md optional skills
|
|
12
|
+
└── avatar.svg optional face; initials without it
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The host runs the definition that is on disk. Changes take effect on the next run. The host reloads definitions when these files, or installed plugins, change. The browser UI's inspector edits the model, thinking, role, memory and schedules; each save is a Git commit by you.
|
|
16
|
+
|
|
17
|
+
Create an agent in the UI with just a folder and a name. Its role then defaults to helping with the work in that folder.
|
|
18
|
+
|
|
19
|
+
The default `~/.gumdrops` directory is a Git repository. Set `GUMDROPS_CONFIG_DIR` to use another global root. Register a project with the host before using its agents.
|
|
20
|
+
|
|
21
|
+
## AGENT.md
|
|
22
|
+
|
|
23
|
+
`AGENT.md` has YAML front matter and a required Markdown role body:
|
|
24
|
+
|
|
25
|
+
```md
|
|
26
|
+
---
|
|
27
|
+
name: Release manager
|
|
28
|
+
model: openai-codex/gpt-5.6-luna
|
|
29
|
+
thinking: high
|
|
30
|
+
workspace: ~/Code/app
|
|
31
|
+
schedule:
|
|
32
|
+
- name: morning
|
|
33
|
+
cron: "0 9 * * 1-5"
|
|
34
|
+
message: Check for new issues.
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
Prepare releases and report blockers.
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Unknown front matter keys are errors. `workspace` resolves from the agent folder. `instructions` resolves from the workspace. `skills`, `extensions`, and MCP `cwd` resolve from the agent folder.
|
|
41
|
+
|
|
42
|
+
### Front matter keys
|
|
43
|
+
|
|
44
|
+
| Key | Meaning, default, and validation |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `name` | Display name; defaults to the folder name; a supplied value must be a non-empty string. |
|
|
47
|
+
| `title` | Optional non-empty title. |
|
|
48
|
+
| `description` | Optional non-empty description. |
|
|
49
|
+
| `icon` | Optional non-empty icon value. |
|
|
50
|
+
| `model` | Model in `provider/name` form; defaults to `openai-codex/gpt-5.6-luna`. |
|
|
51
|
+
| `thinking` | Optional level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. |
|
|
52
|
+
| `workspace` | Workspace directory; defaults to `defaultWorkspace`; the directory must exist. |
|
|
53
|
+
| `instructions` | List of existing instruction files, resolved from the workspace; defaults to an empty list. |
|
|
54
|
+
| `tools` | Optional list of Pi built-in tools: `read`, `bash`, `edit`, `write`, `grep`, `find`, or `ls`. Omit it for Pi defaults. |
|
|
55
|
+
| `skills` | Additional existing skill paths, resolved from the agent folder. Folder skills are also included automatically. |
|
|
56
|
+
| `plugins` | List of Claude plugin names. See [Plugins](#plugins). |
|
|
57
|
+
| `extensions` | List of existing extension paths, resolved from the agent folder; defaults to an empty list. |
|
|
58
|
+
| `mcp` | Inline MCP server map; defaults to an empty map. Each server needs a command or an HTTP(S) URL; `type` (`stdio` or `http`) is optional. |
|
|
59
|
+
| `delegation` | `true`, `false`, or a map with `enabled`, positive integer `maxChildren`, and optional child `model` and `thinking`. Enabled by default. |
|
|
60
|
+
| `collaboration` | `true`, `false`, or a map. It defaults to enabled, contacts `[*]`, and `acceptTasks: true`. |
|
|
61
|
+
| `schedule` | List of schedules with valid `cron` and `message` values. Names must be unique. |
|
|
62
|
+
| `timeout` | Positive seconds or a duration such as `30ms`, `30s`, `45m`, `2h`, or `1d`. |
|
|
63
|
+
|
|
64
|
+
## MCP servers and secrets
|
|
65
|
+
|
|
66
|
+
Every agent gets the host-wide servers in `~/.gumdrops/mcp.json`, which you manage in the UI under Settings. An agent's own `mcp.json` is optional and has no UI. Both accept a plain server map or `{ "mcpServers": { ... } }`. An agent's `mcp.json` merges with its `mcp` key (duplicate names are errors), and its servers win over host-wide ones with the same name.
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"github": {
|
|
71
|
+
"command": "npx",
|
|
72
|
+
"args": ["-y", "@modelcontextprotocol/server-github"],
|
|
73
|
+
"env": { "GITHUB_TOKEN": { "secretRef": "secret:GITHUB_TOKEN" } }
|
|
74
|
+
},
|
|
75
|
+
"docs": {
|
|
76
|
+
"url": "https://example.com/mcp",
|
|
77
|
+
"headers": { "Authorization": { "secretRef": "secret:DOCS_KEY", "prefix": "Bearer " } }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
An `env` or `headers` value is literal text or a `secretRef`:
|
|
83
|
+
|
|
84
|
+
- `secret:NAME`: the host's secret store, `secrets.json` (mode 0600) in the host data folder. Set values in Settings; the UI never shows them again.
|
|
85
|
+
- `env:NAME`: the host's environment. Under launchd or systemd this is not your shell's environment, so prefer `secret:`.
|
|
86
|
+
- `keychain:SERVICE[/ACCOUNT]`: the macOS Keychain.
|
|
87
|
+
|
|
88
|
+
A server with `oauth` (as in most plugin connectors) is accepted but skipped when the agent runs, with an activity note: OAuth sign-in isn't supported yet.
|
|
89
|
+
|
|
90
|
+
## Plugins
|
|
91
|
+
|
|
92
|
+
`plugins: [marketing]` gives the agent a [Claude plugin](https://github.com/anthropics/knowledge-work-plugins)'s skills, commands and MCP servers, used as they are. The host looks for `~/.gumdrops/plugins/<name>/` first, then for the plugins bundled with it (`gumdrops`). An unknown or unreadable plugin is a definition error.
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
~/.gumdrops/plugins/<name>/
|
|
96
|
+
├── .claude-plugin/plugin.json name, version, description
|
|
97
|
+
├── .mcp.json { "mcpServers": { ... } }
|
|
98
|
+
├── skills/<name>/SKILL.md skills
|
|
99
|
+
├── commands/*.md single-file skills; they work the same
|
|
100
|
+
├── agents/*.md listed, not used yet
|
|
101
|
+
└── hooks/ ignored
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- `${CLAUDE_PLUGIN_ROOT}` in `.mcp.json` (commands, args, env, cwd, url) is the plugin folder. The agent's prompt says where each plugin is, for skills that mention it.
|
|
105
|
+
- The agent's own skills and MCP servers win over a plugin's with the same name, and earlier plugins win over later ones.
|
|
106
|
+
- The inspector lists each plugin's skills and servers, with notes on what is skipped: hooks, sub-agents, OAuth servers, name clashes.
|
|
107
|
+
- Customise a plugin by editing the installed copy; every agent that uses it sees the change. Editing, adding or removing a plugin reloads the agents.
|
|
108
|
+
|
|
109
|
+
Install, customise and update plugins by asking the Gum Drops agent (below), or by hand: copy the plugin folder into `~/.gumdrops/plugins/<name>/`.
|
|
110
|
+
|
|
111
|
+
## Avatars
|
|
112
|
+
|
|
113
|
+
`avatar.svg` in the agent folder is its face in the UI. The UI loads it only with `<img>`, idle by default, `<file>#working` while the agent runs and `<file>#still` in lists. The `gumdrops` plugin ships 14 designs in `host/plugins/gumdrops/avatars/`; copy one and change its `--avatar-color`.
|
|
114
|
+
|
|
115
|
+
The file must have `<svg viewBox="0 0 100 110">`, its main colour in `--avatar-color`, and its CSS and animations in one `<style>` block, with `:target` rules for `#working` and `#still` and no motion under `prefers-reduced-motion`. It may not use `<script>`, `<foreignObject>`, images, SMIL animation, `on*` attributes, fonts, `@import`, or `href` and `url()` to anything but `#id` in the file, and must be at most 64 KB. A file that breaks a rule shows initials, with the error in the inspector.
|
|
116
|
+
|
|
117
|
+
## The Gum Drops agent
|
|
118
|
+
|
|
119
|
+
`gumdrops setup` creates `~/.gumdrops/agents/gumdrops/` when there are no agents yet. It uses the `gumdrops` plugin and works in `~/.gumdrops`, so it can write agents and plugins: ask it for "a marketing agent for ~/Code/site from knowledge-work-plugins". Its skills, in `host/plugins/gumdrops/skills/`, describe the folder layout, every key, plugin marketplaces, and avatars.
|
|
120
|
+
|
|
121
|
+
## Identity and memory
|
|
122
|
+
|
|
123
|
+
The absolute `AGENT.md` path is the agent identity. The database maps that path to an ID. Gum Drops does not write IDs into files. Renaming or moving a folder creates a new agent. The old agent becomes `missing`.
|
|
124
|
+
|
|
125
|
+
`memory.md` is user-editable. The `remember` tool appends dated notes and creates a `# Memory` header when needed. Memory writes enter history. The last 4,000 bytes appear in the prompt under **Your memory**.
|
|
126
|
+
|
|
127
|
+
## Change definitions
|
|
128
|
+
|
|
129
|
+
An agent with file tools edits its own folder directly. It can create an agent by adding `<agentsDir>/<name>/AGENT.md`. It can retire an agent by deleting its folder.
|
|
130
|
+
|
|
131
|
+
After changing a definition, commit the folder with Git:
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
git add <folder>
|
|
135
|
+
git commit -m "why"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Committing is best practice, not a host requirement. The UI and CLI warn when a definition folder has uncommitted changes. The warning excludes `memory.md`.
|
|
139
|
+
|
|
140
|
+
## Git history
|
|
141
|
+
|
|
142
|
+
Gum Drops snapshots definition changes and every root agent run's workspace changes.
|
|
143
|
+
|
|
144
|
+
- Every run is snapshotted on a hidden ref, so changes can be seen and reverted.
|
|
145
|
+
- For a folder or workspace inside a real Git repository, Gum Drops uses a private index and a hidden `refs/gumdrops/...` ref.
|
|
146
|
+
- It never changes `HEAD`, the user's index, branch, or working tree.
|
|
147
|
+
- A folder outside Git uses a shadow repository in the host data directory.
|
|
148
|
+
- A non-Git workspace has no run snapshot.
|
|
149
|
+
|
|
150
|
+
Inspect history with:
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
git log refs/gumdrops/history
|
|
154
|
+
gumdrops history <agent> [<commit>]
|
|
155
|
+
gumdrops log <agent> [--limit <n>]
|
|
156
|
+
gumdrops revert <agent> <commit>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The API provides definition history at `/v1/agents/:id/history` and workspace history at `/v1/agents/:id/changes`. History includes diffs and restore or revert actions.
|
|
160
|
+
|
|
161
|
+
## Migration and commands
|
|
162
|
+
|
|
163
|
+
Run `gumdrops migrate` to convert old YAML definitions in `<configDir>/agents` and project `.gumdrops/agents` folders. Migration creates agent folders, keeps IDs, moves lessons into `memory.md`, updates database paths, and moves each YAML file into `.migrated/`. Stop the host before migration.
|
|
164
|
+
|
|
165
|
+
```sh
|
|
166
|
+
gumdrops init [dir] [--name <name>]
|
|
167
|
+
gumdrops migrate
|
|
168
|
+
gumdrops history <agent> [<commit>]
|
|
169
|
+
gumdrops log <agent> [--limit <n>]
|
|
170
|
+
gumdrops revert <agent> <commit>
|
|
171
|
+
```
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/* An agent is a folder. AGENT.md holds its settings in front matter and its role in the
|
|
2
|
+
body. The agent keeps what it learns in memory.md beside it. The folder is the source of
|
|
3
|
+
truth for configuration; the host database keeps runtime state. */
|
|
4
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
7
|
+
import { Document, isMap, parseDocument, stringify } from "yaml";
|
|
8
|
+
import { AVATAR_FILE, loadAvatar } from "./avatar.js";
|
|
9
|
+
import { parseCron } from "./cron.js";
|
|
10
|
+
import { forRuntime, MCP_SERVER_NAME, mcpServerEntry } from "./mcp.js";
|
|
11
|
+
import { loadPlugin, pluginFiles, skillName } from "./plugins.js";
|
|
12
|
+
export const AGENT_FILE = "AGENT.md";
|
|
13
|
+
export const THINKING_LEVELS = [
|
|
14
|
+
"off",
|
|
15
|
+
"minimal",
|
|
16
|
+
"low",
|
|
17
|
+
"medium",
|
|
18
|
+
"high",
|
|
19
|
+
"xhigh",
|
|
20
|
+
"max",
|
|
21
|
+
];
|
|
22
|
+
export const BUILTIN_TOOLS = [
|
|
23
|
+
"read",
|
|
24
|
+
"bash",
|
|
25
|
+
"edit",
|
|
26
|
+
"write",
|
|
27
|
+
"grep",
|
|
28
|
+
"find",
|
|
29
|
+
"ls",
|
|
30
|
+
];
|
|
31
|
+
export const DEFAULT_MODEL = "openai-codex/gpt-5.6-luna";
|
|
32
|
+
export const DEFAULT_COLLABORATION = { enabled: true, contacts: ["*"], acceptTasks: true };
|
|
33
|
+
/** Every front matter key AGENT.md accepts. The role is the body. */
|
|
34
|
+
const KEYS = [
|
|
35
|
+
"name",
|
|
36
|
+
"title",
|
|
37
|
+
"description",
|
|
38
|
+
"icon",
|
|
39
|
+
"model",
|
|
40
|
+
"thinking",
|
|
41
|
+
"workspace",
|
|
42
|
+
"instructions",
|
|
43
|
+
"tools",
|
|
44
|
+
"skills",
|
|
45
|
+
"plugins",
|
|
46
|
+
"extensions",
|
|
47
|
+
"mcp",
|
|
48
|
+
"delegation",
|
|
49
|
+
"collaboration",
|
|
50
|
+
"schedule",
|
|
51
|
+
"timeout",
|
|
52
|
+
];
|
|
53
|
+
const isObj = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
54
|
+
export const expandHome = (path) => path === "~" ? homedir() : path.startsWith("~/") ? join(homedir(), path.slice(2)) : path;
|
|
55
|
+
const absolute = (path, base) => resolve(base, expandHome(path));
|
|
56
|
+
export function parseDuration(value, field) {
|
|
57
|
+
if (typeof value === "number" && value > 0)
|
|
58
|
+
return value * 1000;
|
|
59
|
+
const match = typeof value === "string" ? value.trim().match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/) : null;
|
|
60
|
+
if (!match)
|
|
61
|
+
throw new Error(`${field} must be a duration such as 30s, 45m, 2h or 1d`);
|
|
62
|
+
const unit = { ms: 1, s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }[match[2]];
|
|
63
|
+
return Math.round(Number(match[1]) * unit);
|
|
64
|
+
}
|
|
65
|
+
const FRONT_MATTER = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
66
|
+
/** Splits AGENT.md into its front matter (as a YAML document) and its body. */
|
|
67
|
+
function split(source) {
|
|
68
|
+
const match = source.match(FRONT_MATTER);
|
|
69
|
+
const document = parseDocument(match ? match[1] : "");
|
|
70
|
+
if (document.errors.length)
|
|
71
|
+
throw new Error(`Front matter: ${document.errors[0].message}`);
|
|
72
|
+
const value = document.toJS() ?? {};
|
|
73
|
+
if (!isObj(value))
|
|
74
|
+
throw new Error("Front matter must be a YAML mapping");
|
|
75
|
+
return { document, data: value, body: (match ? source.slice(match[0].length) : source).trim() };
|
|
76
|
+
}
|
|
77
|
+
/** AGENT.md's front matter as plain values. Throws on bad YAML. */
|
|
78
|
+
export const frontMatterOf = (source) => split(source).data;
|
|
79
|
+
/** AGENT.md text from front matter and a role. */
|
|
80
|
+
export function agentMarkdown(data, role) {
|
|
81
|
+
const front = Object.keys(data).length ? `---\n${stringify(data)}---\n\n` : "";
|
|
82
|
+
return `${front}${role.trim()}\n`;
|
|
83
|
+
}
|
|
84
|
+
/** The project that owns a folder under `<project>/.gumdrops/agents`, if any. */
|
|
85
|
+
export function projectOf(folder) {
|
|
86
|
+
const agents = dirname(folder);
|
|
87
|
+
return basename(agents) === "agents" && basename(dirname(agents)) === ".gumdrops"
|
|
88
|
+
? dirname(dirname(agents))
|
|
89
|
+
: null;
|
|
90
|
+
}
|
|
91
|
+
/** The folder that holds an agent's `agents` folder: `~/.gumdrops` or `<project>/.gumdrops`. */
|
|
92
|
+
export const definitionRootOf = (folder) => dirname(dirname(folder));
|
|
93
|
+
function strings(value, field) {
|
|
94
|
+
if (value === undefined)
|
|
95
|
+
return [];
|
|
96
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string"))
|
|
97
|
+
throw new Error(`${field} must be a list of strings`);
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
function existing(paths, field) {
|
|
101
|
+
for (const path of paths)
|
|
102
|
+
if (!existsSync(path))
|
|
103
|
+
throw new Error(`${field}: ${path} does not exist`);
|
|
104
|
+
return paths;
|
|
105
|
+
}
|
|
106
|
+
function text(value, field, required = false) {
|
|
107
|
+
if (value === undefined && !required)
|
|
108
|
+
return undefined;
|
|
109
|
+
if (typeof value !== "string" || !value.trim())
|
|
110
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
function thinking(value, field) {
|
|
114
|
+
if (value === undefined)
|
|
115
|
+
return undefined;
|
|
116
|
+
if (!THINKING_LEVELS.includes(value))
|
|
117
|
+
throw new Error(`${field} must be one of ${THINKING_LEVELS.join(", ")}`);
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
/** "provider/name"; the name may itself contain slashes. */
|
|
121
|
+
function model(value, field) {
|
|
122
|
+
const raw = text(value, field);
|
|
123
|
+
if (raw === undefined)
|
|
124
|
+
return undefined;
|
|
125
|
+
const slash = raw.indexOf("/");
|
|
126
|
+
if (slash <= 0 || slash === raw.length - 1)
|
|
127
|
+
throw new Error(`${field} must look like provider/model, such as ${DEFAULT_MODEL}`);
|
|
128
|
+
return { provider: raw.slice(0, slash).trim(), name: raw.slice(slash + 1).trim() };
|
|
129
|
+
}
|
|
130
|
+
function mcpServers(value, source, base) {
|
|
131
|
+
if (value === undefined)
|
|
132
|
+
return {};
|
|
133
|
+
if (!isObj(value))
|
|
134
|
+
throw new Error(`${source} must be a mapping of server name to server`);
|
|
135
|
+
const servers = {};
|
|
136
|
+
for (const [server, entry] of Object.entries(value)) {
|
|
137
|
+
const field = `${source}.${server}`;
|
|
138
|
+
if (!MCP_SERVER_NAME.test(server))
|
|
139
|
+
throw new Error(`${field}: server names may use letters, digits, _ and -`);
|
|
140
|
+
servers[server] = forRuntime(mcpServerEntry(entry, field), base);
|
|
141
|
+
}
|
|
142
|
+
return servers;
|
|
143
|
+
}
|
|
144
|
+
/** mcp.json in the agent folder, in the shape other MCP clients use. */
|
|
145
|
+
function mcpFile(folder) {
|
|
146
|
+
const path = join(folder, "mcp.json");
|
|
147
|
+
if (!existsSync(path))
|
|
148
|
+
return {};
|
|
149
|
+
let value;
|
|
150
|
+
try {
|
|
151
|
+
value = JSON.parse(readFileSync(path, "utf8"));
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
throw new Error(`mcp.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
155
|
+
}
|
|
156
|
+
return mcpServers(isObj(value) && isObj(value.mcpServers) ? value.mcpServers : value, "mcp.json", folder);
|
|
157
|
+
}
|
|
158
|
+
/** Skills in the folder's skills/ directory: every subfolder with a SKILL.md. */
|
|
159
|
+
function folderSkills(folder) {
|
|
160
|
+
const skills = join(folder, "skills");
|
|
161
|
+
try {
|
|
162
|
+
return readdirSync(skills)
|
|
163
|
+
.sort()
|
|
164
|
+
.map((name) => join(skills, name))
|
|
165
|
+
.filter((path) => existsSync(join(path, "SKILL.md")));
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Adds the plugins' skills, commands and MCP servers to the agent's own. The agent's own win on a
|
|
173
|
+
* name clash, then earlier plugins; each loser is a note on its plugin.
|
|
174
|
+
*/
|
|
175
|
+
function withPlugins(names, configDir, skills, mcp) {
|
|
176
|
+
const taken = new Set(skills.map(skillName));
|
|
177
|
+
const servers = {};
|
|
178
|
+
const commands = [];
|
|
179
|
+
const plugins = [];
|
|
180
|
+
const pluginSkills = [];
|
|
181
|
+
for (const name of new Set(names)) {
|
|
182
|
+
const { skillFolders, commands: pluginCommands, mcp: pluginServers, ...summary } = loadPlugin(name, configDir);
|
|
183
|
+
const notes = [...summary.notes];
|
|
184
|
+
const used = { skills: [], mcpServers: [] };
|
|
185
|
+
const claim = (skill) => {
|
|
186
|
+
if (taken.has(skill))
|
|
187
|
+
notes.push(`Skill ${skill} is already defined, so this plugin's is not used`);
|
|
188
|
+
else
|
|
189
|
+
taken.add(skill), used.skills.push(skill);
|
|
190
|
+
return used.skills.includes(skill);
|
|
191
|
+
};
|
|
192
|
+
for (const skill of skillFolders)
|
|
193
|
+
if (claim(skill.name))
|
|
194
|
+
pluginSkills.push(skill.path);
|
|
195
|
+
for (const command of pluginCommands)
|
|
196
|
+
if (claim(command.name))
|
|
197
|
+
commands.push(command);
|
|
198
|
+
for (const [server, entry] of Object.entries(pluginServers))
|
|
199
|
+
if (mcp[server] || servers[server])
|
|
200
|
+
notes.push(`MCP server ${server} is already defined, so this plugin's is not used`);
|
|
201
|
+
else
|
|
202
|
+
(servers[server] = entry), used.mcpServers.push(server);
|
|
203
|
+
plugins.push({ ...summary, ...used, notes });
|
|
204
|
+
}
|
|
205
|
+
return { skills: [...skills, ...pluginSkills], commands, mcp: { ...servers, ...mcp }, plugins };
|
|
206
|
+
}
|
|
207
|
+
function schedules(value) {
|
|
208
|
+
if (value === undefined)
|
|
209
|
+
return [];
|
|
210
|
+
if (!Array.isArray(value))
|
|
211
|
+
throw new Error("schedule must be a list");
|
|
212
|
+
const names = new Set();
|
|
213
|
+
return value.map((item, index) => {
|
|
214
|
+
const field = `schedule[${index}]`;
|
|
215
|
+
if (!isObj(item))
|
|
216
|
+
throw new Error(`${field} must be a mapping with cron and message`);
|
|
217
|
+
const cron = text(item.cron, `${field}.cron`, true);
|
|
218
|
+
const entry = {
|
|
219
|
+
name: text(item.name, `${field}.name`) ?? cron,
|
|
220
|
+
cron,
|
|
221
|
+
timezone: text(item.timezone, `${field}.timezone`),
|
|
222
|
+
message: text(item.message, `${field}.message`, true),
|
|
223
|
+
};
|
|
224
|
+
if (names.has(entry.name))
|
|
225
|
+
throw new Error(`${field}: two schedules are called "${entry.name}"; give them names`);
|
|
226
|
+
names.add(entry.name);
|
|
227
|
+
parseCron(entry.cron);
|
|
228
|
+
if (entry.timezone)
|
|
229
|
+
try {
|
|
230
|
+
new Intl.DateTimeFormat("en-US", { timeZone: entry.timezone });
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
throw new Error(`${field}.timezone is not a valid IANA time zone`);
|
|
234
|
+
}
|
|
235
|
+
return entry;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/** The workspace an agent gets when AGENT.md does not name one: its project, or its own folder. */
|
|
239
|
+
export function defaultWorkspace(folder, configDir) {
|
|
240
|
+
const project = projectOf(folder);
|
|
241
|
+
return project && resolve(definitionRootOf(folder)) !== resolve(configDir) ? project : folder;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Loads an agent folder. With `source`, validates that text as the folder's AGENT.md
|
|
245
|
+
* without writing it. Throws a readable error for anything wrong.
|
|
246
|
+
*/
|
|
247
|
+
export function loadAgent(folder, configDir, source) {
|
|
248
|
+
const raw = source ?? readFileSync(join(folder, AGENT_FILE), "utf8");
|
|
249
|
+
const { data, body } = split(raw);
|
|
250
|
+
for (const key of Object.keys(data))
|
|
251
|
+
if (!KEYS.includes(key))
|
|
252
|
+
throw new Error(`Unknown key "${key}" (use ${KEYS.join(", ")})`);
|
|
253
|
+
if (!body)
|
|
254
|
+
throw new Error("AGENT.md needs a role: write it below the front matter");
|
|
255
|
+
const workspace = absolute(text(data.workspace, "workspace") ?? defaultWorkspace(folder, configDir), folder);
|
|
256
|
+
if (!existsSync(workspace) || !statSync(workspace).isDirectory())
|
|
257
|
+
throw new Error(`workspace is not a directory: ${workspace}`);
|
|
258
|
+
const tools = data.tools === undefined ? undefined : strings(data.tools, "tools");
|
|
259
|
+
for (const tool of tools ?? [])
|
|
260
|
+
if (!BUILTIN_TOOLS.includes(tool))
|
|
261
|
+
throw new Error(`tools: unknown built-in tool "${tool}" (use ${BUILTIN_TOOLS.join(", ")})`);
|
|
262
|
+
const inline = mcpServers(data.mcp, "mcp", folder);
|
|
263
|
+
const fromFile = mcpFile(folder);
|
|
264
|
+
for (const server of Object.keys(inline))
|
|
265
|
+
if (fromFile[server])
|
|
266
|
+
throw new Error(`MCP server "${server}" is in both AGENT.md and mcp.json`);
|
|
267
|
+
if (data.delegation !== undefined && typeof data.delegation !== "boolean" && !isObj(data.delegation))
|
|
268
|
+
throw new Error("delegation must be true, false or a mapping");
|
|
269
|
+
const delegation = isObj(data.delegation) ? data.delegation : {};
|
|
270
|
+
for (const key of Object.keys(delegation))
|
|
271
|
+
if (!["enabled", "maxChildren", "model", "thinking"].includes(key))
|
|
272
|
+
throw new Error(`delegation.${key} is not a setting (use enabled, maxChildren, model, thinking)`);
|
|
273
|
+
if (delegation.maxChildren !== undefined && (!Number.isInteger(delegation.maxChildren) || Number(delegation.maxChildren) < 1))
|
|
274
|
+
throw new Error("delegation.maxChildren must be a positive integer");
|
|
275
|
+
const childModel = model(delegation.model, "delegation.model");
|
|
276
|
+
const childThinking = thinking(delegation.thinking, "delegation.thinking");
|
|
277
|
+
if (data.collaboration !== undefined && typeof data.collaboration !== "boolean" && !isObj(data.collaboration))
|
|
278
|
+
throw new Error("collaboration must be true, false or a mapping");
|
|
279
|
+
const collaboration = isObj(data.collaboration) ? data.collaboration : { enabled: data.collaboration };
|
|
280
|
+
for (const flag of ["enabled", "acceptTasks"])
|
|
281
|
+
if (collaboration[flag] !== undefined && typeof collaboration[flag] !== "boolean")
|
|
282
|
+
throw new Error(`collaboration.${flag} must be true or false`);
|
|
283
|
+
const main = model(data.model, "model") ?? model(DEFAULT_MODEL, "model");
|
|
284
|
+
const resolved = withPlugins(strings(data.plugins, "plugins"), configDir, [...folderSkills(folder), ...existing(strings(data.skills, "skills").map((path) => absolute(path, folder)), "skills")], { ...fromFile, ...inline });
|
|
285
|
+
return {
|
|
286
|
+
id: "",
|
|
287
|
+
name: text(data.name, "name") ?? basename(folder),
|
|
288
|
+
folder,
|
|
289
|
+
title: text(data.title, "title"),
|
|
290
|
+
description: text(data.description, "description"),
|
|
291
|
+
icon: text(data.icon, "icon"),
|
|
292
|
+
role: body,
|
|
293
|
+
instructionsFiles: existing(strings(data.instructions, "instructions").map((path) => absolute(path, workspace)), "instructions"),
|
|
294
|
+
model: { ...main, thinking: thinking(data.thinking, "thinking") },
|
|
295
|
+
workspace,
|
|
296
|
+
tools,
|
|
297
|
+
skills: resolved.skills,
|
|
298
|
+
commands: resolved.commands,
|
|
299
|
+
plugins: resolved.plugins,
|
|
300
|
+
extensions: existing(strings(data.extensions, "extensions").map((path) => absolute(path, folder)), "extensions"),
|
|
301
|
+
mcp: resolved.mcp,
|
|
302
|
+
...loadAvatar(folder),
|
|
303
|
+
delegation: {
|
|
304
|
+
enabled: data.delegation !== false && delegation.enabled !== false,
|
|
305
|
+
maxChildren: delegation.maxChildren,
|
|
306
|
+
model: childModel || childThinking ? { ...childModel, ...(childThinking ? { thinking: childThinking } : {}) } : undefined,
|
|
307
|
+
},
|
|
308
|
+
collaboration: {
|
|
309
|
+
enabled: collaboration.enabled !== false,
|
|
310
|
+
contacts: collaboration.contacts === undefined
|
|
311
|
+
? [...DEFAULT_COLLABORATION.contacts]
|
|
312
|
+
: strings(collaboration.contacts, "collaboration.contacts"),
|
|
313
|
+
acceptTasks: collaboration.acceptTasks !== false,
|
|
314
|
+
},
|
|
315
|
+
schedule: schedules(data.schedule),
|
|
316
|
+
limits: { runTimeoutMs: data.timeout === undefined ? undefined : parseDuration(data.timeout, "timeout") },
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* AGENT.md text with front matter keys set (undefined deletes the key) and optionally a new
|
|
321
|
+
* role, keeping everything else as written.
|
|
322
|
+
*/
|
|
323
|
+
export function editAgentMarkdown(source, keys, role) {
|
|
324
|
+
const { document: parsed, body } = split(source);
|
|
325
|
+
// An empty front matter parses to null, not a mapping; start a fresh one.
|
|
326
|
+
const document = isMap(parsed.contents) ? parsed : new Document({});
|
|
327
|
+
for (const [key, value] of Object.entries(keys))
|
|
328
|
+
if (value === undefined)
|
|
329
|
+
document.delete(key);
|
|
330
|
+
else
|
|
331
|
+
document.set(key, document.createNode(value));
|
|
332
|
+
const front = document.toString().trim();
|
|
333
|
+
return `${front && front !== "{}" ? `---\n${front}\n---\n\n` : ""}${(role ?? body).trim()}\n`;
|
|
334
|
+
}
|
|
335
|
+
/** Replaces the schedule list in AGENT.md, keeping the rest of the file as written. */
|
|
336
|
+
export function writeSchedules(folder, list) {
|
|
337
|
+
const path = join(folder, AGENT_FILE);
|
|
338
|
+
const items = list.map(({ name, cron, timezone, message }) => ({
|
|
339
|
+
name,
|
|
340
|
+
cron,
|
|
341
|
+
...(timezone ? { timezone } : {}),
|
|
342
|
+
message,
|
|
343
|
+
}));
|
|
344
|
+
writeFileSync(path, editAgentMarkdown(readFileSync(path, "utf8"), { schedule: items.length ? items : undefined }));
|
|
345
|
+
}
|
|
346
|
+
/** What the agent remembers between runs. It writes here; the user may edit it too. */
|
|
347
|
+
export const memoryPath = (folder) => join(folder, "memory.md");
|
|
348
|
+
const read = (path) => {
|
|
349
|
+
try {
|
|
350
|
+
return readFileSync(path, "utf8");
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
return "";
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
/** Agent folders in one `agents` folder: each subfolder with an AGENT.md. */
|
|
357
|
+
function agentFoldersIn(folder) {
|
|
358
|
+
try {
|
|
359
|
+
return readdirSync(folder)
|
|
360
|
+
.filter((name) => !name.startsWith("."))
|
|
361
|
+
.sort()
|
|
362
|
+
.map((name) => join(folder, name))
|
|
363
|
+
.filter((path) => existsSync(join(path, AGENT_FILE)));
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/** Folders searched for agents: `~/.gumdrops/agents` and each project's `.gumdrops/agents`. */
|
|
370
|
+
export const agentsFolders = (roots) => [
|
|
371
|
+
...new Set([
|
|
372
|
+
join(roots.configDir, "agents"),
|
|
373
|
+
...roots.projects.map((project) => join(project, ".gumdrops", "agents")),
|
|
374
|
+
]),
|
|
375
|
+
];
|
|
376
|
+
/** A cheap fingerprint of every agent folder, so the host reloads only on change. */
|
|
377
|
+
export function fingerprint(roots) {
|
|
378
|
+
return [
|
|
379
|
+
...agentsFolders(roots)
|
|
380
|
+
.flatMap(agentFoldersIn)
|
|
381
|
+
.flatMap((folder) => [AGENT_FILE, "memory.md", "mcp.json", "skills", AVATAR_FILE].map((file) => join(folder, file))),
|
|
382
|
+
// Installed plugins, so editing, installing or updating one reloads the agents that use it.
|
|
383
|
+
...pluginFiles(roots.configDir),
|
|
384
|
+
]
|
|
385
|
+
.map((file) => {
|
|
386
|
+
try {
|
|
387
|
+
const stat = statSync(file);
|
|
388
|
+
return `${file}:${stat.mtimeMs}:${stat.size}`;
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
return "";
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
.join("\n");
|
|
395
|
+
}
|
|
396
|
+
export function loadAll(roots) {
|
|
397
|
+
const agents = [];
|
|
398
|
+
const failures = [];
|
|
399
|
+
for (const folder of agentsFolders(roots).flatMap(agentFoldersIn)) {
|
|
400
|
+
const path = join(folder, AGENT_FILE);
|
|
401
|
+
try {
|
|
402
|
+
agents.push({ path, spec: loadAgent(folder, roots.configDir), memory: read(memoryPath(folder)).trim() });
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
failures.push({ path, error: error instanceof Error ? error.message : String(error) });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return { agents, failures };
|
|
409
|
+
}
|
|
410
|
+
export const slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "agent";
|
|
411
|
+
/** A free folder for a new agent in an `agents` folder. */
|
|
412
|
+
export function newAgentFolder(agents, name) {
|
|
413
|
+
let folder = join(agents, slug(name));
|
|
414
|
+
for (let index = 2; existsSync(folder); index += 1)
|
|
415
|
+
folder = join(agents, `${slug(name)}-${index}`);
|
|
416
|
+
return folder;
|
|
417
|
+
}
|
|
418
|
+
//# sourceMappingURL=agentfile.js.map
|
package/host/dist/api.js
ADDED