@chloejs/core 0.2.1 → 0.2.3

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,5 +1,9 @@
1
1
  # Chloe
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@chloejs/core)](https://www.npmjs.com/package/@chloejs/core)
4
+
5
+ **[chloejs.org](https://chloejs.org)**: the docs, the examples and the reference.
6
+
3
7
  Chloe is a TypeScript agent framework that uses AI only when you need it.
4
8
 
5
9
  You write the workflow in code, **and ask AI where a step needs judgement**. You
@@ -198,16 +202,17 @@ serve/ the one port: every route, the login, tokens, the plain page
198
202
  core/ the floor. steps.ts runs a job, turn.ts runs a prompt, clock.ts
199
203
  starts each job when its cron line is due
200
204
  scorers/ how a run is marked
201
- do/ the work itself, called straight from a job
205
+ services/ the work itself, called straight from a job, published as
206
+ "@chloejs/core/services"
202
207
  channels/ the ways in, for an agent to bind
203
208
  ops/ the tests, the evals, talking to an agent, making the account,
204
209
  and install.sh, which installs the service
205
210
  test-agent/ the agent the tests load. Not published
206
211
  ```
207
212
 
208
- Six entrances and no others: `@chloejs/core`, `@chloejs/core/tools`,
209
- `@chloejs/core/channels/<name>`, `@chloejs/core/scorers`, `@chloejs/core/timer` and
210
- `@chloejs/core/test`.
213
+ Seven entrances and no others: `@chloejs/core`, `@chloejs/core/services`,
214
+ `@chloejs/core/tools`, `@chloejs/core/channels`, `@chloejs/core/scorers`,
215
+ `@chloejs/core/timer` and `@chloejs/core/test`.
211
216
 
212
217
  ## Use code when you know what to do. Use AI when you do not.
213
218
 
package/channels/api.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // Opting one agent in to being reached by another system.
2
2
  //
3
3
  // // agents/<name>/agent.ts
4
- // import { apiChannel } from "@chloejs/core/channels/api";
4
+ // import { apiChannel } from "@chloejs/core/channels";
5
5
  // channels: [apiChannel()],
6
6
  //
7
7
  // Binding it makes two routes answer for that agent when the caller holds a
@@ -0,0 +1,13 @@
1
+ // The ways an agent is reached. Each is a function an agent calls in the
2
+ // `channels` list of its agent.ts.
3
+ //
4
+ // import { telegramChannel, apiChannel } from "@chloejs/core/channels";
5
+ //
6
+ // A channel chloe does not ship is written in the agent's own channels/
7
+ // folder, and hands each message to `receive`. Same rule as `index.ts`:
8
+ // adding a name here is publishing it.
9
+
10
+ export { telegramChannel, type TelegramOptions } from "./telegram.ts";
11
+ export { slackChannel, type SlackOptions } from "./slack.ts";
12
+ export { apiChannel } from "./api.ts";
13
+ export { receive, commands, type Incoming, type Rules, type While, type Handled } from "./shared.ts";
@@ -4,7 +4,7 @@
4
4
  // here, once, so Telegram, the API and any channel written later behave the
5
5
  // same way. A channel written in an agent's own folder imports it too:
6
6
  //
7
- // import { receive, type Incoming } from "@chloejs/core/channels/shared";
7
+ // import { receive, type Incoming } from "@chloejs/core/channels";
8
8
  //
9
9
  // In order, and the first that applies decides:
10
10
  //
package/channels/slack.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // Talking to an agent from Slack. It is one entry in the agent's channels:
2
2
  //
3
3
  // // agents/<name>/agent.ts
4
- // import { slackChannel } from "@chloejs/core/channels/slack";
4
+ // import { slackChannel } from "@chloejs/core/channels";
5
5
  // channels: [slackChannel({ allowFrom: ["U0123ABCD"] })],
6
6
  //
7
7
  // It needs a Slack app with Socket Mode on, which is chloe opening a
@@ -1,7 +1,7 @@
1
1
  // Talking to an agent from Telegram. It is one entry in the agent's channels:
2
2
  //
3
3
  // // agents/<name>/agent.ts
4
- // import { telegramChannel } from "@chloejs/core/channels/telegram";
4
+ // import { telegramChannel } from "@chloejs/core/channels";
5
5
  // channels: [telegramChannel({ allowFrom: [111111111] })],
6
6
  //
7
7
  // The bot's token is TELEGRAM_BOT_TOKEN, or `credentials: { botToken }`. To
package/core/settings.ts CHANGED
@@ -32,13 +32,13 @@ const schema = z.object({
32
32
  judge: z.string().default("anthropic/claude-sonnet-5"),
33
33
  })
34
34
  .prefault({}),
35
- app: z
35
+ email: z
36
36
  .object({
37
- /** Where an agent's mail goes. Comma separated for more than one. */
38
- send_email_to: z.string().default(""),
37
+ /** Who carries an agent's mail. Its key is in that provider's own section. */
38
+ provider: z.enum(["resend"]).default("resend"),
39
39
  })
40
40
  .prefault({}),
41
- /** Sending mail. */
41
+ /** Sending mail through Resend. */
42
42
  resend: z
43
43
  .object({
44
44
  /** The key an agent's mail is sent with. Without one, nothing is sent. */
package/index.ts CHANGED
@@ -2,10 +2,12 @@
2
2
  //
3
3
  // Everything an agent, a job, a tool or a channel is written with is named
4
4
  // here, and nothing else in this folder is anybody's business. Import it as
5
- // "@chloejs/core": the three lines below say the rest.
5
+ // "@chloejs/core": the lines below say the rest.
6
6
  //
7
7
  // import { defineJob, tool, note } from "@chloejs/core";
8
- // import { telegramChannel } from "@chloejs/core/channels/telegram"; // reaching an agent
8
+ // import { run, sendEmail } from "@chloejs/core/services"; // the work, for a job
9
+ // import { read_mail } from "@chloejs/core/tools"; // the work, for a model
10
+ // import { telegramChannel } from "@chloejs/core/channels"; // reaching an agent
9
11
  // import { calls, expectations } from "@chloejs/core/scorers"; // marking a run
10
12
  //
11
13
  // Adding a name here is publishing it, and taking one away is a break, so this
@@ -38,15 +40,3 @@ export { readSettings, setting, settings, type Settings } from "./core/settings.
38
40
  export { confine } from "./core/confine.ts";
39
41
  export { note, type Note } from "./core/notes.ts";
40
42
  export { copyDatabase, DATABASE, db, trim } from "./core/db.ts";
41
-
42
- // What a job can do without asking anybody: running a command, sending mail,
43
- // reading mail, reading and writing files in one folder, running one of an
44
- // agent's own scripts, reading a web page. The same work offered
45
- // to a model instead is "@chloejs/core/tools", and each of those is a wrapper over one
46
- // of these.
47
- export { run, type Result } from "./do/run.ts";
48
- export { send, type Address } from "./do/email.ts";
49
- export { messages, oneMessage, type Message as Mail } from "./do/mail.ts";
50
- export { list, read, search, write } from "./do/files.ts";
51
- export { script, scripts } from "./do/scripts.ts";
52
- export { readPage, htmlToText, isPrivate, type Page } from "./do/web.ts";
package/load/load.ts CHANGED
@@ -47,7 +47,7 @@ export interface Home {
47
47
  memory: Memory & { folder: string };
48
48
  }
49
49
 
50
- /** A set of tools made for one agent as it loads, like readMail({ ... }). */
50
+ /** A set of tools made for one agent as it loads, like read_mail({ ... }). */
51
51
  export type Binding = (agent: Home) => Tools;
52
52
 
53
53
  /** What defineAgent is given. */
@@ -82,7 +82,7 @@ export interface Definition {
82
82
  /** `prompt("instructions.md")`, a path inside the agent's folder, or the words themselves. */
83
83
  instructions: string | Prompt;
84
84
  /**
85
- * Each tool, or a set of them like readMail({ ... }). A model calls one by
85
+ * Each tool, or a set of them like read_mail({ ... }). A model calls one by
86
86
  * its id. What `features` turns on is added to these and not listed here.
87
87
  */
88
88
  tools?: (Tool | Tools | Binding)[];
@@ -1,10 +1,10 @@
1
- // The tools over do/files.ts: one folder, offered to a model.
1
+ // The tools over services/filesService.ts: one folder, offered to a model.
2
2
  //
3
3
  // An agent binds each one to a root it is allowed to see, and names that
4
4
  // folder in plain words for the description.
5
5
  import { z } from "zod";
6
6
 
7
- import { list, read, search, write } from "#chloe/do/files.ts";
7
+ import { listFiles, readFiles, searchFiles, writeFiles } from "#chloe/services/filesService.ts";
8
8
  import { tool } from "#chloe/model/tool.ts";
9
9
 
10
10
  /**
@@ -19,29 +19,29 @@ interface Folder {
19
19
  }
20
20
 
21
21
  /** A tool that lists what is in one folder, and nothing outside it. */
22
- export function listIn({ root, what, id = "list_notes" }: Folder) {
22
+ export function list_in({ root, what, id = "list_notes" }: Folder) {
23
23
  return tool({
24
24
  id,
25
25
  description: `List a folder in ${what}, so you can find the right file before reading it. Start here rather than guessing at a path.`,
26
26
  inputSchema: z.object({
27
27
  path: z.string().optional().describe("Folder to list. Omit for the top level."),
28
28
  }),
29
- execute: ({ path }) => list(root, path),
29
+ execute: ({ path }) => listFiles(root, path),
30
30
  });
31
31
  }
32
32
 
33
33
  /** A tool that reads one file inside that folder. */
34
- export function readIn({ root, what, id = "read_notes" }: Folder) {
34
+ export function read_in({ root, what, id = "read_notes" }: Folder) {
35
35
  return tool({
36
36
  id,
37
37
  description: `Read one file from ${what}. Read before answering, and read before writing: guessing from memory is how you end up confidently wrong.`,
38
38
  inputSchema: z.object({ path: z.string() }),
39
- execute: ({ path }) => read(root, path),
39
+ execute: ({ path }) => readFiles(root, path),
40
40
  });
41
41
  }
42
42
 
43
43
  /** A tool that searches the text of the files in that folder. */
44
- export function searchIn({ root, what, id = "search_notes" }: Folder) {
44
+ export function search_in({ root, what, id = "search_notes" }: Folder) {
45
45
  return tool({
46
46
  id,
47
47
  description: `Search ${what} for text, and return the matching files and lines. Search before answering anything you are not certain of.`,
@@ -49,12 +49,12 @@ export function searchIn({ root, what, id = "search_notes" }: Folder) {
49
49
  query: z.string().min(2).describe("Text to look for, case-insensitive."),
50
50
  folder: z.string().optional().describe("Narrow to one folder. Omit to search everything."),
51
51
  }),
52
- execute: ({ query, folder }) => search(root, query, folder),
52
+ execute: ({ query, folder }) => searchFiles(root, query, folder),
53
53
  });
54
54
  }
55
55
 
56
56
  /** `commit` makes every write a git commit, for a folder that is a repo. */
57
- export function writeIn({ root, what, id = "write_notes", commit = false }: Folder & { commit?: boolean }) {
57
+ export function write_in({ root, what, id = "write_notes", commit = false }: Folder & { commit?: boolean }) {
58
58
  return tool({
59
59
  id,
60
60
  description:
@@ -66,6 +66,6 @@ export function writeIn({ root, what, id = "write_notes", commit = false }: Fold
66
66
  content: z.string().min(1),
67
67
  message: z.string().optional().describe("Commit message saying what changed. Required here."),
68
68
  }),
69
- execute: ({ path, content, message }) => write(root, path, content, { commit, message }),
69
+ execute: ({ path, content, message }) => writeFiles(root, path, content, { commit, message }),
70
70
  });
71
71
  }
@@ -1,10 +1,10 @@
1
- // The tool over do/mail.ts: reading mail, bound to a fixed search.
1
+ // The tool over services/gmailService.ts: reading mail, bound to a fixed search.
2
2
  //
3
3
  // The binding lives in the agent's config, not in anything the model can
4
4
  // write. All the model chooses is how far back and how many.
5
5
  import { z } from "zod";
6
6
 
7
- import { messages, oneMessage } from "#chloe/do/mail.ts";
7
+ import { messages, oneMessage } from "#chloe/services/gmailService.ts";
8
8
  import { tool } from "#chloe/model/tool.ts";
9
9
 
10
10
  interface Options {
@@ -25,7 +25,7 @@ interface Options {
25
25
  * A tool that reads the mail the agent is bound to. The search is the
26
26
  * binding's, and the model chooses only how far back and how many.
27
27
  */
28
- export function readMail({
28
+ export function read_mail({
29
29
  search = "in:inbox",
30
30
  what = "mail in the inbox",
31
31
  days = 7,
@@ -5,12 +5,12 @@
5
5
  // its own folder, its own mailbox, its own From line, so nothing here names an
6
6
  // agent or a person.
7
7
  //
8
- // import { readMail, readWeb } from "@chloejs/core/tools";
8
+ // import { read_mail, read_web } from "@chloejs/core/tools";
9
9
  //
10
10
  // The notes tools, write_skill and run_script are not here: an agent turns
11
11
  // them on with `features` in its definition.
12
12
  //
13
- // The work itself is in do/, published from "@chloejs/core", and a job calls it
13
+ // The work itself is in services/, published as "@chloejs/core/services", and a job calls it
14
14
  // from a step rather than coming through here. If a job imports this file,
15
15
  // something is in the wrong place.
16
16
  //
@@ -18,11 +18,11 @@
18
18
 
19
19
 
20
20
  // One folder, as tools, for an agent that needs a different set.
21
- export { listIn, readIn, searchIn, writeIn } from "./files.ts";
21
+ export { list_in, read_in, search_in, write_in } from "./files.ts";
22
22
 
23
23
  // Mail in, mail out.
24
- export { readMail } from "./gmail.ts";
25
- export { sendEmail } from "./send_email.ts";
24
+ export { read_mail } from "./gmail.ts";
25
+ export { send_email } from "./send_email.ts";
26
26
 
27
27
  // Reading a public web page.
28
- export { readWeb } from "./web.ts";
28
+ export { read_web } from "./web.ts";
@@ -4,7 +4,7 @@
4
4
  // has them: the loader adds them, so an agent's `tools` never lists them.
5
5
  import { mkdirSync } from "node:fs";
6
6
 
7
- import { listIn, readIn, searchIn, writeIn } from "./files.ts";
7
+ import { list_in, read_in, search_in, write_in } from "./files.ts";
8
8
  import type { Tools } from "../tool.ts";
9
9
 
10
10
  /** The four notes tools for one agent's memory. A write is a git commit when `commit` says so. */
@@ -15,9 +15,9 @@ export function memoryTools(memory: { folder: string; commit?: boolean }): Tools
15
15
  // to fail on one missing.
16
16
  mkdirSync(folder, { recursive: true });
17
17
  return {
18
- list_notes: listIn({ root: folder, what }),
19
- read_notes: readIn({ root: folder, what }),
20
- search_notes: searchIn({ root: folder, what }),
21
- write_notes: writeIn({ root: folder, what, commit }),
18
+ list_notes: list_in({ root: folder, what }),
19
+ read_notes: read_in({ root: folder, what }),
20
+ search_notes: search_in({ root: folder, what }),
21
+ write_notes: write_in({ root: folder, what, commit }),
22
22
  };
23
23
  }
@@ -1,4 +1,4 @@
1
- // The tool over do/scripts.ts: an agent running one of its own scripts.
1
+ // The tool over services/scriptsService.ts: an agent running one of its own scripts.
2
2
  //
3
3
  // This is the plug-and-play half of the system. A capability is a script in
4
4
  // the agent's `scripts/` folder plus a short file in its `skills/` folder
@@ -8,7 +8,7 @@ import { existsSync, readdirSync } from "node:fs";
8
8
  import { z } from "zod";
9
9
 
10
10
  import { agentDir } from "#chloe/core/paths.ts";
11
- import { script, scripts } from "#chloe/do/scripts.ts";
11
+ import { script, scripts } from "#chloe/services/scriptsService.ts";
12
12
  import { tool, type Tools } from "#chloe/model/tool.ts";
13
13
 
14
14
  /** A tool that runs one file from that agent's own `scripts/` folder. */
@@ -1,13 +1,13 @@
1
- // The tool over do/email.ts: sending one email.
1
+ // The tool over services/emailService.ts: sending one email.
2
2
  //
3
3
  // The agent binds its own From line and its own recipients. All the model
4
4
  // writes is the subject and the body.
5
5
  import { z } from "zod";
6
6
 
7
- import { type Address, send } from "#chloe/do/email.ts";
7
+ import { type EmailSender, sendEmail } from "#chloe/services/emailService.ts";
8
8
  import { tool } from "#chloe/model/tool.ts";
9
9
 
10
- interface Sender extends Address {
10
+ interface Options extends EmailSender {
11
11
  /** Who it reaches and when to use it, in the agent's own words. Shown to the model. */
12
12
  when: string;
13
13
  }
@@ -16,7 +16,7 @@ interface Sender extends Address {
16
16
  * A tool that sends mail from the address the agent was given, to the address
17
17
  * it was given.
18
18
  */
19
- export function sendEmail({ when, ...address }: Sender) {
19
+ export function send_email({ when, ...sender }: Options) {
20
20
  return tool({
21
21
  id: "send_email",
22
22
  description: `Send an email. ${when}`,
@@ -24,6 +24,6 @@ export function sendEmail({ when, ...address }: Sender) {
24
24
  subject: z.string().min(5).max(120),
25
25
  body: z.string().min(20).describe("Plain text. Lead with what happened and what you did."),
26
26
  }),
27
- execute: ({ subject, body }) => send(address, subject, body),
27
+ execute: ({ subject, body }) => sendEmail(sender, subject, body),
28
28
  });
29
29
  }
@@ -1,13 +1,13 @@
1
- // The tool over do/web.ts: reading one public web page.
1
+ // The tool over services/webService.ts: reading one public web page.
2
2
  import { z } from "zod";
3
3
 
4
- import { readPage } from "#chloe/do/web.ts";
4
+ import { readPage } from "#chloe/services/webService.ts";
5
5
  import { tool } from "#chloe/model/tool.ts";
6
6
 
7
7
  /** A tool that reads one public web page as plain text. */
8
- export function readWeb() {
8
+ export function read_web() {
9
9
  return tool({
10
- id: "read_page",
10
+ id: "read_web",
11
11
  description:
12
12
  "Read a public web page as plain text. Links come back as `[text](url)`: to follow one, pass that url " +
13
13
  "exactly as it came back, never one you rebuilt by hand, because one changed character can make a site " +
@@ -11,13 +11,13 @@
11
11
  // point at a script that a person already put there.
12
12
  //
13
13
  // Every write is a git commit, so self-improvement always leaves a diff.
14
- import { writeIn } from "./files.ts";
14
+ import { write_in } from "./files.ts";
15
15
  import { agentDir } from "#chloe/core/paths.ts";
16
16
  import type { Tools } from "../tool.ts";
17
17
 
18
18
  /** A tool that rewrites one of the agent's own skills. Every write is a commit. */
19
19
  export function writeSkill(agent: string) {
20
- return writeIn({
20
+ return write_in({
21
21
  root: `${agentDir(agent)}/skills`,
22
22
  what: "your own skills",
23
23
  id: "write_skill",
package/ops/test.ts CHANGED
@@ -574,7 +574,7 @@ about("a model step that never fits");
574
574
 
575
575
  {
576
576
  about("what mail says when a person has to sign in");
577
- const { explain } = await import("#chloe/do/mail.ts");
577
+ const { explain } = await import("#chloe/services/gmailService.ts");
578
578
 
579
579
  // The account is read from settings, which on a real box has a real one in it.
580
580
  const { settings } = await import("@chloejs/core");
@@ -833,7 +833,7 @@ about("a model step that never fits");
833
833
 
834
834
  // A tool is for a model only, whether it is one of chloe's or the agent's
835
835
  // own. A job that imports one is either doing work through a wrapper built
836
- // for a model, or it wanted a `do/` folder and took the first import that
836
+ // for a model, or it wanted a `services/` folder and took the first import that
837
837
  // compiled. The other direction is fine: a tool may call a job's function.
838
838
  const { loadAll } = await import("@chloejs/core");
839
839
  const found = [];
@@ -1344,13 +1344,13 @@ for (const agent of (await (await import("@chloejs/core")).loadAll()).values())
1344
1344
  const { recall, remember } = await import("#chloe/model/memory.ts");
1345
1345
  remember("test/tools", "user", "What board am I on?");
1346
1346
  remember("test/tools", "assistant", "Board 210.", [
1347
- { tool: "read_page", args: { url: "https://example.com/pairings" } },
1347
+ { tool: "read_web", args: { url: "https://example.com/pairings" } },
1348
1348
  { tool: "write_notes", args: { path: "chess.html", content: "x".repeat(1000) } },
1349
1349
  ]);
1350
1350
  remember("test/tools", "assistant", "Anything else?");
1351
1351
  const told = recall("test/tools", { limit: 10, tools: true });
1352
1352
  is("the next turn sees the calls, then the reply", told.map((one) => one.role), ["user", "assistant", "tool", "tool", "assistant", "assistant"]);
1353
- is("in the shape a turn's own calls take", told[1].tool_calls?.[0].function, { name: "read_page", arguments: '{"url":"https://example.com/pairings"}' });
1353
+ is("in the shape a turn's own calls take", told[1].tool_calls?.[0].function, { name: "read_web", arguments: '{"url":"https://example.com/pairings"}' });
1354
1354
  is("a whole file written is cut short", JSON.parse(told[1].tool_calls![1].function.arguments).content.length, 303);
1355
1355
  is("each call is answered, or a provider refuses the history", told[2].tool_call_id, told[1].tool_calls?.[0].id);
1356
1356
  is("the reply itself is left as it was", told[4].content, "Board 210.");
@@ -1393,7 +1393,7 @@ for (const agent of (await (await import("@chloejs/core")).loadAll()).values())
1393
1393
  {
1394
1394
  about("reading a web page");
1395
1395
 
1396
- const { htmlToText, isPrivate, readPage } = await import("@chloejs/core");
1396
+ const { htmlToText, isPrivate, readPage } = await import("@chloejs/core/services");
1397
1397
  const html =
1398
1398
  "<!doctype html><html><head><title>Wall &amp; chart</title><style>td{}</style></head><body>\n" +
1399
1399
  "<table>\n<tr><td><a href=\"report.php?section=Novice - under 900\">Novice</a></td>\n<td>239</td></tr>\n" +
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@chloejs/core",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "description": "A job runner where asking a model is one kind of step.",
6
6
  "exports": {
7
7
  ".": "./index.ts",
8
- "./channels/*": "./channels/*.ts",
8
+ "./channels": "./channels/index.ts",
9
+ "./services": "./services/index.ts",
9
10
  "./scorers": "./scorers/index.ts",
10
11
  "./timer": "./timer/index.ts",
11
12
  "./tools": "./model/tools/index.ts",
@@ -20,13 +21,13 @@
20
21
  "files": [
21
22
  "channels",
22
23
  "core",
23
- "do",
24
24
  "index.ts",
25
25
  "load",
26
26
  "model",
27
27
  "ops",
28
28
  "scorers",
29
29
  "serve",
30
+ "services",
30
31
  "server.ts",
31
32
  "timer"
32
33
  ],
package/serve/alerts.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  // nobody was told about, and the sign-in is recorded either way.
11
11
  import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
12
 
13
- import { send } from "#chloe/do/email.ts";
13
+ import { sendEmail } from "#chloe/services/emailService.ts";
14
14
  import { STATE } from "#chloe/core/paths.ts";
15
15
  import { settings } from "#chloe/core/settings.ts";
16
16
 
@@ -45,7 +45,7 @@ function where(): { to: string[]; from: string } | null {
45
45
  function mail(subject: string, body: string): void {
46
46
  const address = where();
47
47
  if (!address) return;
48
- void send({ from: address.from, to: address.to, tag: "chloe" }, subject, body).catch((error: unknown) => {
48
+ void sendEmail({ from: address.from, to: address.to, tag: "chloe" }, subject, body).catch((error: unknown) => {
49
49
  console.error("could not send the alert:", error instanceof Error ? error.message : error);
50
50
  });
51
51
  }
package/serve/files.ts CHANGED
@@ -11,7 +11,7 @@ import { existsSync, statSync } from "node:fs";
11
11
 
12
12
  import { confine } from "#chloe/core/confine.ts";
13
13
  import { agentDir } from "#chloe/core/paths.ts";
14
- import { list, read, write } from "#chloe/do/files.ts";
14
+ import { listFiles, readFiles, writeFiles } from "#chloe/services/filesService.ts";
15
15
 
16
16
  export interface Entry {
17
17
  name: string;
@@ -28,7 +28,7 @@ const JUNK = ["__pycache__", "node_modules"];
28
28
 
29
29
  /** Everything in one agent's folder, folders first, as a tree. */
30
30
  export async function tree(agent: string, path = "", depth = 0): Promise<Entry[]> {
31
- const { entries } = await list(agentDir(agent), path || undefined);
31
+ const { entries } = await listFiles(agentDir(agent), path || undefined);
32
32
  const out: Entry[] = [];
33
33
  for (const entry of entries) {
34
34
  const dir = entry.endsWith("/");
@@ -59,12 +59,12 @@ export async function open(agent: string, path: string) {
59
59
  if (statSync(resolved).isDirectory()) {
60
60
  return { path, dir: true as const, entries: await tree(agent, path) };
61
61
  }
62
- const file = await read(agentDir(agent), path);
62
+ const file = await readFiles(agentDir(agent), path);
63
63
  return { path, dir: false as const, content: file.content, editable: editable(path) };
64
64
  }
65
65
 
66
66
  export async function save(agent: string, path: string, content: string) {
67
67
  if (!editable(path)) throw new Error(`${path} is not markdown.`);
68
- const written = await write(agentDir(agent), path, content);
68
+ const written = await writeFiles(agentDir(agent), path, content);
69
69
  return { path, bytes: written.bytes };
70
70
  }
package/serve/memory.ts CHANGED
@@ -21,7 +21,7 @@ import { promisify } from "node:util";
21
21
 
22
22
  import { confine, unreachable } from "#chloe/core/confine.ts";
23
23
  import type { Agent } from "#chloe/load/load.ts";
24
- import { list, read, write } from "#chloe/do/files.ts";
24
+ import { listFiles, readFiles, writeFiles } from "#chloe/services/filesService.ts";
25
25
  import { STATE } from "#chloe/core/paths.ts";
26
26
  import { BadRequest } from "./errors.ts";
27
27
  import { noteHead } from "./page.ts";
@@ -91,7 +91,7 @@ export async function memoryTree(agent: Agent, from = "unknown"): Promise<Entry[
91
91
  }
92
92
 
93
93
  async function walk(agent: Agent, path: string, depth = 0): Promise<Entry[]> {
94
- const { entries } = await list(folder(agent), path || undefined);
94
+ const { entries } = await listFiles(folder(agent), path || undefined);
95
95
  const out: Entry[] = [];
96
96
  for (const entry of entries) {
97
97
  const dir = entry.endsWith("/");
@@ -139,7 +139,7 @@ export async function memoryOpen(agent: Agent, path: string, from: string) {
139
139
  await record(agent, "list", path, from);
140
140
  return { path, dir: true as const, entries: await walk(agent, path) };
141
141
  }
142
- const file = await read(folder(agent), path);
142
+ const file = await readFiles(folder(agent), path);
143
143
  // After the read and before the reply, so nothing is handed over unrecorded.
144
144
  await record(agent, "read", path, from, { bytes: file.bytes });
145
145
  return { path, dir: false as const, content: file.content, bytes: file.bytes };
@@ -213,7 +213,7 @@ export function withHead(html: string, head: string): string {
213
213
  export async function memorySave(agent: Agent, path: string, content: string, from: string) {
214
214
  if (!inside(agent, path, from)) throw new BadRequest(`${path} is not somewhere in this memory.`);
215
215
  const commit = Boolean(agent.memory.commit) && (await isRepo(agent));
216
- const written = await write(folder(agent), path, content, {
216
+ const written = await writeFiles(folder(agent), path, content, {
217
217
  commit,
218
218
  message: commit ? `memory: ${path} from the site` : undefined,
219
219
  });
@@ -0,0 +1,73 @@
1
+ // Sending one email.
2
+ //
3
+ // The caller supplies who it is from and who it is to. Which provider carries
4
+ // it is email.provider in settings, and each provider reads its own section
5
+ // of settings.local.json for its key (resend.api_key for Resend).
6
+ //
7
+ // The tool a model reaches is model/tools/send_email.ts, which calls
8
+ // this. A job calls this directly, from a step.
9
+
10
+ import { setting, settings } from "#chloe/core/settings.ts";
11
+
12
+ /**
13
+ * Who an agent's mail comes from, who it goes to, and the tag in front of
14
+ * every subject.
15
+ */
16
+ export interface EmailSender {
17
+ /** The From line, e.g. "Backups <info@example.com>". */
18
+ from: string;
19
+ /** Who it goes to. */
20
+ to: string[];
21
+ /** Prefix put in front of every subject, so an inbox can be filtered. */
22
+ tag?: string;
23
+ }
24
+
25
+ /** One message, ready to go: the tag is already in the subject. */
26
+ export interface Email {
27
+ from: string;
28
+ to: string[];
29
+ subject: string;
30
+ body: string;
31
+ }
32
+
33
+ /** Something that can carry an email. Returns the provider's id for it, if it gives one. */
34
+ export interface EmailProvider {
35
+ send(email: Email): Promise<{ id?: string }>;
36
+ }
37
+
38
+ const resend: EmailProvider = {
39
+ async send({ from, to, subject, body }) {
40
+ const key = setting(settings.resend.api_key, "RESEND_API_KEY");
41
+ if (!key) throw new Error("No Resend key. Put it in settings.local.json as resend.api_key.");
42
+ const response = await fetch("https://api.resend.com/emails", {
43
+ method: "POST",
44
+ headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
45
+ body: JSON.stringify({ from, to, subject, text: body }),
46
+ signal: AbortSignal.timeout(30_000),
47
+ });
48
+ if (!response.ok) {
49
+ throw new Error(`Resend refused the message (${response.status}): ${await response.text()}`);
50
+ }
51
+ return (await response.json()) as { id?: string };
52
+ },
53
+ };
54
+
55
+ /** Every provider email.provider can name. Adding one is an entry here and in the settings schema. */
56
+ const providers: Record<typeof settings.email.provider, EmailProvider> = { resend };
57
+
58
+ /** Sends one email through the configured provider and returns its id. The tag is put in front of the subject. */
59
+ export async function sendEmail(
60
+ { from, to, tag }: EmailSender,
61
+ subject: string,
62
+ body: string,
63
+ ): Promise<{ sent: true; id?: string; subject: string }> {
64
+ // Refuse rather than send nowhere.
65
+ if (to.length === 0) throw new Error("Nobody to send to. Give the sender at least one address in to.");
66
+ const { id } = await providers[settings.email.provider].send({
67
+ from,
68
+ to,
69
+ subject: tag ? `[${tag}] ${subject}` : subject,
70
+ body,
71
+ });
72
+ return { sent: true, id, subject };
73
+ }
@@ -15,10 +15,10 @@ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
15
15
  import { dirname } from "node:path";
16
16
 
17
17
  import { confine } from "#chloe/core/confine.ts";
18
- import { run } from "./run.ts";
18
+ import { run } from "./runService.ts";
19
19
 
20
20
  /** List a folder. `path` is relative to `root`, and omitting it means the top. */
21
- export async function list(root: string, path?: string) {
21
+ export async function listFiles(root: string, path?: string) {
22
22
  const resolved = path ? confine(root, path) : root;
23
23
  const entries = await readdir(resolved, { withFileTypes: true });
24
24
  return {
@@ -31,14 +31,14 @@ export async function list(root: string, path?: string) {
31
31
  }
32
32
 
33
33
  /** Read one file. `path` is relative to `root` and cannot leave it. */
34
- export async function read(root: string, path: string) {
34
+ export async function readFiles(root: string, path: string) {
35
35
  const resolved = confine(root, path);
36
36
  const content = await readFile(resolved, "utf8");
37
37
  return { path: resolved, bytes: content.length, content };
38
38
  }
39
39
 
40
40
  /** Search a folder for text, case-insensitive. `folder` narrows it. */
41
- export async function search(root: string, query: string, folder?: string) {
41
+ export async function searchFiles(root: string, query: string, folder?: string) {
42
42
  const target = folder ? confine(root, folder) : root;
43
43
  // ripgrep if the box has it, grep otherwise. An earlier version assumed
44
44
  // ripgrep, and when it was not installed every search quietly answered
@@ -68,7 +68,7 @@ export async function search(root: string, query: string, folder?: string) {
68
68
  * Write one file, replacing it. `commit` makes the write a git commit, for a
69
69
  * folder that is a repo, and then `message` is required.
70
70
  */
71
- export async function write(
71
+ export async function writeFiles(
72
72
  root: string,
73
73
  path: string,
74
74
  content: string,
@@ -11,7 +11,7 @@
11
11
  // The tool a model reaches is model/tools/gmail.ts, which calls this
12
12
  // with the same binding, so a job does not get a wider search for skipping
13
13
  // the model.
14
- import { run } from "./run.ts";
14
+ import { run } from "./runService.ts";
15
15
  import { setting, settings } from "#chloe/core/settings.ts";
16
16
 
17
17
  const GOG = "gog";
@@ -0,0 +1,16 @@
1
+ // What a job can do without asking anybody: running a command, sending mail,
2
+ // reading mail, reading and writing files in one folder, running one of an
3
+ // agent's own scripts, reading a web page.
4
+ //
5
+ // import { run, sendEmail } from "@chloejs/core/services";
6
+ //
7
+ // The same work offered to a model instead is "@chloejs/core/tools", and each
8
+ // of those is a wrapper over one of these. Same rule as `index.ts`: adding a
9
+ // name here is publishing it.
10
+
11
+ export { run, type Result } from "./runService.ts";
12
+ export { sendEmail, type EmailSender } from "./emailService.ts";
13
+ export { messages, oneMessage, type Message as Mail } from "./gmailService.ts";
14
+ export { listFiles, readFiles, searchFiles, writeFiles } from "./filesService.ts";
15
+ export { script, scripts } from "./scriptsService.ts";
16
+ export { readPage, htmlToText, isPrivate, type Page } from "./webService.ts";
@@ -11,7 +11,7 @@ import { readdir } from "node:fs/promises";
11
11
 
12
12
  import { agentDir } from "#chloe/core/paths.ts";
13
13
  import { settings } from "#chloe/core/settings.ts";
14
- import { run, type Result } from "./run.ts";
14
+ import { run, type Result } from "./runService.ts";
15
15
 
16
16
  /** What this agent has in scripts/, sorted. Nothing hidden. */
17
17
  export async function scripts(agent: string): Promise<string[]> {
package/do/email.ts DELETED
@@ -1,45 +0,0 @@
1
- // Sending one email.
2
- //
3
- // The caller supplies who it is from and who it is to; this file only knows
4
- // how to send. The key is resend.api_key in settings.local.json.
5
- //
6
- // The tool a model reaches is model/tools/send_email.ts, which calls
7
- // this. A job calls this directly, from a step.
8
-
9
- import { setting, settings } from "#chloe/core/settings.ts";
10
-
11
- /**
12
- * Who an agent's mail comes from, who it goes to, and the tag in front of
13
- * every subject.
14
- */
15
- export interface Address {
16
- /** The From line, e.g. "Backups <info@example.com>". */
17
- from: string;
18
- /** Who it goes to. */
19
- to: string[];
20
- /** Prefix put in front of every subject, so an inbox can be filtered. */
21
- tag?: string;
22
- }
23
-
24
- /** Sends one email and returns its id. The tag is put in front of the subject. */
25
- export async function send(
26
- { from, to, tag }: Address,
27
- subject: string,
28
- body: string,
29
- ): Promise<{ sent: true; id?: string; subject: string }> {
30
- const key = setting(settings.resend.api_key, "RESEND_API_KEY");
31
- if (!key) throw new Error("No Resend key. Put it in settings.local.json as resend.api_key.");
32
- // Refuse rather than send nowhere.
33
- if (to.length === 0) throw new Error("Nobody to send to. The address belongs in settings.local.json.");
34
- const response = await fetch("https://api.resend.com/emails", {
35
- method: "POST",
36
- headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
37
- body: JSON.stringify({ from, to, subject: tag ? `[${tag}] ${subject}` : subject, text: body }),
38
- signal: AbortSignal.timeout(30_000),
39
- });
40
- if (!response.ok) {
41
- throw new Error(`Resend refused the message (${response.status}): ${await response.text()}`);
42
- }
43
- const sent = (await response.json()) as { id?: string };
44
- return { sent: true, id: sent.id, subject };
45
- }
File without changes
File without changes