@chloejs/core 0.2.0 → 0.2.2
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 +5 -1
- package/core/settings.ts +4 -4
- package/index.ts +6 -6
- package/model/tools/files.ts +2 -2
- package/model/tools/gmail.ts +15 -6
- package/model/tools/index.ts +1 -1
- package/model/tools/run_script.ts +2 -2
- package/model/tools/send_email.ts +5 -5
- package/model/tools/web.ts +2 -2
- package/ops/test.ts +2 -2
- package/package.json +2 -2
- package/serve/alerts.ts +2 -2
- package/serve/files.ts +1 -1
- package/serve/memory.ts +1 -1
- package/services/emailService.ts +73 -0
- package/{do/files.ts → services/filesService.ts} +1 -1
- package/{do/mail.ts → services/gmailService.ts} +1 -1
- package/{do/scripts.ts → services/scriptsService.ts} +1 -1
- package/do/email.ts +0 -45
- /package/{do/run.ts → services/runService.ts} +0 -0
- /package/{do/web.ts → services/webService.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Chloe
|
|
2
2
|
|
|
3
|
+
[](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,7 +202,7 @@ 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
|
-
|
|
205
|
+
services/ the work itself, called straight from a job
|
|
202
206
|
channels/ the ways in, for an agent to bind
|
|
203
207
|
ops/ the tests, the evals, talking to an agent, making the account,
|
|
204
208
|
and install.sh, which installs the service
|
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
|
-
|
|
35
|
+
email: z
|
|
36
36
|
.object({
|
|
37
|
-
/**
|
|
38
|
-
|
|
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
|
@@ -44,9 +44,9 @@ export { copyDatabase, DATABASE, db, trim } from "./core/db.ts";
|
|
|
44
44
|
// agent's own scripts, reading a web page. The same work offered
|
|
45
45
|
// to a model instead is "@chloejs/core/tools", and each of those is a wrapper over one
|
|
46
46
|
// of these.
|
|
47
|
-
export { run, type Result } from "./
|
|
48
|
-
export {
|
|
49
|
-
export { messages, oneMessage, type Message as Mail } from "./
|
|
50
|
-
export { list, read, search, write } from "./
|
|
51
|
-
export { script, scripts } from "./
|
|
52
|
-
export { readPage, htmlToText, isPrivate, type Page } from "./
|
|
47
|
+
export { run, type Result } from "./services/runService.ts";
|
|
48
|
+
export { sendEmail, type EmailSender } from "./services/emailService.ts";
|
|
49
|
+
export { messages, oneMessage, type Message as Mail } from "./services/gmailService.ts";
|
|
50
|
+
export { list, read, search, write } from "./services/filesService.ts";
|
|
51
|
+
export { script, scripts } from "./services/scriptsService.ts";
|
|
52
|
+
export { readPage, htmlToText, isPrivate, type Page } from "./services/webService.ts";
|
package/model/tools/files.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// The tools over
|
|
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/
|
|
7
|
+
import { list, read, search, write } from "#chloe/services/filesService.ts";
|
|
8
8
|
import { tool } from "#chloe/model/tool.ts";
|
|
9
9
|
|
|
10
10
|
/**
|
package/model/tools/gmail.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
// The tool over
|
|
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/
|
|
7
|
+
import { messages, oneMessage } from "#chloe/services/gmailService.ts";
|
|
8
8
|
import { tool } from "#chloe/model/tool.ts";
|
|
9
9
|
|
|
10
10
|
interface Options {
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Gmail query this agent may see, and nothing else. Set in its config.
|
|
13
|
+
* Unsaid it is `in:inbox`, which is the whole inbox: a binding of its own
|
|
14
|
+
* is what keeps the agent to one slice of the mailbox.
|
|
15
|
+
*/
|
|
16
|
+
search?: string;
|
|
13
17
|
/** How to describe that mail in the tool's description, in plain words. */
|
|
14
|
-
what
|
|
18
|
+
what?: string;
|
|
15
19
|
/** Days back when the agent does not say. */
|
|
16
20
|
days?: number;
|
|
17
21
|
id?: string;
|
|
@@ -21,7 +25,12 @@ interface Options {
|
|
|
21
25
|
* A tool that reads the mail the agent is bound to. The search is the
|
|
22
26
|
* binding's, and the model chooses only how far back and how many.
|
|
23
27
|
*/
|
|
24
|
-
export function readMail({
|
|
28
|
+
export function readMail({
|
|
29
|
+
search = "in:inbox",
|
|
30
|
+
what = "mail in the inbox",
|
|
31
|
+
days = 7,
|
|
32
|
+
id = "read_mail",
|
|
33
|
+
}: Options = {}) {
|
|
25
34
|
return tool({
|
|
26
35
|
id,
|
|
27
36
|
description:
|
package/model/tools/index.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
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
|
|
13
|
+
// The work itself is in services/, published from "@chloejs/core", 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
|
//
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The tool over
|
|
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/
|
|
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
|
|
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
|
|
7
|
+
import { type EmailSender, sendEmail as send } from "#chloe/services/emailService.ts";
|
|
8
8
|
import { tool } from "#chloe/model/tool.ts";
|
|
9
9
|
|
|
10
|
-
interface
|
|
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, ...
|
|
19
|
+
export function sendEmail({ 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(
|
|
27
|
+
execute: ({ subject, body }) => send(sender, subject, body),
|
|
28
28
|
});
|
|
29
29
|
}
|
package/model/tools/web.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// The tool over
|
|
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/
|
|
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. */
|
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/
|
|
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 `
|
|
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 = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chloejs/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A job runner where asking a model is one kind of step.",
|
|
6
6
|
"exports": {
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"channels",
|
|
22
22
|
"core",
|
|
23
|
-
"do",
|
|
24
23
|
"index.ts",
|
|
25
24
|
"load",
|
|
26
25
|
"model",
|
|
27
26
|
"ops",
|
|
28
27
|
"scorers",
|
|
29
28
|
"serve",
|
|
29
|
+
"services",
|
|
30
30
|
"server.ts",
|
|
31
31
|
"timer"
|
|
32
32
|
],
|
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 {
|
|
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
|
|
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/
|
|
14
|
+
import { list, read, write } from "#chloe/services/filesService.ts";
|
|
15
15
|
|
|
16
16
|
export interface Entry {
|
|
17
17
|
name: string;
|
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/
|
|
24
|
+
import { list, read, write } 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";
|
|
@@ -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,7 +15,7 @@ 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 "./
|
|
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
21
|
export async function list(root: string, path?: 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 "./
|
|
14
|
+
import { run } from "./runService.ts";
|
|
15
15
|
import { setting, settings } from "#chloe/core/settings.ts";
|
|
16
16
|
|
|
17
17
|
const GOG = "gog";
|
|
@@ -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 "./
|
|
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
|