@chloejs/core 0.2.2 → 0.2.4
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 -4
- package/channels/api.ts +1 -1
- package/channels/index.ts +13 -0
- package/channels/shared.ts +1 -1
- package/channels/slack.ts +1 -1
- package/channels/telegram.ts +1 -1
- package/index.ts +4 -14
- package/load/load.ts +2 -2
- package/model/tools/files.ts +9 -9
- package/model/tools/gmail.ts +4 -4
- package/model/tools/index.ts +6 -6
- package/model/tools/memory.ts +5 -5
- package/model/tools/run_script.ts +3 -3
- package/model/tools/send_email.ts +3 -3
- package/model/tools/web.ts +2 -2
- package/model/tools/write_skill.ts +2 -2
- package/ops/test.ts +3 -3
- package/package.json +3 -2
- package/serve/files.ts +4 -4
- package/serve/memory.ts +4 -4
- package/services/filesService.ts +4 -4
- package/services/gmailService.ts +3 -3
- package/services/index.ts +16 -0
- package/services/scriptsService.ts +3 -3
package/README.md
CHANGED
|
@@ -202,16 +202,17 @@ serve/ the one port: every route, the login, tokens, the plain page
|
|
|
202
202
|
core/ the floor. steps.ts runs a job, turn.ts runs a prompt, clock.ts
|
|
203
203
|
starts each job when its cron line is due
|
|
204
204
|
scorers/ how a run is marked
|
|
205
|
-
services/ the work itself, called straight from a job
|
|
205
|
+
services/ the work itself, called straight from a job, published as
|
|
206
|
+
"@chloejs/core/services"
|
|
206
207
|
channels/ the ways in, for an agent to bind
|
|
207
208
|
ops/ the tests, the evals, talking to an agent, making the account,
|
|
208
209
|
and install.sh, which installs the service
|
|
209
210
|
test-agent/ the agent the tests load. Not published
|
|
210
211
|
```
|
|
211
212
|
|
|
212
|
-
|
|
213
|
-
`@chloejs/core/
|
|
214
|
-
`@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`.
|
|
215
216
|
|
|
216
217
|
## Use code when you know what to do. Use AI when you do not.
|
|
217
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
|
|
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";
|
package/channels/shared.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
package/channels/telegram.ts
CHANGED
|
@@ -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
|
|
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/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
|
|
5
|
+
// "@chloejs/core": the lines below say the rest.
|
|
6
6
|
//
|
|
7
7
|
// import { defineJob, tool, note } from "@chloejs/core";
|
|
8
|
-
// import {
|
|
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 "./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/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
|
|
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
|
|
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)[];
|
package/model/tools/files.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// folder in plain words for the description.
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
|
|
7
|
-
import {
|
|
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
|
|
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 }) =>
|
|
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
|
|
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 }) =>
|
|
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
|
|
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 }) =>
|
|
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
|
|
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 }) =>
|
|
69
|
+
execute: ({ path, content, message }) => writeFiles(root, path, content, { commit, message }),
|
|
70
70
|
});
|
|
71
71
|
}
|
package/model/tools/gmail.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// write. All the model chooses is how far back and how many.
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { readEmailMessages, readOneEmailMessage } 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
|
|
28
|
+
export function read_mail({
|
|
29
29
|
search = "in:inbox",
|
|
30
30
|
what = "mail in the inbox",
|
|
31
31
|
days = 7,
|
|
@@ -46,7 +46,7 @@ export function readMail({
|
|
|
46
46
|
}),
|
|
47
47
|
execute: ({ days: back, limit, messageId }) =>
|
|
48
48
|
messageId
|
|
49
|
-
?
|
|
50
|
-
:
|
|
49
|
+
? readOneEmailMessage({ search, what, days: back ?? days, limit: limit ?? 10, messageId })
|
|
50
|
+
: readEmailMessages({ search, days: back ?? days, limit: limit ?? 10 }),
|
|
51
51
|
});
|
|
52
52
|
}
|
package/model/tools/index.ts
CHANGED
|
@@ -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 {
|
|
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 services/, published
|
|
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 {
|
|
21
|
+
export { list_in, read_in, search_in, write_in } from "./files.ts";
|
|
22
22
|
|
|
23
23
|
// Mail in, mail out.
|
|
24
|
-
export {
|
|
25
|
-
export {
|
|
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 {
|
|
28
|
+
export { read_web } from "./web.ts";
|
package/model/tools/memory.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
19
|
-
read_notes:
|
|
20
|
-
search_notes:
|
|
21
|
-
write_notes:
|
|
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
|
}
|
|
@@ -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 {
|
|
11
|
+
import { listScripts, runScripts as runOne } 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. */
|
|
@@ -25,8 +25,8 @@ export function runScript(agent: string) {
|
|
|
25
25
|
}),
|
|
26
26
|
execute: async ({ script: name, args = [], timeoutSeconds }) =>
|
|
27
27
|
name === "list"
|
|
28
|
-
? { scripts: await
|
|
29
|
-
:
|
|
28
|
+
? { scripts: await listScripts(agent) }
|
|
29
|
+
: runOne(agent, name, args, { timeoutMs: (timeoutSeconds ?? 300) * 1000 }),
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// writes is the subject and the body.
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
|
|
7
|
-
import { type EmailSender, sendEmail
|
|
7
|
+
import { type EmailSender, sendEmail } from "#chloe/services/emailService.ts";
|
|
8
8
|
import { tool } from "#chloe/model/tool.ts";
|
|
9
9
|
|
|
10
10
|
interface Options extends EmailSender {
|
|
@@ -16,7 +16,7 @@ interface Options extends EmailSender {
|
|
|
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
|
|
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, ...sender }: Options) {
|
|
|
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 }) =>
|
|
27
|
+
execute: ({ subject, body }) => sendEmail(sender, subject, body),
|
|
28
28
|
});
|
|
29
29
|
}
|
package/model/tools/web.ts
CHANGED
|
@@ -5,9 +5,9 @@ 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
|
|
8
|
+
export function read_web() {
|
|
9
9
|
return tool({
|
|
10
|
-
id: "
|
|
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 {
|
|
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
|
|
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
|
@@ -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: "
|
|
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: "
|
|
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 & 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.
|
|
3
|
+
"version": "0.2.4",
|
|
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
|
|
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",
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
216
|
+
const written = await writeFiles(folder(agent), path, content, {
|
|
217
217
|
commit,
|
|
218
218
|
message: commit ? `memory: ${path} from the site` : undefined,
|
|
219
219
|
});
|
package/services/filesService.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { confine } from "#chloe/core/confine.ts";
|
|
|
18
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
|
|
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
|
|
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
|
|
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
|
|
71
|
+
export async function writeFiles(
|
|
72
72
|
root: string,
|
|
73
73
|
path: string,
|
|
74
74
|
content: string,
|
package/services/gmailService.ts
CHANGED
|
@@ -35,7 +35,7 @@ export interface Message {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/** What the bound search matches. Nothing here widens it. */
|
|
38
|
-
export async function
|
|
38
|
+
export async function readEmailMessages({
|
|
39
39
|
search,
|
|
40
40
|
days = 7,
|
|
41
41
|
limit = 10,
|
|
@@ -55,7 +55,7 @@ export async function messages({
|
|
|
55
55
|
* filter. Same check as the tool, because a job is not more trusted than a
|
|
56
56
|
* model here: it is only more predictable.
|
|
57
57
|
*/
|
|
58
|
-
export async function
|
|
58
|
+
export async function readOneEmailMessage({
|
|
59
59
|
search,
|
|
60
60
|
what,
|
|
61
61
|
days = 7,
|
|
@@ -68,7 +68,7 @@ export async function oneMessage({
|
|
|
68
68
|
limit?: number;
|
|
69
69
|
messageId: string;
|
|
70
70
|
}): Promise<{ query: string; message: unknown }> {
|
|
71
|
-
const { query, messages: found } = await
|
|
71
|
+
const { query, messages: found } = await readEmailMessages({ search, days, limit });
|
|
72
72
|
if (!found.some((m) => m.id === messageId || m.threadId === messageId)) {
|
|
73
73
|
throw new Error(
|
|
74
74
|
`That message is not in ${what}. You can only read what this search listed. ` +
|
|
@@ -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 { readEmailMessages, readOneEmailMessage, type Message as Mail } from "./gmailService.ts";
|
|
14
|
+
export { listFiles, readFiles, searchFiles, writeFiles } from "./filesService.ts";
|
|
15
|
+
export { listScripts, runScripts } from "./scriptsService.ts";
|
|
16
|
+
export { readPage, htmlToText, isPrivate, type Page } from "./webService.ts";
|
|
@@ -14,7 +14,7 @@ import { settings } from "#chloe/core/settings.ts";
|
|
|
14
14
|
import { run, type Result } from "./runService.ts";
|
|
15
15
|
|
|
16
16
|
/** What this agent has in scripts/, sorted. Nothing hidden. */
|
|
17
|
-
export async function
|
|
17
|
+
export async function listScripts(agent: string): Promise<string[]> {
|
|
18
18
|
const dir = `${agentDir(agent)}/scripts`;
|
|
19
19
|
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
20
20
|
return entries
|
|
@@ -30,13 +30,13 @@ export async function scripts(agent: string): Promise<string[]> {
|
|
|
30
30
|
* `cwd` defaults to the scripts folder, so a script may use relative paths.
|
|
31
31
|
* An agent whose scripts work on a tree somewhere else passes that instead.
|
|
32
32
|
*/
|
|
33
|
-
export async function
|
|
33
|
+
export async function runScripts(
|
|
34
34
|
agent: string,
|
|
35
35
|
name: string,
|
|
36
36
|
args: string[] = [],
|
|
37
37
|
{ timeoutMs = 300_000, cwd }: { timeoutMs?: number; cwd?: string } = {},
|
|
38
38
|
): Promise<Result & { script: string; args: string[] }> {
|
|
39
|
-
const available = await
|
|
39
|
+
const available = await listScripts(agent);
|
|
40
40
|
if (!available.includes(name)) {
|
|
41
41
|
throw new Error(`No script called ${JSON.stringify(name)}. You have: ${available.join(", ")}`);
|
|
42
42
|
}
|