@chloejs/core 0.2.3 → 0.3.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/services/index.ts CHANGED
@@ -9,8 +9,8 @@
9
9
  // name here is publishing it.
10
10
 
11
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";
12
+ export { markdownToHtml, markdownToText, sendEmail, type EmailSender } from "./emailService.ts";
13
+ export { readEmailMessages, readOneEmailMessage, type Message as Mail } from "./gmailService.ts";
14
14
  export { listFiles, readFiles, searchFiles, writeFiles } from "./filesService.ts";
15
- export { script, scripts } from "./scriptsService.ts";
15
+ export { listScripts, runScripts } from "./scriptsService.ts";
16
16
  export { readPage, htmlToText, isPrivate, type Page } from "./webService.ts";
@@ -0,0 +1,141 @@
1
+ // An agent's own folder, as the agent itself may see and change it: its
2
+ // instructions, skills and jobs, and whatever other text its definition lets
3
+ // it write.
4
+ //
5
+ // Reading is anything in the folder except its memory, which has tools of its
6
+ // own. Writing is narrower, and every rule is here in code rather than in a
7
+ // prompt: only the file endings its definition lists, never code, never a
8
+ // path its definition keeps back, and never a file somebody else is in the
9
+ // middle of changing. A write is a commit of that one file under the agent's
10
+ // name, so every change can be read and undone.
11
+ import { existsSync, realpathSync, statSync } from "node:fs";
12
+ import { readdir } from "node:fs/promises";
13
+ import { extname, join, relative } from "node:path";
14
+
15
+ import { confine, unreachable } from "#chloe/core/confine.ts";
16
+ import { markdownJobProblem, type Home, type OwnFileRules } from "#chloe/load/load.ts";
17
+ import { parse } from "#chloe/timer/cron.ts";
18
+ import { settingsAndBody } from "#chloe/core/markdown.ts";
19
+ import { readFiles, writeFiles } from "./filesService.ts";
20
+ import { uncommitted } from "./historyService.ts";
21
+
22
+ /** Folders of code, or of what marks the agent's runs. Never written, whatever `files` says. */
23
+ const KEPT_BACK = ["tools", "services", "channels", "scripts", "evals"];
24
+
25
+ /** Endings that are code. Never written, whatever `files` says. */
26
+ const CODE = ["ts", "tsx", "js", "jsx", "mjs", "cjs", "py", "sh"];
27
+
28
+ /** Made by a program, not by anybody. */
29
+ const JUNK = ["node_modules", "__pycache__"];
30
+
31
+ /** A path inside the agent's folder, as the folder sees it, or a throw when it leaves it. */
32
+ function within(agent: Home, path: string): string {
33
+ return relative(realpathSync(agent.folder), confine(agent.folder, path));
34
+ }
35
+
36
+ function inMemory(agent: Home, path: string): boolean {
37
+ const real = (folder: string) => (existsSync(folder) ? realpathSync(folder) : folder);
38
+ const memory = relative(real(agent.folder), real(agent.memory.folder));
39
+ return !memory.startsWith("..") && (path === memory || path.startsWith(`${memory}/`));
40
+ }
41
+
42
+ /** Why the agent may not write this path, or undefined when it may. */
43
+ export function whyNot(agent: Home, rules: OwnFileRules, path: string): string | undefined {
44
+ const at = within(agent, path);
45
+ if (inMemory(agent, at)) return "that is your memory, which you write with write_notes";
46
+ const top = at.split("/")[0];
47
+ if (KEPT_BACK.includes(top)) return top === "evals" ? "evals/ is how your runs are marked" : `${top}/ is code`;
48
+ const ending = extname(at).slice(1).toLowerCase();
49
+ if (CODE.includes(ending)) return "it is code";
50
+ if (!rules.files.map((one) => one.replace(/^\./, "").toLowerCase()).includes(ending)) {
51
+ return `only files ending in ${rules.files.map((one) => `.${one.replace(/^\./, "")}`).join(", ")} can be written`;
52
+ }
53
+ if ((rules.except ?? []).some((one) => within(agent, one) === at)) return "it is kept back for a person to change";
54
+ if (/^(skills|jobs)\/[^/]+\//.test(at)) return `a file in a folder inside ${top}/ is never read`;
55
+ if (top === "skills" && at !== "skills" && ending !== "md") return "a skill is one markdown file, and anything else in skills/ is never read";
56
+ return undefined;
57
+ }
58
+
59
+ /** Every file in the agent's folder outside its memory, and whether it may write each one. */
60
+ export async function listOwn(agent: Home, rules: OwnFileRules) {
61
+ const files: { path: string; canWrite: boolean; why?: string }[] = [];
62
+ const walk = async (dir: string, depth: number): Promise<void> => {
63
+ for (const entry of (await readdir(join(agent.folder, dir), { withFileTypes: true }).catch(() => [])).sort((a, b) =>
64
+ a.name.localeCompare(b.name),
65
+ )) {
66
+ const at = dir ? `${dir}/${entry.name}` : entry.name;
67
+ if (entry.name.startsWith(".") || JUNK.includes(entry.name) || unreachable(entry.name) || inMemory(agent, at)) continue;
68
+ if (entry.isDirectory()) {
69
+ if (depth < 6) await walk(at, depth + 1);
70
+ } else if (files.length < 500) {
71
+ const why = whyNot(agent, rules, at);
72
+ files.push({ path: at, canWrite: !why, ...(why && { why }) });
73
+ }
74
+ }
75
+ };
76
+ await walk("", 0);
77
+ return { files };
78
+ }
79
+
80
+ /** One file in the agent's folder, and whether it may write it. */
81
+ export async function readOwn(agent: Home, rules: OwnFileRules, path: string) {
82
+ const at = within(agent, path);
83
+ if (inMemory(agent, at)) throw new Error(`${path} is in your memory: read it with read_notes.`);
84
+ const resolved = confine(agent.folder, at);
85
+ if (existsSync(resolved) && statSync(resolved).isDirectory()) {
86
+ throw new Error(`${path} is a folder. list_own_files shows what is in it.`);
87
+ }
88
+ const { content } = await readFiles(agent.folder, at);
89
+ const why = whyNot(agent, rules, at);
90
+ return { path: at, content, canWrite: !why, ...(why && { why }) };
91
+ }
92
+
93
+ /**
94
+ * Replaces one file in the agent's folder and commits it under the agent's
95
+ * name. Refused, with the reason, when the rules keep it back, when it has
96
+ * changes nobody has committed (they would go in under the agent's name), or
97
+ * when what is written would not load: a job that does not read, a job made to
98
+ * run more than once an hour, JSON that does not parse.
99
+ */
100
+ export async function writeOwn(agent: Home, rules: OwnFileRules, path: string, content: string, message: string) {
101
+ const why = whyNot(agent, rules, path);
102
+ if (why) throw new Error(`You cannot write ${path}: ${why}.`);
103
+ const at = within(agent, path);
104
+ // An empty instructions file stops the agent loading, and so every reload after it.
105
+ if (!content.trim()) throw new Error(`${at} would be empty. Write what it should say.`);
106
+ const resolved = confine(agent.folder, at);
107
+ if (await uncommitted(resolved)) {
108
+ throw new Error(
109
+ `${at} has changes nobody has committed yet, and writing it would put them under your name. ` +
110
+ "Leave it for now, and say that you could not change it and why.",
111
+ );
112
+ }
113
+
114
+ if (/^jobs\/[^/]+\.md$/.test(at) && !existsSync(resolved.replace(/\.md$/, ".ts"))) {
115
+ // Jobs are named in agent.ts, which is code and so not yours. A new file
116
+ // here would be written, committed and never run.
117
+ if (!existsSync(resolved)) {
118
+ throw new Error(
119
+ `${at} would be a new job, and a job only runs once it is named in agent.ts, which only a person can change. ` +
120
+ "Ask for it, and change a job that is already there meanwhile.",
121
+ );
122
+ }
123
+ const problem = markdownJobProblem(content);
124
+ if (problem) throw new Error(`${at} would not load as a job: ${problem}.`);
125
+ const cron = settingsAndBody(content).settings.cron;
126
+ const before = existsSync(resolved) ? (await readFiles(agent.folder, at)).content : "";
127
+ if (cron && cron !== settingsAndBody(before).settings.cron && parse(cron).minute.length > 1) {
128
+ throw new Error(`A job you write runs at most once an hour: give its cron line one minute, like "0 7 * * *".`);
129
+ }
130
+ }
131
+ if (extname(at).toLowerCase() === ".json") {
132
+ try {
133
+ JSON.parse(content);
134
+ } catch (error) {
135
+ throw new Error(`${at} is not valid JSON: ${error instanceof Error ? error.message : String(error)}.`);
136
+ }
137
+ }
138
+
139
+ const written = await writeFiles(agent.folder, at, content, { commit: true, message, author: agent.name, in: "folder" });
140
+ return { path: at, bytes: written.bytes, commit: written.commit };
141
+ }
@@ -9,12 +9,12 @@
9
9
  // these from a step.
10
10
  import { readdir } from "node:fs/promises";
11
11
 
12
- import { agentDir } from "#chloe/core/paths.ts";
12
+ import { agentDir, memoryDir } from "#chloe/core/paths.ts";
13
13
  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 scripts(agent: string): Promise<string[]> {
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,20 +30,25 @@ 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 script(
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 scripts(agent);
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
  }
43
43
  const dir = `${agentDir(agent)}/scripts`;
44
- // A script is someone else's program, so what it needs from settings reaches
45
- // it the way a program expects, as an environment variable.
46
- const env: Record<string, string> = settings.google.GA_KEY_FILE ? { GA_KEY_FILE: settings.google.GA_KEY_FILE } : {};
44
+ // A script is someone else's program, so what it needs reaches it the way a
45
+ // program expects, as an environment variable: its agent's memory folder as
46
+ // MEMORY_FOLDER, and what it needs from settings.
47
+ const memory = memoryDir(agent);
48
+ const env: Record<string, string> = {
49
+ ...(memory && { MEMORY_FOLDER: memory }),
50
+ ...(settings.google.GA_KEY_FILE && { GA_KEY_FILE: settings.google.GA_KEY_FILE }),
51
+ };
47
52
  const result = await run(`${dir}/${name}`, args, { timeoutMs, cwd: cwd ?? dir, env });
48
53
  return { script: name, args, ...result };
49
54
  }
@@ -1,31 +0,0 @@
1
- // An agent rewriting one of its own skills.
2
- //
3
- // This is the self-improving part, and it is deliberately the only part. A
4
- // skill is markdown: when to do something, which script does it, and what the
5
- // output means. An agent that learns something about its own job can put it
6
- // where the next run will read it, and the change is live in about fifteen
7
- // seconds with no restart.
8
- //
9
- // What an agent may NOT write is its tools and its scripts/ folder. Those are
10
- // code, and code it writes is code it then runs as itself. A skill can only
11
- // point at a script that a person already put there.
12
- //
13
- // Every write is a git commit, so self-improvement always leaves a diff.
14
- import { write_in } from "./files.ts";
15
- import { agentDir } from "#chloe/core/paths.ts";
16
- import type { Tools } from "../tool.ts";
17
-
18
- /** A tool that rewrites one of the agent's own skills. Every write is a commit. */
19
- export function writeSkill(agent: string) {
20
- return write_in({
21
- root: `${agentDir(agent)}/skills`,
22
- what: "your own skills",
23
- id: "write_skill",
24
- commit: true,
25
- });
26
- }
27
-
28
- /** write_skill: the agent rewrites its own skills, and every write is a commit. */
29
- export function selfImprovement(): (agent: { name: string }) => Tools {
30
- return ({ name }) => ({ write_skill: writeSkill(name) });
31
- }