@getfamiliar/shared 0.1.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/build/AgentRun.d.ts +187 -0
- package/build/AgentRun.js +1 -0
- package/build/AgentRunBus.d.ts +147 -0
- package/build/AgentRunBus.js +362 -0
- package/build/Calendar.d.ts +450 -0
- package/build/Calendar.js +37 -0
- package/build/ChatMessage.d.ts +53 -0
- package/build/ChatMessage.js +11 -0
- package/build/ChatMessageBus.d.ts +110 -0
- package/build/ChatMessageBus.js +257 -0
- package/build/Config.d.ts +83 -0
- package/build/Config.js +1 -0
- package/build/ContainerTool.d.ts +34 -0
- package/build/ContainerTool.js +1 -0
- package/build/CronExpression.d.ts +53 -0
- package/build/CronExpression.js +77 -0
- package/build/Event.d.ts +280 -0
- package/build/Event.js +14 -0
- package/build/EventBus.d.ts +198 -0
- package/build/EventBus.js +482 -0
- package/build/HandlerCatalog.d.ts +75 -0
- package/build/HandlerCatalog.js +165 -0
- package/build/InferenceEvent.d.ts +30 -0
- package/build/InferenceEvent.js +1 -0
- package/build/InferenceEventBus.d.ts +39 -0
- package/build/InferenceEventBus.js +80 -0
- package/build/Mail.d.ts +250 -0
- package/build/Mail.js +73 -0
- package/build/MailStyleTemplate.d.ts +71 -0
- package/build/MailStyleTemplate.js +47 -0
- package/build/ModelMetaData.d.ts +63 -0
- package/build/ModelMetaData.js +12 -0
- package/build/PathGlob.d.ts +20 -0
- package/build/PathGlob.js +33 -0
- package/build/Plugin.d.ts +709 -0
- package/build/Plugin.js +29 -0
- package/build/PostgresConnection.d.ts +75 -0
- package/build/PostgresConnection.js +162 -0
- package/build/ScheduledSubagent.d.ts +49 -0
- package/build/ScheduledSubagent.js +1 -0
- package/build/ScheduledSubagentBus.d.ts +94 -0
- package/build/ScheduledSubagentBus.js +187 -0
- package/build/Schema.d.ts +84 -0
- package/build/Schema.js +570 -0
- package/build/StepResult.d.ts +103 -0
- package/build/StepResult.js +1 -0
- package/build/StepResultBus.d.ts +64 -0
- package/build/StepResultBus.js +169 -0
- package/build/Timezone.d.ts +58 -0
- package/build/Timezone.js +84 -0
- package/build/ToolCall.d.ts +25 -0
- package/build/ToolCall.js +1 -0
- package/build/ToolCallBus.d.ts +65 -0
- package/build/ToolCallBus.js +110 -0
- package/build/ToolLevel.d.ts +20 -0
- package/build/ToolLevel.js +2 -0
- package/build/ToolRunner.d.ts +118 -0
- package/build/ToolRunner.js +192 -0
- package/build/ToolsExpressionUtils.d.ts +80 -0
- package/build/ToolsExpressionUtils.js +102 -0
- package/build/WorkspaceFile.d.ts +104 -0
- package/build/WorkspaceFile.js +7 -0
- package/build/estimateTokens.d.ts +16 -0
- package/build/estimateTokens.js +18 -0
- package/build/index.d.ts +37 -0
- package/build/index.js +26 -0
- package/build/logging/Logger.d.ts +66 -0
- package/build/logging/Logger.js +90 -0
- package/build/markdownHtml.d.ts +14 -0
- package/build/markdownHtml.js +21 -0
- package/build/markdownTerminal.d.ts +14 -0
- package/build/markdownTerminal.js +376 -0
- package/build/testing/MockAgentRunBus.d.ts +52 -0
- package/build/testing/MockAgentRunBus.js +175 -0
- package/build/testing/MockBusStore.d.ts +22 -0
- package/build/testing/MockBusStore.js +22 -0
- package/build/testing/MockEventBus.d.ts +20 -0
- package/build/testing/MockEventBus.js +57 -0
- package/build/testing/MockLogger.d.ts +52 -0
- package/build/testing/MockLogger.js +65 -0
- package/build/testing/index.d.ts +4 -0
- package/build/testing/index.js +4 -0
- package/package.json +63 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { matchesAnyGlob } from "./PathGlob.js";
|
|
4
|
+
/**
|
|
5
|
+
* Workspace files at the very top level that are not handlers and
|
|
6
|
+
* must be filtered out of {@link HandlerCatalog.list}. These are the
|
|
7
|
+
* documents read by `buildSystemPrompt` rather than executed as
|
|
8
|
+
* handlers (see `CLAUDE.md` — markdown layers / global context).
|
|
9
|
+
*/
|
|
10
|
+
const RESERVED_ROOT_FILES = new Set(["SOUL.md", "CONTEXT.md"]);
|
|
11
|
+
/**
|
|
12
|
+
* Workspace subtrees that contain markdown but never handlers.
|
|
13
|
+
* `skills/<name>/SKILL.md` is a shared recipe loaded via `fs_read`,
|
|
14
|
+
* not an agent entry point.
|
|
15
|
+
*/
|
|
16
|
+
const RESERVED_TOP_DIRS = new Set(["skills"]);
|
|
17
|
+
/**
|
|
18
|
+
* Read-only view over the `workspace/` directory's handler files.
|
|
19
|
+
*
|
|
20
|
+
* Mirrors the resolution semantics implemented by
|
|
21
|
+
* `container/src/HandlerFile.ts` so host-side code (CLI, cron
|
|
22
|
+
* scheduler introspection, tab-completion) can answer "does this
|
|
23
|
+
* handler exist?" and "list every handler" without depending on
|
|
24
|
+
* container internals or reimplementing the topic-walk inheritance.
|
|
25
|
+
*
|
|
26
|
+
* The catalog is stateless on the disk — every method re-scans —
|
|
27
|
+
* so live workspace edits are reflected without a daemon restart.
|
|
28
|
+
* At REPL frequencies this is cheap; if a hot path later needs
|
|
29
|
+
* caching it can wrap an instance.
|
|
30
|
+
*/
|
|
31
|
+
export class HandlerCatalog {
|
|
32
|
+
workspaceDir;
|
|
33
|
+
writablePathGlobs;
|
|
34
|
+
/**
|
|
35
|
+
* @param workspaceDir Absolute path of the `workspace/` directory.
|
|
36
|
+
* @param writablePathGlobs `core.writablePaths` globs (e.g. `wiki/**`).
|
|
37
|
+
* Files under these are writable by non-privileged runs and are
|
|
38
|
+
* therefore never handlers — they are excluded from {@link list}
|
|
39
|
+
* and {@link resolve} so they are never proposed or resolved as a
|
|
40
|
+
* handler. Mirrors the container's `HandlerFile.load` guard.
|
|
41
|
+
*/
|
|
42
|
+
constructor(workspaceDir, writablePathGlobs = []) {
|
|
43
|
+
this.workspaceDir = workspaceDir;
|
|
44
|
+
this.writablePathGlobs = writablePathGlobs;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Walk `workspace/` and return one {@link HandlerPath} per
|
|
48
|
+
* candidate handler file. Excludes the global-context files at
|
|
49
|
+
* the workspace root (`SOUL.md`, `CONTEXT.md`)
|
|
50
|
+
* and the `skills/` subtree. Every other `.md` file under a topic
|
|
51
|
+
* directory is included, even if it's a knowledge file (e.g.
|
|
52
|
+
* `people/anna.md`) — `HandlerCatalog` does not police what is or
|
|
53
|
+
* isn't a "proper" handler; that's a handler-loading concern.
|
|
54
|
+
*
|
|
55
|
+
* Sorted by `slashPath` for stable display ordering in
|
|
56
|
+
* tab-completion. Returns an empty array when the workspace
|
|
57
|
+
* directory does not exist.
|
|
58
|
+
*/
|
|
59
|
+
async list() {
|
|
60
|
+
const exists = await directoryExists(this.workspaceDir);
|
|
61
|
+
if (!exists) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
const results = [];
|
|
65
|
+
await walkMarkdown(this.workspaceDir, [], results, this.workspaceDir, this.writablePathGlobs);
|
|
66
|
+
results.sort((a, b) => a.slashPath.localeCompare(b.slashPath));
|
|
67
|
+
return results;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Resolve a handler call: given a topic (e.g. `chat:telegram`)
|
|
71
|
+
* and a basename (e.g. `index`), return the absolute path of the
|
|
72
|
+
* deepest existing handler file, mirroring the container's
|
|
73
|
+
* inheritance walk.
|
|
74
|
+
*
|
|
75
|
+
* For topic `a:b:c` and basename `x` the resolution tries, in
|
|
76
|
+
* order: `a/b/c/x.md`, `a/b/x.md`, `a/x.md`. Returns the first
|
|
77
|
+
* existing path or `null` if none exist. The deepest existing
|
|
78
|
+
* file is what `HandlerFile.load` would treat as the leaf — for
|
|
79
|
+
* cli-chat existence checks, that's exactly the question we
|
|
80
|
+
* want answered.
|
|
81
|
+
*/
|
|
82
|
+
async resolve(topic, handler) {
|
|
83
|
+
const segments = topic.split(":");
|
|
84
|
+
for (let depth = segments.length; depth >= 1; depth--) {
|
|
85
|
+
const rel = path.join(...segments.slice(0, depth), `${handler}.md`);
|
|
86
|
+
const relPosix = rel.split(path.sep).join("/");
|
|
87
|
+
const absolute = path.join(this.workspaceDir, rel);
|
|
88
|
+
if (await fileExists(absolute)) {
|
|
89
|
+
// A file under `core.writablePaths` is never a handler, even
|
|
90
|
+
// when it exists — mirror the container's `HandlerFile.load`
|
|
91
|
+
// guard so host-side callers agree it does not resolve.
|
|
92
|
+
if (matchesAnyGlob(this.writablePathGlobs, relPosix)) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return absolute;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Recursively walk `dir`, accumulating one {@link HandlerPath} per
|
|
103
|
+
* `.md` file. `segments` tracks the topic path from the workspace
|
|
104
|
+
* root; an empty array means we're at the root, where reserved files
|
|
105
|
+
* are skipped.
|
|
106
|
+
*/
|
|
107
|
+
async function walkMarkdown(dir, segments, out, workspaceDir, writablePathGlobs) {
|
|
108
|
+
let entries;
|
|
109
|
+
try {
|
|
110
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
if (entry.isDirectory()) {
|
|
117
|
+
if (segments.length === 0 && RESERVED_TOP_DIRS.has(entry.name)) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
await walkMarkdown(path.join(dir, entry.name), [...segments, entry.name], out, workspaceDir, writablePathGlobs);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (segments.length === 0 && RESERVED_ROOT_FILES.has(entry.name)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (segments.length === 0) {
|
|
130
|
+
// Stray `.md` at the workspace root with no topic folder —
|
|
131
|
+
// not a handler. Skip silently rather than reporting an
|
|
132
|
+
// empty topic.
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const handlerBase = entry.name.slice(0, -".md".length);
|
|
136
|
+
const topic = segments.join(":");
|
|
137
|
+
const absolutePath = path.join(dir, entry.name);
|
|
138
|
+
const relativePath = path.relative(workspaceDir, absolutePath).split(path.sep).join("/");
|
|
139
|
+
// Files under `core.writablePaths` (e.g. `wiki/**`) are writable by
|
|
140
|
+
// non-privileged runs and are never handlers — never propose them.
|
|
141
|
+
if (matchesAnyGlob(writablePathGlobs, relativePath)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const slashPath = `/${[...segments, handlerBase].join("/")}`;
|
|
145
|
+
out.push({ topic, handler: handlerBase, absolutePath, relativePath, slashPath });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function fileExists(absolute) {
|
|
149
|
+
try {
|
|
150
|
+
const stat = await fs.stat(absolute);
|
|
151
|
+
return stat.isFile();
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function directoryExists(absolute) {
|
|
158
|
+
try {
|
|
159
|
+
const stat = await fs.stat(absolute);
|
|
160
|
+
return stat.isDirectory();
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One model HTTP call outcome.
|
|
3
|
+
*
|
|
4
|
+
* - `success` — the upstream returned a 2xx the runner accepted.
|
|
5
|
+
* - `retryable` — the upstream returned 408/429/5xx or otherwise signalled
|
|
6
|
+
* transient failure; AgentRunner postponed the agentrun.
|
|
7
|
+
* - `fatal` — non-retryable upstream error (auth, malformed request,
|
|
8
|
+
* unknown model id) that immediately settled the agentrun as `failed`.
|
|
9
|
+
*/
|
|
10
|
+
export type InferenceOutcome = "success" | "retryable" | "fatal";
|
|
11
|
+
/** Persisted shape of an `inference_events` row. */
|
|
12
|
+
export interface InferenceEventRow {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly provider: string;
|
|
15
|
+
readonly model: string;
|
|
16
|
+
readonly agentRunId: string | null;
|
|
17
|
+
readonly outcome: InferenceOutcome;
|
|
18
|
+
readonly statusCode: number | null;
|
|
19
|
+
readonly errorExcerpt: string | null;
|
|
20
|
+
readonly occurredAt: Date;
|
|
21
|
+
}
|
|
22
|
+
/** Insert shape — `id` and `occurredAt` are filled by the DB. */
|
|
23
|
+
export interface NewInferenceEvent {
|
|
24
|
+
readonly provider: string;
|
|
25
|
+
readonly model: string;
|
|
26
|
+
readonly agentRunId?: string | null;
|
|
27
|
+
readonly outcome: InferenceOutcome;
|
|
28
|
+
readonly statusCode?: number | null;
|
|
29
|
+
readonly errorExcerpt?: string | null;
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { InferenceEventRow, NewInferenceEvent } from "./InferenceEvent.js";
|
|
2
|
+
import type { PostgresConnection } from "./PostgresConnection.js";
|
|
3
|
+
/**
|
|
4
|
+
* Domain client for the `inference_events` table — every upstream model
|
|
5
|
+
* HTTP call AgentRunner attempted, classified into success / retryable /
|
|
6
|
+
* fatal. Append-only: there is no update or delete on this table outside
|
|
7
|
+
* the global retention sweep. Mirrors {@link import("./AgentRunBus.js").AgentRunBus}
|
|
8
|
+
* in its constructor + Pool access pattern; does not own connection
|
|
9
|
+
* lifecycle.
|
|
10
|
+
*/
|
|
11
|
+
export declare class InferenceEventBus {
|
|
12
|
+
private readonly connection;
|
|
13
|
+
constructor(connection: PostgresConnection);
|
|
14
|
+
/**
|
|
15
|
+
* Apply the bus-state schema (idempotent). Same SQL bundle every
|
|
16
|
+
* other bus client installs; calling any one is sufficient on
|
|
17
|
+
* daemon start.
|
|
18
|
+
*/
|
|
19
|
+
installSchema(): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Insert one inference event. Fire-and-forget from AgentRunner; any
|
|
22
|
+
* thrown error is the caller's responsibility to log — we don't want
|
|
23
|
+
* to mask a real model error with an audit-table write error.
|
|
24
|
+
*/
|
|
25
|
+
add(row: NewInferenceEvent): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Fetch every inference event with `occurred_at >= since`, optionally
|
|
28
|
+
* filtered to one model id. Ordered by `occurred_at DESC` so the
|
|
29
|
+
* reflection tool's "last successful call" lookup is the first hit.
|
|
30
|
+
*/
|
|
31
|
+
listSince(since: Date, model?: string): Promise<readonly InferenceEventRow[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Delete rows older than `cutoff`. Returns the number of rows
|
|
34
|
+
* removed. Run from the same retention pass that prunes daemon log
|
|
35
|
+
* files so the two retention policies stay aligned with
|
|
36
|
+
* `core.logRetentionDays`.
|
|
37
|
+
*/
|
|
38
|
+
pruneBefore(cutoff: Date): Promise<number>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { SCHEMA_SQL } from "./Schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Domain client for the `inference_events` table — every upstream model
|
|
4
|
+
* HTTP call AgentRunner attempted, classified into success / retryable /
|
|
5
|
+
* fatal. Append-only: there is no update or delete on this table outside
|
|
6
|
+
* the global retention sweep. Mirrors {@link import("./AgentRunBus.js").AgentRunBus}
|
|
7
|
+
* in its constructor + Pool access pattern; does not own connection
|
|
8
|
+
* lifecycle.
|
|
9
|
+
*/
|
|
10
|
+
export class InferenceEventBus {
|
|
11
|
+
connection;
|
|
12
|
+
constructor(connection) {
|
|
13
|
+
this.connection = connection;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Apply the bus-state schema (idempotent). Same SQL bundle every
|
|
17
|
+
* other bus client installs; calling any one is sufficient on
|
|
18
|
+
* daemon start.
|
|
19
|
+
*/
|
|
20
|
+
async installSchema() {
|
|
21
|
+
await this.connection.getPool().query(SCHEMA_SQL);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Insert one inference event. Fire-and-forget from AgentRunner; any
|
|
25
|
+
* thrown error is the caller's responsibility to log — we don't want
|
|
26
|
+
* to mask a real model error with an audit-table write error.
|
|
27
|
+
*/
|
|
28
|
+
async add(row) {
|
|
29
|
+
await this.connection.getPool().query(`INSERT INTO inference_events
|
|
30
|
+
(provider, model, agent_run_id, outcome, status_code, error_excerpt)
|
|
31
|
+
VALUES ($1, $2, $3, $4, $5, $6)`, [
|
|
32
|
+
row.provider,
|
|
33
|
+
row.model,
|
|
34
|
+
row.agentRunId ?? null,
|
|
35
|
+
row.outcome,
|
|
36
|
+
row.statusCode ?? null,
|
|
37
|
+
row.errorExcerpt ?? null,
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Fetch every inference event with `occurred_at >= since`, optionally
|
|
42
|
+
* filtered to one model id. Ordered by `occurred_at DESC` so the
|
|
43
|
+
* reflection tool's "last successful call" lookup is the first hit.
|
|
44
|
+
*/
|
|
45
|
+
async listSince(since, model) {
|
|
46
|
+
const pool = this.connection.getPool();
|
|
47
|
+
const result = model === undefined
|
|
48
|
+
? await pool.query(`SELECT * FROM inference_events
|
|
49
|
+
WHERE occurred_at >= $1
|
|
50
|
+
ORDER BY occurred_at DESC`, [since])
|
|
51
|
+
: await pool.query(`SELECT * FROM inference_events
|
|
52
|
+
WHERE occurred_at >= $1 AND model = $2
|
|
53
|
+
ORDER BY occurred_at DESC`, [since, model]);
|
|
54
|
+
return result.rows.map(mapRow);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Delete rows older than `cutoff`. Returns the number of rows
|
|
58
|
+
* removed. Run from the same retention pass that prunes daemon log
|
|
59
|
+
* files so the two retention policies stay aligned with
|
|
60
|
+
* `core.logRetentionDays`.
|
|
61
|
+
*/
|
|
62
|
+
async pruneBefore(cutoff) {
|
|
63
|
+
const result = await this.connection
|
|
64
|
+
.getPool()
|
|
65
|
+
.query(`DELETE FROM inference_events WHERE occurred_at < $1`, [cutoff]);
|
|
66
|
+
return result.rowCount ?? 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function mapRow(raw) {
|
|
70
|
+
return {
|
|
71
|
+
id: raw.id,
|
|
72
|
+
provider: raw.provider,
|
|
73
|
+
model: raw.model,
|
|
74
|
+
agentRunId: raw.agent_run_id,
|
|
75
|
+
outcome: raw.outcome,
|
|
76
|
+
statusCode: raw.status_code,
|
|
77
|
+
errorExcerpt: raw.error_excerpt,
|
|
78
|
+
occurredAt: raw.occurred_at,
|
|
79
|
+
};
|
|
80
|
+
}
|
package/build/Mail.d.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin-agnostic mail model. Lives in `shared/` because both the host
|
|
3
|
+
* (which owns the `mail_*` agent tools and the safety gate) and the
|
|
4
|
+
* plugin providers (which translate the typed interface into provider
|
|
5
|
+
* API calls) trade in the same shapes.
|
|
6
|
+
*
|
|
7
|
+
* The core has no mail cache of its own — providers fetch live every
|
|
8
|
+
* time. Plugin pollers still emit `mail:<plugin>` events to wake
|
|
9
|
+
* handlers, but the persisted entity is the event row; the mail body
|
|
10
|
+
* and attachments are pulled on demand through the provider.
|
|
11
|
+
*
|
|
12
|
+
* **Mail ids** are provider-prefixed strings: `<pluginId>:<mailbox>:<realId>`.
|
|
13
|
+
* The mailbox segment lets the host route a tool call to the correct
|
|
14
|
+
* provider AND the correct logged-in account without needing a separate
|
|
15
|
+
* mailbox registry. {@link buildMailId} / {@link parseMailId} are the
|
|
16
|
+
* canonical helpers; raw string concatenation is forbidden.
|
|
17
|
+
*
|
|
18
|
+
* **Folders.** The core uses three abstract aliases — `inbox`, `archive`,
|
|
19
|
+
* `trash` — which each provider maps to its native folder names
|
|
20
|
+
* (e.g. ms365 → `inbox`, `archive`, `deleteditems`; gmail eventually →
|
|
21
|
+
* `INBOX`, `[Gmail]/All Mail`, `[Gmail]/Trash`).
|
|
22
|
+
*/
|
|
23
|
+
export type MailFolder = "inbox" | "archive" | "trash" | "sent";
|
|
24
|
+
/**
|
|
25
|
+
* One downloaded attachment. The core tool stages these into the
|
|
26
|
+
* agentrun's scratch directory; the provider supplies the raw bytes.
|
|
27
|
+
*/
|
|
28
|
+
export interface MailAttachment {
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly contents: Buffer;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Sanitized address bundle as it appears in the `mail:new` event
|
|
34
|
+
* payload and in {@link MailSearchHit} results. `address` is always
|
|
35
|
+
* safe to use as a filename component; `rawAddress` is `null` when the
|
|
36
|
+
* upstream address passed validation, otherwise it carries the
|
|
37
|
+
* original bytes for audit (handlers must NEVER use it as a path).
|
|
38
|
+
*/
|
|
39
|
+
export interface MailAddress {
|
|
40
|
+
readonly name: string | null;
|
|
41
|
+
readonly address: string;
|
|
42
|
+
readonly rawAddress: string | null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Attachment metadata that ships with a `mail:new` payload and search
|
|
46
|
+
* hits. Bytes never travel through this shape — the agent calls
|
|
47
|
+
* `mail_fetch_attachments` to download contents on demand.
|
|
48
|
+
*/
|
|
49
|
+
export interface MailAttachmentMeta {
|
|
50
|
+
readonly id: string;
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly contentType: string;
|
|
53
|
+
readonly size: number;
|
|
54
|
+
readonly isInline: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Predicates for {@link MailProvider.search}. Date bounds are passed
|
|
58
|
+
* in already-resolved UTC ISO form (the host parses agent-supplied
|
|
59
|
+
* local-tz strings against `core.timezone` before dispatching, so
|
|
60
|
+
* providers never have to know about timezones).
|
|
61
|
+
*/
|
|
62
|
+
export interface MailSearchQuery {
|
|
63
|
+
/** Full-text query, applied across subject/body/from/to/cc. */
|
|
64
|
+
readonly text?: string;
|
|
65
|
+
/** Address that must appear as sender, To, or Cc. */
|
|
66
|
+
readonly contact?: string;
|
|
67
|
+
/** Inclusive lower bound on receivedDateTime (UTC ISO instant). */
|
|
68
|
+
readonly after?: string;
|
|
69
|
+
/** Exclusive upper bound on receivedDateTime (UTC ISO instant). */
|
|
70
|
+
readonly before?: string;
|
|
71
|
+
/** Restrict to one abstract folder; otherwise any folder is in scope. */
|
|
72
|
+
readonly folder?: MailFolder;
|
|
73
|
+
/**
|
|
74
|
+
* Optional mailbox restriction. When omitted the provider searches
|
|
75
|
+
* every mailbox in its configured set, regardless of polling state.
|
|
76
|
+
* When given and not in the configured set, providers may still
|
|
77
|
+
* attempt the call as a best-effort fallback.
|
|
78
|
+
*/
|
|
79
|
+
readonly mailbox?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Hard ceiling on hits to return. The host passes the remaining
|
|
82
|
+
* global budget (initial 100, decremented across providers) so a
|
|
83
|
+
* provider can stop paging early instead of fetching results that
|
|
84
|
+
* would be dropped anyway.
|
|
85
|
+
*/
|
|
86
|
+
readonly limit: number;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* One match returned by {@link MailProvider.search}. Shape is
|
|
90
|
+
* intentionally identical to the `mail:new` event payload so handlers
|
|
91
|
+
* can treat search hits and live mail events with the same code path.
|
|
92
|
+
*/
|
|
93
|
+
export interface MailSearchHit {
|
|
94
|
+
readonly mail_id: string;
|
|
95
|
+
readonly isShared: boolean;
|
|
96
|
+
readonly from: MailAddress;
|
|
97
|
+
readonly to: readonly MailAddress[];
|
|
98
|
+
readonly cc: readonly MailAddress[];
|
|
99
|
+
readonly subject: string;
|
|
100
|
+
readonly date: string;
|
|
101
|
+
readonly internetMessageId: string;
|
|
102
|
+
readonly hasAttachments: boolean;
|
|
103
|
+
readonly attachments: readonly MailAttachmentMeta[] | null;
|
|
104
|
+
/**
|
|
105
|
+
* Folder the hit currently lives in, using the same vocabulary the
|
|
106
|
+
* agent passes to `mail_search`'s `folder` argument and `mail_move`:
|
|
107
|
+
* one of the three known aliases (`inbox` / `archive` / `trash`) or
|
|
108
|
+
* `other` for anything else (custom user folders the agent doesn't
|
|
109
|
+
* have a verb for today). `null` only when the provider couldn't
|
|
110
|
+
* resolve the folder for a given hit.
|
|
111
|
+
*/
|
|
112
|
+
readonly folder: MailFolder | "other" | null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Composition input for {@link MailProvider.draftReply} /
|
|
116
|
+
* {@link MailProvider.sendReply}. The body is markdown; providers render
|
|
117
|
+
* to their native body format (HTML for Graph) before dispatch.
|
|
118
|
+
*/
|
|
119
|
+
export interface ReplyInput {
|
|
120
|
+
readonly bodyMarkdown: string;
|
|
121
|
+
/** When `true`, address every recipient on the original mail. */
|
|
122
|
+
readonly replyAll: boolean;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Composition input for {@link MailProvider.draftForward} /
|
|
126
|
+
* {@link MailProvider.sendForward}. The optional comment renders above
|
|
127
|
+
* the quoted original. Original attachments are carried over by the
|
|
128
|
+
* provider where possible.
|
|
129
|
+
*/
|
|
130
|
+
export interface ForwardInput {
|
|
131
|
+
readonly to: readonly string[];
|
|
132
|
+
readonly cc: readonly string[];
|
|
133
|
+
readonly commentMarkdown: string;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Composition input for a brand-new mail (no source message). The
|
|
137
|
+
* "from" mailbox is passed separately to {@link MailProvider.draftNew} /
|
|
138
|
+
* {@link MailProvider.sendNew} as the first argument so the same shape
|
|
139
|
+
* carries through every send/draft variant.
|
|
140
|
+
*/
|
|
141
|
+
export interface NewMailInput {
|
|
142
|
+
readonly to: readonly string[];
|
|
143
|
+
readonly cc: readonly string[];
|
|
144
|
+
readonly subject: string;
|
|
145
|
+
readonly bodyMarkdown: string;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The contract a provider plugin implements + registers via
|
|
149
|
+
* {@link MailApi.registerProvider}. The host's `mail_*` tools dispatch
|
|
150
|
+
* here by parsing the mail id's `<pluginId>:` prefix and looking the
|
|
151
|
+
* provider up in the registry.
|
|
152
|
+
*
|
|
153
|
+
* Methods return raw provider-native ids; the core wraps them with
|
|
154
|
+
* the `<pluginId>:<mailbox>:` prefix before handing them back to the
|
|
155
|
+
* agent. That way every id the agent sees is self-routing.
|
|
156
|
+
*
|
|
157
|
+
* Provider safety (send guards, attendee stripping, etc.) is **not**
|
|
158
|
+
* the provider's concern — those policies live in core (`MailSafety`
|
|
159
|
+
* in the host). Providers focus on the API call.
|
|
160
|
+
*/
|
|
161
|
+
export interface MailProvider {
|
|
162
|
+
/** Identifies this provider in the registry. Matches the plugin id. */
|
|
163
|
+
readonly pluginId: string;
|
|
164
|
+
/** Fetch the plain-text body of one message. */
|
|
165
|
+
fetchBody(mailbox: string, messageId: string): Promise<string>;
|
|
166
|
+
/** Download every non-inline attachment as in-memory buffers. */
|
|
167
|
+
fetchAttachments(mailbox: string, messageId: string): Promise<readonly MailAttachment[]>;
|
|
168
|
+
/**
|
|
169
|
+
* Inspect an existing message and return the recipient set a reply
|
|
170
|
+
* would address (sender always; to/cc lists when `replyAll`). Used
|
|
171
|
+
* by core to gate `send_reply` against the safety whitelist *before*
|
|
172
|
+
* the send call; saves one round trip in the gated path and lets
|
|
173
|
+
* the agent see the recipients in the result envelope.
|
|
174
|
+
*/
|
|
175
|
+
previewReplyRecipients(mailbox: string, messageId: string, replyAll: boolean): Promise<readonly string[]>;
|
|
176
|
+
/** Create a draft reply. Returns the provider-native draft id. */
|
|
177
|
+
draftReply(mailbox: string, messageId: string, input: ReplyInput): Promise<string>;
|
|
178
|
+
/** Create a draft forward. Returns the provider-native draft id. */
|
|
179
|
+
draftForward(mailbox: string, messageId: string, input: ForwardInput): Promise<string>;
|
|
180
|
+
/** Create a brand-new draft under `mailbox`. Returns its id. */
|
|
181
|
+
draftNew(mailbox: string, input: NewMailInput): Promise<string>;
|
|
182
|
+
/**
|
|
183
|
+
* Compose and dispatch a reply immediately. The core has already
|
|
184
|
+
* cleared the safety gate by the time this is called.
|
|
185
|
+
*/
|
|
186
|
+
sendReply(mailbox: string, messageId: string, input: ReplyInput): Promise<void>;
|
|
187
|
+
/** Compose and dispatch a forward immediately. */
|
|
188
|
+
sendForward(mailbox: string, messageId: string, input: ForwardInput): Promise<void>;
|
|
189
|
+
/** Compose and dispatch a brand-new mail immediately. */
|
|
190
|
+
sendNew(mailbox: string, input: NewMailInput): Promise<void>;
|
|
191
|
+
/** Move a message to one of the abstract folder aliases. */
|
|
192
|
+
move(mailbox: string, messageId: string, folder: MailFolder): Promise<void>;
|
|
193
|
+
/**
|
|
194
|
+
* Search this provider's mail for hits matching `query`. The host
|
|
195
|
+
* fans out to every registered provider when no `plugin` filter is
|
|
196
|
+
* set; per-provider results are merged into a single JSONL response
|
|
197
|
+
* to the agent. Implementations should:
|
|
198
|
+
*
|
|
199
|
+
* - target `query.mailbox` when given (best-effort: try even if
|
|
200
|
+
* the address isn't in the configured set), otherwise iterate
|
|
201
|
+
* over every mailbox the provider considers configured —
|
|
202
|
+
* independent of whether that mailbox is being polled;
|
|
203
|
+
* - cap returned hits at `query.limit` (the host's remaining
|
|
204
|
+
* global budget) and stop paging once the cap is hit;
|
|
205
|
+
* - shape each hit identically to the provider's `mail:new`
|
|
206
|
+
* event payload, so downstream tools accept the `mail_id` as
|
|
207
|
+
* self-routing.
|
|
208
|
+
*/
|
|
209
|
+
search(query: MailSearchQuery): Promise<readonly MailSearchHit[]>;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Capabilities the host exposes to plugins for mail data. Reached via
|
|
213
|
+
* `ctx.mail` on a plugin's `HostContext`. Tiny by design: there is no
|
|
214
|
+
* mail cache in core, so the only thing plugins do here is register
|
|
215
|
+
* their provider during `start()`.
|
|
216
|
+
*/
|
|
217
|
+
export interface MailApi {
|
|
218
|
+
/**
|
|
219
|
+
* Register the create / send / fetch callbacks a provider plugin
|
|
220
|
+
* implements. Throws if the same `pluginId` registers twice — a
|
|
221
|
+
* wiring bug, not a feature.
|
|
222
|
+
*/
|
|
223
|
+
registerProvider(provider: MailProvider): void;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Build a self-routing mail id from the three segments. Format:
|
|
227
|
+
* `<pluginId>:<mailbox>:<realId>`. The id is what flows through tool
|
|
228
|
+
* args, event payloads, and agent prompts — the agent never sees the
|
|
229
|
+
* three segments separately.
|
|
230
|
+
*
|
|
231
|
+
* No escaping is applied: mailboxes containing literal `:` (rare;
|
|
232
|
+
* RFC 5321 allows quoted local parts) are unsupported. Graph ids
|
|
233
|
+
* routinely contain `=`, `+`, `/` but never `:`, so the parse split is
|
|
234
|
+
* unambiguous in practice.
|
|
235
|
+
*/
|
|
236
|
+
export declare function buildMailId(pluginId: string, mailbox: string, realId: string): string;
|
|
237
|
+
/**
|
|
238
|
+
* Parse a mail id back into its three segments. The split is on the
|
|
239
|
+
* first two `:` characters; anything after the second colon is the
|
|
240
|
+
* real id (which provider implementations may use freely).
|
|
241
|
+
*
|
|
242
|
+
* @throws If the id has fewer than two `:` separators or any segment
|
|
243
|
+
* is empty. The error message names the offending input so a bad
|
|
244
|
+
* id surfaced from agent args produces an actionable tool failure.
|
|
245
|
+
*/
|
|
246
|
+
export declare function parseMailId(id: string): {
|
|
247
|
+
readonly pluginId: string;
|
|
248
|
+
readonly mailbox: string;
|
|
249
|
+
readonly realId: string;
|
|
250
|
+
};
|
package/build/Mail.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin-agnostic mail model. Lives in `shared/` because both the host
|
|
3
|
+
* (which owns the `mail_*` agent tools and the safety gate) and the
|
|
4
|
+
* plugin providers (which translate the typed interface into provider
|
|
5
|
+
* API calls) trade in the same shapes.
|
|
6
|
+
*
|
|
7
|
+
* The core has no mail cache of its own — providers fetch live every
|
|
8
|
+
* time. Plugin pollers still emit `mail:<plugin>` events to wake
|
|
9
|
+
* handlers, but the persisted entity is the event row; the mail body
|
|
10
|
+
* and attachments are pulled on demand through the provider.
|
|
11
|
+
*
|
|
12
|
+
* **Mail ids** are provider-prefixed strings: `<pluginId>:<mailbox>:<realId>`.
|
|
13
|
+
* The mailbox segment lets the host route a tool call to the correct
|
|
14
|
+
* provider AND the correct logged-in account without needing a separate
|
|
15
|
+
* mailbox registry. {@link buildMailId} / {@link parseMailId} are the
|
|
16
|
+
* canonical helpers; raw string concatenation is forbidden.
|
|
17
|
+
*
|
|
18
|
+
* **Folders.** The core uses three abstract aliases — `inbox`, `archive`,
|
|
19
|
+
* `trash` — which each provider maps to its native folder names
|
|
20
|
+
* (e.g. ms365 → `inbox`, `archive`, `deleteditems`; gmail eventually →
|
|
21
|
+
* `INBOX`, `[Gmail]/All Mail`, `[Gmail]/Trash`).
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Build a self-routing mail id from the three segments. Format:
|
|
25
|
+
* `<pluginId>:<mailbox>:<realId>`. The id is what flows through tool
|
|
26
|
+
* args, event payloads, and agent prompts — the agent never sees the
|
|
27
|
+
* three segments separately.
|
|
28
|
+
*
|
|
29
|
+
* No escaping is applied: mailboxes containing literal `:` (rare;
|
|
30
|
+
* RFC 5321 allows quoted local parts) are unsupported. Graph ids
|
|
31
|
+
* routinely contain `=`, `+`, `/` but never `:`, so the parse split is
|
|
32
|
+
* unambiguous in practice.
|
|
33
|
+
*/
|
|
34
|
+
export function buildMailId(pluginId, mailbox, realId) {
|
|
35
|
+
if (pluginId.length === 0 || pluginId.includes(":")) {
|
|
36
|
+
throw new Error(`pluginId must be non-empty and ":"-free: "${pluginId}"`);
|
|
37
|
+
}
|
|
38
|
+
if (mailbox.length === 0 || mailbox.includes(":")) {
|
|
39
|
+
throw new Error(`mailbox must be non-empty and ":"-free: "${mailbox}"`);
|
|
40
|
+
}
|
|
41
|
+
if (realId.length === 0) {
|
|
42
|
+
throw new Error("realId must be non-empty");
|
|
43
|
+
}
|
|
44
|
+
return `${pluginId}:${mailbox}:${realId}`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Parse a mail id back into its three segments. The split is on the
|
|
48
|
+
* first two `:` characters; anything after the second colon is the
|
|
49
|
+
* real id (which provider implementations may use freely).
|
|
50
|
+
*
|
|
51
|
+
* @throws If the id has fewer than two `:` separators or any segment
|
|
52
|
+
* is empty. The error message names the offending input so a bad
|
|
53
|
+
* id surfaced from agent args produces an actionable tool failure.
|
|
54
|
+
*/
|
|
55
|
+
export function parseMailId(id) {
|
|
56
|
+
const first = id.indexOf(":");
|
|
57
|
+
if (first <= 0) {
|
|
58
|
+
throw new Error(`mail id "${id}" is malformed: expected "<plugin>:<mailbox>:<realId>"`);
|
|
59
|
+
}
|
|
60
|
+
const second = id.indexOf(":", first + 1);
|
|
61
|
+
if (second <= first + 1) {
|
|
62
|
+
throw new Error(`mail id "${id}" is malformed: missing mailbox segment`);
|
|
63
|
+
}
|
|
64
|
+
const realId = id.slice(second + 1);
|
|
65
|
+
if (realId.length === 0) {
|
|
66
|
+
throw new Error(`mail id "${id}" is malformed: empty real-id segment`);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
pluginId: id.slice(0, first),
|
|
70
|
+
mailbox: id.slice(first + 1, second),
|
|
71
|
+
realId,
|
|
72
|
+
};
|
|
73
|
+
}
|