surface-cli 0.3.3 → 0.3.9
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 +60 -17
- package/dist/cli.js +83 -5
- package/dist/cli.js.map +1 -1
- package/dist/contracts/account.js +1 -1
- package/dist/contracts/account.js.map +1 -1
- package/dist/lib/compose-attachments.js +138 -0
- package/dist/lib/compose-attachments.js.map +1 -0
- package/dist/lib/compose-attachments.test.js +47 -0
- package/dist/lib/compose-attachments.test.js.map +1 -0
- package/dist/lib/sent-mail.js +64 -0
- package/dist/lib/sent-mail.js.map +1 -0
- package/dist/lib/skill-install.js +55 -0
- package/dist/lib/skill-install.js.map +1 -0
- package/dist/providers/gmail/adapter.js +32 -45
- package/dist/providers/gmail/adapter.js.map +1 -1
- package/dist/providers/imap/adapter.js +1606 -0
- package/dist/providers/imap/adapter.js.map +1 -0
- package/dist/providers/imap/adapter.test.js +323 -0
- package/dist/providers/imap/adapter.test.js.map +1 -0
- package/dist/providers/imap/auth.js +155 -0
- package/dist/providers/imap/auth.js.map +1 -0
- package/dist/providers/imap/auth.test.js +30 -0
- package/dist/providers/imap/auth.test.js.map +1 -0
- package/dist/providers/index.js +6 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/outlook/adapter.js +89 -21
- package/dist/providers/outlook/adapter.js.map +1 -1
- package/package.json +12 -4
- package/skills/surface-cli/SKILL.md +339 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, extname, join, resolve } from "node:path";
|
|
5
|
+
import { SurfaceError } from "./errors.js";
|
|
6
|
+
const MIME_TYPES_BY_EXTENSION = {
|
|
7
|
+
".csv": "text/csv",
|
|
8
|
+
".gif": "image/gif",
|
|
9
|
+
".htm": "text/html",
|
|
10
|
+
".html": "text/html",
|
|
11
|
+
".ics": "text/calendar",
|
|
12
|
+
".jpeg": "image/jpeg",
|
|
13
|
+
".jpg": "image/jpeg",
|
|
14
|
+
".json": "application/json",
|
|
15
|
+
".pdf": "application/pdf",
|
|
16
|
+
".png": "image/png",
|
|
17
|
+
".text": "text/plain",
|
|
18
|
+
".txt": "text/plain",
|
|
19
|
+
};
|
|
20
|
+
function expandUserPath(inputPath) {
|
|
21
|
+
const trimmed = inputPath.trim();
|
|
22
|
+
if (trimmed === "~") {
|
|
23
|
+
return homedir();
|
|
24
|
+
}
|
|
25
|
+
if (trimmed.startsWith("~/")) {
|
|
26
|
+
return join(homedir(), trimmed.slice(2));
|
|
27
|
+
}
|
|
28
|
+
return trimmed;
|
|
29
|
+
}
|
|
30
|
+
function mimeTypeForPath(path) {
|
|
31
|
+
return MIME_TYPES_BY_EXTENSION[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
32
|
+
}
|
|
33
|
+
function resolveLocalComposeAttachment(inputPath) {
|
|
34
|
+
if (!inputPath.trim()) {
|
|
35
|
+
throw new SurfaceError("invalid_argument", "Attachment path must be non-empty.");
|
|
36
|
+
}
|
|
37
|
+
const resolvedPath = resolve(expandUserPath(inputPath));
|
|
38
|
+
let stats;
|
|
39
|
+
try {
|
|
40
|
+
stats = statSync(resolvedPath);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new SurfaceError("invalid_argument", `Attachment '${inputPath}' was not found.`);
|
|
44
|
+
}
|
|
45
|
+
if (!stats.isFile()) {
|
|
46
|
+
throw new SurfaceError("invalid_argument", `Attachment '${inputPath}' is not a regular file.`);
|
|
47
|
+
}
|
|
48
|
+
const content = readFileSync(resolvedPath);
|
|
49
|
+
const filename = basename(resolvedPath).trim() || "attachment";
|
|
50
|
+
return {
|
|
51
|
+
path: resolvedPath,
|
|
52
|
+
filename,
|
|
53
|
+
mime_type: mimeTypeForPath(resolvedPath),
|
|
54
|
+
size_bytes: content.length,
|
|
55
|
+
content_base64: content.toString("base64"),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function resolveLocalComposeAttachments(paths) {
|
|
59
|
+
return (paths ?? []).map(resolveLocalComposeAttachment);
|
|
60
|
+
}
|
|
61
|
+
export function composeAttachmentMetas(attachments) {
|
|
62
|
+
return (attachments ?? []).map((attachment) => ({
|
|
63
|
+
filename: attachment.filename,
|
|
64
|
+
mime_type: attachment.mime_type,
|
|
65
|
+
size_bytes: attachment.size_bytes,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
function sanitizeHeaderValue(value) {
|
|
69
|
+
return value.replace(/[\r\n]+/g, " ").trim();
|
|
70
|
+
}
|
|
71
|
+
function sanitizeMimeType(value) {
|
|
72
|
+
const normalized = value.trim();
|
|
73
|
+
return /^[A-Za-z0-9][A-Za-z0-9.+-]*\/[A-Za-z0-9][A-Za-z0-9.+-]*$/u.test(normalized)
|
|
74
|
+
? normalized
|
|
75
|
+
: "application/octet-stream";
|
|
76
|
+
}
|
|
77
|
+
function sanitizeMimeParameter(value) {
|
|
78
|
+
const normalized = sanitizeHeaderValue(value).replace(/[\\"]/g, "_");
|
|
79
|
+
return normalized || "attachment";
|
|
80
|
+
}
|
|
81
|
+
function wrapBase64(value) {
|
|
82
|
+
return value.match(/.{1,76}/gu)?.join("\r\n") ?? "";
|
|
83
|
+
}
|
|
84
|
+
function makeMimeBoundary() {
|
|
85
|
+
return `surface-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
86
|
+
}
|
|
87
|
+
export function encodeRawMimeBase64Url(mime) {
|
|
88
|
+
return Buffer.from(mime, "utf8").toString("base64url");
|
|
89
|
+
}
|
|
90
|
+
export function rfc2822Date(value = new Date()) {
|
|
91
|
+
return value.toUTCString().replace("GMT", "+0000");
|
|
92
|
+
}
|
|
93
|
+
export function buildRawMimeMessage(input) {
|
|
94
|
+
const attachments = input.attachments ?? [];
|
|
95
|
+
const includeBccHeader = input.includeBccHeader ?? true;
|
|
96
|
+
const headers = [
|
|
97
|
+
`From: ${sanitizeHeaderValue(input.from)}`,
|
|
98
|
+
...(input.to.length > 0 ? [`To: ${input.to.map(sanitizeHeaderValue).join(", ")}`] : []),
|
|
99
|
+
...(input.cc.length > 0 ? [`Cc: ${input.cc.map(sanitizeHeaderValue).join(", ")}`] : []),
|
|
100
|
+
...(includeBccHeader && input.bcc.length > 0 ? [`Bcc: ${input.bcc.map(sanitizeHeaderValue).join(", ")}`] : []),
|
|
101
|
+
`Subject: ${sanitizeHeaderValue(input.subject)}`,
|
|
102
|
+
...(input.messageId ? [`Message-ID: ${sanitizeHeaderValue(input.messageId)}`] : []),
|
|
103
|
+
...(input.date || input.messageId ? [`Date: ${sanitizeHeaderValue(input.date ?? rfc2822Date())}`] : []),
|
|
104
|
+
...(input.inReplyTo ? [`In-Reply-To: ${sanitizeHeaderValue(input.inReplyTo)}`] : []),
|
|
105
|
+
...(input.references ? [`References: ${sanitizeHeaderValue(input.references)}`] : []),
|
|
106
|
+
"MIME-Version: 1.0",
|
|
107
|
+
];
|
|
108
|
+
const body = input.body.replace(/\r\n/g, "\n");
|
|
109
|
+
if (attachments.length === 0) {
|
|
110
|
+
return [
|
|
111
|
+
...headers,
|
|
112
|
+
'Content-Type: text/plain; charset="UTF-8"',
|
|
113
|
+
"Content-Transfer-Encoding: 8bit",
|
|
114
|
+
"",
|
|
115
|
+
body,
|
|
116
|
+
"",
|
|
117
|
+
].join("\r\n");
|
|
118
|
+
}
|
|
119
|
+
const boundary = makeMimeBoundary();
|
|
120
|
+
const lines = [
|
|
121
|
+
...headers,
|
|
122
|
+
`Content-Type: multipart/mixed; boundary="${boundary}"`,
|
|
123
|
+
"",
|
|
124
|
+
`--${boundary}`,
|
|
125
|
+
'Content-Type: text/plain; charset="UTF-8"',
|
|
126
|
+
"Content-Transfer-Encoding: 8bit",
|
|
127
|
+
"",
|
|
128
|
+
body,
|
|
129
|
+
"",
|
|
130
|
+
];
|
|
131
|
+
for (const attachment of attachments) {
|
|
132
|
+
const filename = sanitizeMimeParameter(attachment.filename);
|
|
133
|
+
lines.push(`--${boundary}`, `Content-Type: ${sanitizeMimeType(attachment.mime_type)}; name="${filename}"`, `Content-Disposition: attachment; filename="${filename}"`, "Content-Transfer-Encoding: base64", "", wrapBase64(attachment.content_base64), "");
|
|
134
|
+
}
|
|
135
|
+
lines.push(`--${boundary}--`, "");
|
|
136
|
+
return lines.join("\r\n");
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=compose-attachments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compose-attachments.js","sourceRoot":"","sources":["../../src/lib/compose-attachments.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { buildRawMimeMessage, composeAttachmentMetas, resolveLocalComposeAttachments, } from "./compose-attachments.js";
|
|
5
|
+
test("resolveLocalComposeAttachments reads file metadata and bytes without exposing paths in public metadata", () => {
|
|
6
|
+
const [attachment] = resolveLocalComposeAttachments(["tsconfig.json"]);
|
|
7
|
+
assert.equal(attachment?.filename, "tsconfig.json");
|
|
8
|
+
assert.equal(attachment?.mime_type, "application/json");
|
|
9
|
+
assert.equal(typeof attachment?.size_bytes, "number");
|
|
10
|
+
assert.ok(attachment?.content_base64);
|
|
11
|
+
assert.deepEqual(composeAttachmentMetas([attachment]), [
|
|
12
|
+
{
|
|
13
|
+
filename: "tsconfig.json",
|
|
14
|
+
mime_type: "application/json",
|
|
15
|
+
size_bytes: attachment.size_bytes,
|
|
16
|
+
},
|
|
17
|
+
]);
|
|
18
|
+
});
|
|
19
|
+
test("buildRawMimeMessage emits multipart MIME attachments and can hide Bcc for delivery copies", () => {
|
|
20
|
+
const raw = buildRawMimeMessage({
|
|
21
|
+
from: "surface@example.com",
|
|
22
|
+
to: ["to@example.com"],
|
|
23
|
+
cc: ["cc@example.com"],
|
|
24
|
+
bcc: ["hidden@example.com"],
|
|
25
|
+
subject: "Surface attachment probe",
|
|
26
|
+
body: "Hello with file",
|
|
27
|
+
messageId: "<surface-test@example.com>",
|
|
28
|
+
includeBccHeader: false,
|
|
29
|
+
attachments: [
|
|
30
|
+
{
|
|
31
|
+
path: "/tmp/report.txt",
|
|
32
|
+
filename: "report.txt",
|
|
33
|
+
mime_type: "text/plain",
|
|
34
|
+
size_bytes: 11,
|
|
35
|
+
content_base64: Buffer.from("hello file").toString("base64"),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
assert.match(raw, /^To: to@example\.com/m);
|
|
40
|
+
assert.match(raw, /^Cc: cc@example\.com/m);
|
|
41
|
+
assert.doesNotMatch(raw, /^Bcc:/m);
|
|
42
|
+
assert.match(raw, /^Content-Type: multipart\/mixed; boundary="surface-/m);
|
|
43
|
+
assert.match(raw, /Content-Type: text\/plain; name="report\.txt"/);
|
|
44
|
+
assert.match(raw, /Content-Disposition: attachment; filename="report\.txt"/);
|
|
45
|
+
assert.match(raw, new RegExp(Buffer.from("hello file").toString("base64")));
|
|
46
|
+
});
|
|
47
|
+
//# sourceMappingURL=compose-attachments.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compose-attachments.test.js","sourceRoot":"","sources":["../../src/lib/compose-attachments.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,8BAA8B,GAC/B,MAAM,0BAA0B,CAAC;AAElC,IAAI,CAAC,wGAAwG,EAAE,GAAG,EAAE;IAClH,MAAM,CAAC,UAAU,CAAC,GAAG,8BAA8B,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC;IAEvE,MAAM,CAAC,KAAK,CAAC,UAAU,EAAE,QAAQ,EAAE,eAAe,CAAC,CAAC;IACpD,MAAM,CAAC,KAAK,CAAC,UAAU,EAAE,SAAS,EAAE,kBAAkB,CAAC,CAAC;IACxD,MAAM,CAAC,KAAK,CAAC,OAAO,UAAU,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;IACtD,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;IACtC,MAAM,CAAC,SAAS,CAAC,sBAAsB,CAAC,CAAC,UAAW,CAAC,CAAC,EAAE;QACtD;YACE,QAAQ,EAAE,eAAe;YACzB,SAAS,EAAE,kBAAkB;YAC7B,UAAU,EAAE,UAAW,CAAC,UAAU;SACnC;KACF,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,2FAA2F,EAAE,GAAG,EAAE;IACrG,MAAM,GAAG,GAAG,mBAAmB,CAAC;QAC9B,IAAI,EAAE,qBAAqB;QAC3B,EAAE,EAAE,CAAC,gBAAgB,CAAC;QACtB,EAAE,EAAE,CAAC,gBAAgB,CAAC;QACtB,GAAG,EAAE,CAAC,oBAAoB,CAAC;QAC3B,OAAO,EAAE,0BAA0B;QACnC,IAAI,EAAE,iBAAiB;QACvB,SAAS,EAAE,4BAA4B;QACvC,gBAAgB,EAAE,KAAK;QACvB,WAAW,EAAE;YACX;gBACE,IAAI,EAAE,iBAAiB;gBACvB,QAAQ,EAAE,YAAY;gBACtB,SAAS,EAAE,YAAY;gBACvB,UAAU,EAAE,EAAE;gBACd,cAAc,EAAE,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;aAC7D;SACF;KACF,CAAC,CAAC;IAEH,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,uBAAuB,CAAC,CAAC;IAC3C,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,uBAAuB,CAAC,CAAC;IAC3C,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IACnC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,sDAAsD,CAAC,CAAC;IAC1E,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,+CAA+C,CAAC,CAAC;IACnE,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,yDAAyD,CAAC,CAAC;IAC7E,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AAC9E,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { SurfaceError } from "./errors.js";
|
|
2
|
+
import { loadStoredThread } from "./stored-mail.js";
|
|
3
|
+
export function normalizeComparableEmail(value) {
|
|
4
|
+
return (value ?? "").trim().toLowerCase();
|
|
5
|
+
}
|
|
6
|
+
export function accountIdentityEmails(account, context) {
|
|
7
|
+
const identity = context.db.getAccountIdentity(account);
|
|
8
|
+
return new Set([identity.primary_email, account.email, ...identity.email_aliases]
|
|
9
|
+
.map(normalizeComparableEmail)
|
|
10
|
+
.filter(Boolean));
|
|
11
|
+
}
|
|
12
|
+
export function messageMatchesRecipient(message, recipient) {
|
|
13
|
+
if (!recipient) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
const normalizedRecipient = normalizeComparableEmail(recipient);
|
|
17
|
+
return [...message.envelope.to, ...message.envelope.cc]
|
|
18
|
+
.some((participant) => normalizeComparableEmail(participant.email) === normalizedRecipient);
|
|
19
|
+
}
|
|
20
|
+
function messageWasSentByAccount(message, emails) {
|
|
21
|
+
const fromEmail = normalizeComparableEmail(message.envelope.from?.email);
|
|
22
|
+
return Boolean(fromEmail && emails.has(fromEmail));
|
|
23
|
+
}
|
|
24
|
+
export function sentTimestamp(message) {
|
|
25
|
+
const parsed = Date.parse(message.envelope.sent_at ?? message.envelope.received_at ?? "");
|
|
26
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
27
|
+
}
|
|
28
|
+
export function sentMessagesFromStoredThread(account, context, query) {
|
|
29
|
+
if (!query.thread_ref) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const resolved = context.db.findThreadByRef(query.thread_ref);
|
|
33
|
+
if (!resolved) {
|
|
34
|
+
throw new SurfaceError("not_found", `Thread '${query.thread_ref}' was not found.`, {
|
|
35
|
+
account: account.name,
|
|
36
|
+
threadRef: query.thread_ref,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (resolved.account_id !== account.account_id) {
|
|
40
|
+
throw new SurfaceError("invalid_argument", `Thread '${query.thread_ref}' does not belong to account '${account.name}'.`, {
|
|
41
|
+
account: account.name,
|
|
42
|
+
threadRef: query.thread_ref,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const thread = loadStoredThread(context.db, account, query.thread_ref);
|
|
46
|
+
if (!thread) {
|
|
47
|
+
throw new SurfaceError("not_found", `Thread '${query.thread_ref}' was not found.`, {
|
|
48
|
+
account: account.name,
|
|
49
|
+
threadRef: query.thread_ref,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const emails = accountIdentityEmails(account, context);
|
|
53
|
+
return thread.messages
|
|
54
|
+
.filter((message) => messageWasSentByAccount(message, emails))
|
|
55
|
+
.filter((message) => messageMatchesRecipient(message, query.recipient))
|
|
56
|
+
.sort((left, right) => sentTimestamp(right) - sentTimestamp(left))
|
|
57
|
+
.slice(0, query.limit)
|
|
58
|
+
.map((message) => ({
|
|
59
|
+
...message,
|
|
60
|
+
thread_ref: thread.thread_ref,
|
|
61
|
+
source: thread.source,
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=sent-mail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sent-mail.js","sourceRoot":"","sources":["../../src/lib/sent-mail.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { copyFile, mkdir, stat } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { SurfaceError } from "./errors.js";
|
|
6
|
+
const SKILL_NAME = "surface-cli";
|
|
7
|
+
function packageRoot() {
|
|
8
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
9
|
+
}
|
|
10
|
+
export function bundledSkillPath() {
|
|
11
|
+
return join(packageRoot(), "skills", SKILL_NAME, "SKILL.md");
|
|
12
|
+
}
|
|
13
|
+
export function defaultSkillDestination(target) {
|
|
14
|
+
switch (target) {
|
|
15
|
+
case "codex":
|
|
16
|
+
return join(homedir(), ".codex", "skills", SKILL_NAME, "SKILL.md");
|
|
17
|
+
case "claude-code":
|
|
18
|
+
return join(homedir(), ".claude", "skills", SKILL_NAME, "SKILL.md");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function parseSkillInstallTarget(value) {
|
|
22
|
+
switch (value) {
|
|
23
|
+
case "codex":
|
|
24
|
+
return "codex";
|
|
25
|
+
case "claude":
|
|
26
|
+
case "claude-code":
|
|
27
|
+
return "claude-code";
|
|
28
|
+
case "all":
|
|
29
|
+
return "all";
|
|
30
|
+
default:
|
|
31
|
+
throw new SurfaceError("invalid_argument", `Expected skill install target to be one of: codex, claude-code, all. Received '${value}'.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function expandSkillInstallTargets(target) {
|
|
35
|
+
return target === "all" ? ["codex", "claude-code"] : [target];
|
|
36
|
+
}
|
|
37
|
+
export async function installSurfaceSkill(target) {
|
|
38
|
+
const source = bundledSkillPath();
|
|
39
|
+
try {
|
|
40
|
+
await stat(source);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new SurfaceError("not_found", `Bundled Surface skill was not found at '${source}'. Reinstall surface-cli or install the skill from GitHub.`);
|
|
44
|
+
}
|
|
45
|
+
const destination = defaultSkillDestination(target);
|
|
46
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
47
|
+
await copyFile(source, destination);
|
|
48
|
+
return {
|
|
49
|
+
agent: target,
|
|
50
|
+
skill: SKILL_NAME,
|
|
51
|
+
source,
|
|
52
|
+
path: destination,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=skill-install.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-install.js","sourceRoot":"","sources":["../../src/lib/skill-install.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAW3C,MAAM,UAAU,GAAG,aAAa,CAAC;AAEjC,SAAS,WAAW;IAClB,OAAO,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AACtE,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,IAAI,CAAC,WAAW,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,MAA0B;IAChE,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,OAAO;YACV,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;QACrE,KAAK,aAAa;YAChB,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;IACxE,CAAC;AACH,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,KAAa;IACnD,QAAQ,KAAK,EAAE,CAAC;QACd,KAAK,OAAO;YACV,OAAO,OAAO,CAAC;QACjB,KAAK,QAAQ,CAAC;QACd,KAAK,aAAa;YAChB,OAAO,aAAa,CAAC;QACvB,KAAK,KAAK;YACR,OAAO,KAAK,CAAC;QACf;YACE,MAAM,IAAI,YAAY,CACpB,kBAAkB,EAClB,kFAAkF,KAAK,IAAI,CAC5F,CAAC;IACN,CAAC;AACH,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,MAAkC;IAC1E,OAAO,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAChE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,MAA0B;IAClE,MAAM,MAAM,GAAG,gBAAgB,EAAE,CAAC;IAClC,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,YAAY,CACpB,WAAW,EACX,2CAA2C,MAAM,4DAA4D,CAC9G,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GAAG,uBAAuB,CAAC,MAAM,CAAC,CAAC;IACpD,MAAM,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvD,MAAM,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAEpC,OAAO;QACL,KAAK,EAAE,MAAM;QACb,KAAK,EAAE,UAAU;QACjB,MAAM;QACN,IAAI,EAAE,WAAW;KAClB,CAAC;AACJ,CAAC"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { buildRawMimeMessage, composeAttachmentMetas, encodeRawMimeBase64Url, } from "../../lib/compose-attachments.js";
|
|
4
5
|
import { SurfaceError } from "../../lib/errors.js";
|
|
5
6
|
import { toPublicSentMessage } from "../../lib/public-mail.js";
|
|
7
|
+
import { sentMessagesFromStoredThread } from "../../lib/sent-mail.js";
|
|
6
8
|
import { assertWriteAllowed } from "../../lib/write-safety.js";
|
|
7
9
|
import { makeAttachmentId, makeMessageRef, makeThreadRef } from "../../refs.js";
|
|
8
10
|
import { summarizeAndPersistThreads } from "../../summarizer.js";
|
|
@@ -505,9 +507,6 @@ function normalizeEmailList(values) {
|
|
|
505
507
|
}
|
|
506
508
|
return [...deduped];
|
|
507
509
|
}
|
|
508
|
-
function sanitizeHeaderValue(value) {
|
|
509
|
-
return value.replace(/\r?\n/g, " ").trim();
|
|
510
|
-
}
|
|
511
510
|
function prefixSubject(subject, prefix) {
|
|
512
511
|
const normalized = subject.trim();
|
|
513
512
|
if (!normalized) {
|
|
@@ -551,27 +550,6 @@ function buildForwardBody(inputBody, stored) {
|
|
|
551
550
|
lines.push("", originalBody);
|
|
552
551
|
return lines.join("\n").trim();
|
|
553
552
|
}
|
|
554
|
-
function encodeMimeBase64Url(mime) {
|
|
555
|
-
return Buffer.from(mime, "utf8").toString("base64url");
|
|
556
|
-
}
|
|
557
|
-
function buildRawMimeMessage(input) {
|
|
558
|
-
const lines = [
|
|
559
|
-
`From: ${sanitizeHeaderValue(input.from)}`,
|
|
560
|
-
...(input.to.length > 0 ? [`To: ${input.to.map(sanitizeHeaderValue).join(", ")}`] : []),
|
|
561
|
-
...(input.cc.length > 0 ? [`Cc: ${input.cc.map(sanitizeHeaderValue).join(", ")}`] : []),
|
|
562
|
-
...(input.bcc.length > 0 ? [`Bcc: ${input.bcc.map(sanitizeHeaderValue).join(", ")}`] : []),
|
|
563
|
-
`Subject: ${sanitizeHeaderValue(input.subject)}`,
|
|
564
|
-
...(input.inReplyTo ? [`In-Reply-To: ${sanitizeHeaderValue(input.inReplyTo)}`] : []),
|
|
565
|
-
...(input.references ? [`References: ${sanitizeHeaderValue(input.references)}`] : []),
|
|
566
|
-
"MIME-Version: 1.0",
|
|
567
|
-
'Content-Type: text/plain; charset="UTF-8"',
|
|
568
|
-
"Content-Transfer-Encoding: 8bit",
|
|
569
|
-
"",
|
|
570
|
-
input.body.replace(/\r\n/g, "\n"),
|
|
571
|
-
"",
|
|
572
|
-
];
|
|
573
|
-
return lines.join("\r\n");
|
|
574
|
-
}
|
|
575
553
|
function parseMessageLocator(locatorJson) {
|
|
576
554
|
const parsed = JSON.parse(locatorJson);
|
|
577
555
|
return {
|
|
@@ -589,7 +567,7 @@ function latestStoredThreadMessage(threadRef, context) {
|
|
|
589
567
|
stored: messageRef ? context.db.getStoredMessage(messageRef) ?? null : null,
|
|
590
568
|
};
|
|
591
569
|
}
|
|
592
|
-
function buildSendEnvelope(account, command, status, subject, recipients, result, inReplyToMessageRef) {
|
|
570
|
+
function buildSendEnvelope(account, command, status, subject, recipients, result, inReplyToMessageRef, attachments = []) {
|
|
593
571
|
return {
|
|
594
572
|
schema_version: "1",
|
|
595
573
|
command,
|
|
@@ -598,6 +576,7 @@ function buildSendEnvelope(account, command, status, subject, recipients, result
|
|
|
598
576
|
status,
|
|
599
577
|
subject,
|
|
600
578
|
recipients,
|
|
579
|
+
attachments,
|
|
601
580
|
thread_ref: result.thread_ref,
|
|
602
581
|
message_ref: result.message_ref,
|
|
603
582
|
in_reply_to_message_ref: inReplyToMessageRef,
|
|
@@ -975,6 +954,10 @@ async function fetchGmailThreads(account, context, options) {
|
|
|
975
954
|
return summarizeAndPersistThreads(persisted, context.config, context.db, context.db.getAccountIdentity(account));
|
|
976
955
|
}
|
|
977
956
|
async function fetchGmailSentMessages(account, context, query) {
|
|
957
|
+
if (query.thread_ref) {
|
|
958
|
+
await refreshGmailThread(account, query.thread_ref, context);
|
|
959
|
+
return sentMessagesFromStoredThread(account, context, query);
|
|
960
|
+
}
|
|
978
961
|
const queryParts = ["in:sent"];
|
|
979
962
|
if (query.recipient?.trim()) {
|
|
980
963
|
queryParts.push(buildGmailRecipientQuery(query.recipient));
|
|
@@ -1013,6 +996,23 @@ async function fetchGmailSentMessages(account, context, query) {
|
|
|
1013
996
|
}
|
|
1014
997
|
return results;
|
|
1015
998
|
}
|
|
999
|
+
async function refreshGmailThread(account, threadRef, context) {
|
|
1000
|
+
const locatorRow = context.db.findProviderLocator("thread", threadRef);
|
|
1001
|
+
if (!locatorRow) {
|
|
1002
|
+
throw new SurfaceError("cache_miss", `No provider locator exists for thread '${threadRef}'.`, {
|
|
1003
|
+
account: account.name,
|
|
1004
|
+
threadRef,
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
const locator = JSON.parse(locatorRow.locator_json);
|
|
1008
|
+
if (!locator.thread_id) {
|
|
1009
|
+
throw new SurfaceError("transport_error", `Thread '${threadRef}' is missing a Gmail thread id.`, {
|
|
1010
|
+
account: account.name,
|
|
1011
|
+
threadRef,
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
await fetchAndPersistGmailThread(account, context, locator.thread_id);
|
|
1015
|
+
}
|
|
1016
1016
|
async function sendOrDraftGmailMessage(account, context, payload) {
|
|
1017
1017
|
if (payload.draft) {
|
|
1018
1018
|
const draft = await createGmailDraft(account, context, {
|
|
@@ -1083,21 +1083,7 @@ export class GmailApiAdapter {
|
|
|
1083
1083
|
return fetchGmailSentMessages(account, context, query);
|
|
1084
1084
|
}
|
|
1085
1085
|
async refreshThread(account, threadRef, context) {
|
|
1086
|
-
|
|
1087
|
-
if (!locatorRow) {
|
|
1088
|
-
throw new SurfaceError("cache_miss", `No provider locator exists for thread '${threadRef}'.`, {
|
|
1089
|
-
account: account.name,
|
|
1090
|
-
threadRef,
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
|
-
const locator = JSON.parse(locatorRow.locator_json);
|
|
1094
|
-
if (!locator.thread_id) {
|
|
1095
|
-
throw new SurfaceError("transport_error", `Thread '${threadRef}' is missing a Gmail thread id.`, {
|
|
1096
|
-
account: account.name,
|
|
1097
|
-
threadRef,
|
|
1098
|
-
});
|
|
1099
|
-
}
|
|
1100
|
-
await fetchAndPersistGmailThread(account, context, locator.thread_id);
|
|
1086
|
+
await refreshGmailThread(account, threadRef, context);
|
|
1101
1087
|
}
|
|
1102
1088
|
async readMessage(account, messageRef, refresh, context) {
|
|
1103
1089
|
const stored = context.db.getStoredMessage(messageRef);
|
|
@@ -1242,19 +1228,20 @@ export class GmailApiAdapter {
|
|
|
1242
1228
|
assertWriteAllowed(context.config, account, recipients, {
|
|
1243
1229
|
disposition: input.draft ? "draft" : "send",
|
|
1244
1230
|
});
|
|
1245
|
-
const raw =
|
|
1231
|
+
const raw = encodeRawMimeBase64Url(buildRawMimeMessage({
|
|
1246
1232
|
from: account.email,
|
|
1247
1233
|
to: recipients.to,
|
|
1248
1234
|
cc: recipients.cc,
|
|
1249
1235
|
bcc: recipients.bcc,
|
|
1250
1236
|
subject: input.subject,
|
|
1251
1237
|
body: input.body,
|
|
1238
|
+
attachments: input.attachments,
|
|
1252
1239
|
}));
|
|
1253
1240
|
const result = await sendOrDraftGmailMessage(account, context, {
|
|
1254
1241
|
raw,
|
|
1255
1242
|
draft: input.draft,
|
|
1256
1243
|
});
|
|
1257
|
-
return buildSendEnvelope(account, "send", result.status, input.subject, recipientsFromInput(recipients), result.refs, null);
|
|
1244
|
+
return buildSendEnvelope(account, "send", result.status, input.subject, recipientsFromInput(recipients), result.refs, null, composeAttachmentMetas(input.attachments));
|
|
1258
1245
|
}
|
|
1259
1246
|
async reply(account, messageRef, input, context) {
|
|
1260
1247
|
const target = await resolveGmailMessageContext(account, messageRef, context);
|
|
@@ -1286,7 +1273,7 @@ export class GmailApiAdapter {
|
|
|
1286
1273
|
? `${target.headers.references}${originalMessageId ? ` ${originalMessageId}` : ""}`.trim()
|
|
1287
1274
|
: originalMessageId;
|
|
1288
1275
|
const subject = prefixSubject(target.stored.subject ?? target.headers.subject ?? "", "Re");
|
|
1289
|
-
const raw =
|
|
1276
|
+
const raw = encodeRawMimeBase64Url(buildRawMimeMessage({
|
|
1290
1277
|
from: account.email,
|
|
1291
1278
|
to: recipients.to,
|
|
1292
1279
|
cc: recipients.cc,
|
|
@@ -1341,7 +1328,7 @@ export class GmailApiAdapter {
|
|
|
1341
1328
|
? `${target.headers.references}${originalMessageId ? ` ${originalMessageId}` : ""}`.trim()
|
|
1342
1329
|
: originalMessageId;
|
|
1343
1330
|
const subject = prefixSubject(target.stored.subject ?? target.headers.subject ?? "", "Re");
|
|
1344
|
-
const raw =
|
|
1331
|
+
const raw = encodeRawMimeBase64Url(buildRawMimeMessage({
|
|
1345
1332
|
from: account.email,
|
|
1346
1333
|
to: recipients.to,
|
|
1347
1334
|
cc: recipients.cc,
|
|
@@ -1374,7 +1361,7 @@ export class GmailApiAdapter {
|
|
|
1374
1361
|
disposition: input.draft ? "draft" : "send",
|
|
1375
1362
|
});
|
|
1376
1363
|
const subject = prefixSubject(target.stored.subject ?? target.headers.subject ?? "", "Fwd");
|
|
1377
|
-
const raw =
|
|
1364
|
+
const raw = encodeRawMimeBase64Url(buildRawMimeMessage({
|
|
1378
1365
|
from: account.email,
|
|
1379
1366
|
to: recipients.to,
|
|
1380
1367
|
cc: recipients.cc,
|