@sulala/agent-os 0.1.23 → 0.1.25
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/dashboard-dist/assets/index-BZYG7rCd.js +75 -0
- package/dashboard-dist/assets/index-CAOgf_FY.css +1 -0
- package/dashboard-dist/assets/index-CVI9FAmG.css +1 -0
- package/dashboard-dist/assets/index-DdMu_Z6v.js +75 -0
- package/dashboard-dist/index.html +2 -2
- package/data/agents/crm_hubspot_agent.json +11 -0
- package/data/agents/research_agent.json +1 -1
- package/data/agents/social_media_agent.json +1 -1
- package/data/agents/source_verify_agent.json +10 -0
- package/data/agents/writer_agent.json +1 -1
- package/data/skills/bluesky/SKILL.md +1 -1
- package/data/skills/content-writing/SKILL.md +32 -0
- package/data/skills/date/SKILL.md +1 -1
- package/data/skills/fetch/SKILL.md +1 -1
- package/data/skills/file-search/SKILL.md +1 -1
- package/data/skills/file-stats/SKILL.md +1 -1
- package/data/skills/git/SKILL.md +1 -1
- package/data/skills/hash/SKILL.md +1 -1
- package/data/skills/jq/SKILL.md +1 -1
- package/data/skills/markdown-to-html/SKILL.md +1 -1
- package/data/skills/qr-code/SKILL.md +1 -1
- package/data/skills/rss/SKILL.md +1 -1
- package/data/skills/translate/SKILL.md +1 -1
- package/data/skills/weather/SKILL.md +1 -1
- package/data/skills/web-search/SKILL.md +1 -1
- package/data/skills/webhook/SKILL.md +1 -1
- package/dist/cli.js +234 -69
- package/dist/index.js +168 -57
- package/package.json +1 -1
- package/data/skills/gmail/SKILL.md +0 -55
- package/data/skills/gmail/references/send-email.md +0 -54
- package/data/skills/gmail/scripts/send_email.py +0 -94
- package/data/skills/youtube/SKILL.md +0 -91
- package/data/skills/youtube/config.schema.json +0 -11
- package/data/skills/youtube/package.json +0 -8
- package/data/skills/youtube/references/youtube-upload.md +0 -65
- package/data/skills/youtube/requirements.txt +0 -3
- package/data/skills/youtube/scripts/youtube_upload.js +0 -200
- package/data/skills/youtube/scripts/youtube_upload.py +0 -125
package/dist/index.js
CHANGED
|
@@ -19606,6 +19606,29 @@ function createTokenRequestTool(skillId, toolId, baseUrl, description) {
|
|
|
19606
19606
|
init_config();
|
|
19607
19607
|
var SYSTEM_SKILL_IDS = new Set(["memory"]);
|
|
19608
19608
|
var DEFAULT_SYSTEM_SKILL_IDS = ["memory", "date", "fetch", "jq", "file-search"];
|
|
19609
|
+
var SULALA_META_FILE = ".sulala-meta.json";
|
|
19610
|
+
async function readSkillMeta(skillDir, subEntries) {
|
|
19611
|
+
if (!subEntries.includes(SULALA_META_FILE))
|
|
19612
|
+
return null;
|
|
19613
|
+
try {
|
|
19614
|
+
const raw = await readFile5(join6(skillDir, SULALA_META_FILE), "utf-8");
|
|
19615
|
+
const data = JSON.parse(raw);
|
|
19616
|
+
return typeof data.version === "string" && data.version.trim() !== "" ? { version: data.version.trim() } : null;
|
|
19617
|
+
} catch {
|
|
19618
|
+
return null;
|
|
19619
|
+
}
|
|
19620
|
+
}
|
|
19621
|
+
async function writeSkillMeta(skillId, meta) {
|
|
19622
|
+
const skillDir = join6(getSkillsDir(), skillId);
|
|
19623
|
+
const payload = {};
|
|
19624
|
+
if (meta.version?.trim())
|
|
19625
|
+
payload.version = meta.version.trim();
|
|
19626
|
+
if (meta.source?.trim())
|
|
19627
|
+
payload.source = meta.source.trim();
|
|
19628
|
+
if (Object.keys(payload).length === 0)
|
|
19629
|
+
return;
|
|
19630
|
+
await writeFile3(join6(skillDir, SULALA_META_FILE), JSON.stringify(payload, null, 0), "utf-8");
|
|
19631
|
+
}
|
|
19609
19632
|
async function loadSkill(name) {
|
|
19610
19633
|
const dir = join6(getSkillsDir(), name);
|
|
19611
19634
|
let entries;
|
|
@@ -19848,10 +19871,14 @@ async function listSkills() {
|
|
|
19848
19871
|
continue;
|
|
19849
19872
|
const skillName = doc.name ?? name;
|
|
19850
19873
|
const requiredEnv = await getRequiredEnvForSkill(skillDir, subEntries, doc);
|
|
19874
|
+
const docVersion = doc.version?.trim() || undefined;
|
|
19875
|
+
const meta = await readSkillMeta(skillDir, subEntries);
|
|
19876
|
+
const version = docVersion ?? meta?.version;
|
|
19851
19877
|
results.push({
|
|
19852
19878
|
id: name,
|
|
19853
19879
|
name: skillName,
|
|
19854
19880
|
description: doc.description,
|
|
19881
|
+
version,
|
|
19855
19882
|
tools: (doc.tools ?? []).map((t) => ({ id: t.id, description: t.description })),
|
|
19856
19883
|
required_env: requiredEnv.length ? requiredEnv : undefined,
|
|
19857
19884
|
system: SYSTEM_SKILL_IDS.has(name)
|
|
@@ -19921,7 +19948,7 @@ async function installSystemSkills() {
|
|
|
19921
19948
|
}
|
|
19922
19949
|
return { installed };
|
|
19923
19950
|
}
|
|
19924
|
-
async function installSkillFromUrl(url, explicitId) {
|
|
19951
|
+
async function installSkillFromUrl(url, explicitId, meta) {
|
|
19925
19952
|
const urlLower = url.toLowerCase();
|
|
19926
19953
|
const isStoreSkillContentUrl = urlLower.includes("/api/sulalahub/skills/") && !urlLower.includes("/download") && !urlLower.endsWith(".zip");
|
|
19927
19954
|
const headers = isStoreSkillContentUrl ? { Accept: "application/zip" } : {};
|
|
@@ -19936,31 +19963,35 @@ async function installSkillFromUrl(url, explicitId) {
|
|
|
19936
19963
|
const tmpDir = join6(tmpdir(), `agent-os-skill-${Date.now()}`);
|
|
19937
19964
|
await mkdir3(tmpDir, { recursive: true });
|
|
19938
19965
|
try {
|
|
19966
|
+
let destId;
|
|
19939
19967
|
if (isZip) {
|
|
19940
19968
|
const zipPath = join6(tmpDir, "archive.zip");
|
|
19941
19969
|
await writeFile3(zipPath, new Uint8Array(buf));
|
|
19942
|
-
const
|
|
19943
|
-
const
|
|
19944
|
-
if (
|
|
19945
|
-
const err = await new Response(
|
|
19970
|
+
const proc = Bun.spawn({ cmd: ["unzip", "-q", "-o", zipPath, "-d", tmpDir], stdout: "ignore", stderr: "pipe" });
|
|
19971
|
+
const exit = await proc.exited;
|
|
19972
|
+
if (exit !== 0) {
|
|
19973
|
+
const err = await new Response(proc.stderr).text();
|
|
19946
19974
|
throw new Error(`unzip failed: ${err}`);
|
|
19947
19975
|
}
|
|
19948
|
-
const { id
|
|
19949
|
-
|
|
19950
|
-
await cp(
|
|
19951
|
-
|
|
19952
|
-
|
|
19953
|
-
|
|
19954
|
-
|
|
19955
|
-
|
|
19956
|
-
|
|
19957
|
-
|
|
19958
|
-
|
|
19959
|
-
|
|
19960
|
-
|
|
19961
|
-
|
|
19962
|
-
|
|
19963
|
-
|
|
19976
|
+
const { id, sourcePath } = await chooseSkillRootFromExtract(tmpDir, "archive.zip");
|
|
19977
|
+
destId = explicitId != null && explicitId.trim() !== "" ? slugToSkillId(explicitId) : id;
|
|
19978
|
+
await cp(sourcePath, join6(skillsDir, destId), { recursive: true });
|
|
19979
|
+
} else {
|
|
19980
|
+
const tarPath = join6(tmpDir, "archive.tar.gz");
|
|
19981
|
+
await writeFile3(tarPath, new Uint8Array(buf));
|
|
19982
|
+
const proc = Bun.spawn({ cmd: ["tar", "-xzf", tarPath, "-C", tmpDir], stdout: "ignore", stderr: "pipe" });
|
|
19983
|
+
const exit = await proc.exited;
|
|
19984
|
+
if (exit !== 0) {
|
|
19985
|
+
const err = await new Response(proc.stderr).text();
|
|
19986
|
+
throw new Error(`tar extract failed: ${err}`);
|
|
19987
|
+
}
|
|
19988
|
+
const { id, sourcePath } = await chooseSkillRootFromExtract(tmpDir, "archive.tar.gz");
|
|
19989
|
+
destId = explicitId != null && explicitId.trim() !== "" ? slugToSkillId(explicitId) : id;
|
|
19990
|
+
await cp(sourcePath, join6(skillsDir, destId), { recursive: true });
|
|
19991
|
+
}
|
|
19992
|
+
if (meta?.version?.trim() || meta?.source?.trim()) {
|
|
19993
|
+
await writeSkillMeta(destId, { version: meta.version, source: meta.source ?? (explicitId ? "hub" : undefined) });
|
|
19994
|
+
}
|
|
19964
19995
|
return { id: destId };
|
|
19965
19996
|
} finally {
|
|
19966
19997
|
await rm(tmpDir, { recursive: true, force: true });
|
|
@@ -26537,8 +26568,8 @@ function formatLLMErrorForUser(err) {
|
|
|
26537
26568
|
}
|
|
26538
26569
|
function runAgentInner(options) {
|
|
26539
26570
|
return (async () => {
|
|
26540
|
-
const { task, agent, conversationHistory = [] } = options;
|
|
26541
|
-
const maxTurns = agent.limits?.max_turns ?? 10;
|
|
26571
|
+
const { task, agent, conversationHistory = [], maxTurnsOverride } = options;
|
|
26572
|
+
const maxTurns = maxTurnsOverride ?? agent.limits?.max_turns ?? 10;
|
|
26542
26573
|
const maxTokens = agent.limits?.max_tokens;
|
|
26543
26574
|
await ensureWorkspace(agent.id);
|
|
26544
26575
|
await loadSkillsForAgent(agent);
|
|
@@ -26772,8 +26803,8 @@ async function runAgent(options) {
|
|
|
26772
26803
|
return runAgentInner(options);
|
|
26773
26804
|
}
|
|
26774
26805
|
async function runAgentStream(options, onEvent) {
|
|
26775
|
-
const { task, agent, conversationHistory = [] } = options;
|
|
26776
|
-
const maxTurns = agent.limits?.max_turns ?? 10;
|
|
26806
|
+
const { task, agent, conversationHistory = [], maxTurnsOverride } = options;
|
|
26807
|
+
const maxTurns = maxTurnsOverride ?? agent.limits?.max_turns ?? 10;
|
|
26777
26808
|
const maxTokens = agent.limits?.max_tokens;
|
|
26778
26809
|
await ensureWorkspace(agent.id);
|
|
26779
26810
|
await loadSkillsForAgent(agent);
|
|
@@ -27214,45 +27245,88 @@ import { readFile as readFile8, appendFile as appendFile2, mkdir as mkdir6 } fro
|
|
|
27214
27245
|
import { join as join10 } from "path";
|
|
27215
27246
|
|
|
27216
27247
|
// src/core/graphs.ts
|
|
27217
|
-
import { readFile as readFile7, readdir as readdir4, writeFile as writeFile4, mkdir as mkdir5 } from "fs/promises";
|
|
27248
|
+
import { readFile as readFile7, readdir as readdir4, writeFile as writeFile4, mkdir as mkdir5, copyFile, unlink as unlink2 } from "fs/promises";
|
|
27218
27249
|
import { join as join9 } from "path";
|
|
27250
|
+
import { existsSync as existsSync3 } from "fs";
|
|
27219
27251
|
var DEFAULT_GRAPHS_DIR = join9(process.env.HOME || process.env.USERPROFILE || "~", ".agent-os", "graphs");
|
|
27220
27252
|
function getGraphsDir() {
|
|
27221
27253
|
return process.env.AGENT_OS_GRAPHS_DIR || DEFAULT_GRAPHS_DIR;
|
|
27222
27254
|
}
|
|
27255
|
+
function getSeedGraphsDir() {
|
|
27256
|
+
if (process.env.AGENT_OS_SEED_GRAPHS_DIR)
|
|
27257
|
+
return process.env.AGENT_OS_SEED_GRAPHS_DIR;
|
|
27258
|
+
const fromDist = join9(import.meta.dir, "..", "data", "graphs");
|
|
27259
|
+
const fromSrc = join9(import.meta.dir, "..", "..", "data", "graphs");
|
|
27260
|
+
if (existsSync3(fromDist))
|
|
27261
|
+
return fromDist;
|
|
27262
|
+
if (existsSync3(fromSrc))
|
|
27263
|
+
return fromSrc;
|
|
27264
|
+
return join9(process.cwd(), "data", "graphs");
|
|
27265
|
+
}
|
|
27223
27266
|
async function listGraphs() {
|
|
27224
27267
|
const dir = getGraphsDir();
|
|
27268
|
+
let entries;
|
|
27225
27269
|
try {
|
|
27226
|
-
|
|
27227
|
-
|
|
27228
|
-
|
|
27229
|
-
|
|
27230
|
-
|
|
27231
|
-
|
|
27232
|
-
|
|
27270
|
+
entries = await readdir4(dir);
|
|
27271
|
+
} catch (err) {
|
|
27272
|
+
if (err.code !== "ENOENT")
|
|
27273
|
+
throw err;
|
|
27274
|
+
entries = [];
|
|
27275
|
+
}
|
|
27276
|
+
if (entries.length === 0) {
|
|
27277
|
+
const seedDir = getSeedGraphsDir();
|
|
27278
|
+
try {
|
|
27279
|
+
const seedEntries = await readdir4(seedDir);
|
|
27280
|
+
await mkdir5(dir, { recursive: true });
|
|
27281
|
+
for (const name of seedEntries) {
|
|
27282
|
+
if (name.endsWith(".json")) {
|
|
27283
|
+
await copyFile(join9(seedDir, name), join9(dir, name));
|
|
27284
|
+
}
|
|
27233
27285
|
}
|
|
27286
|
+
entries = await readdir4(dir);
|
|
27287
|
+
} catch {}
|
|
27288
|
+
}
|
|
27289
|
+
const summaries = [];
|
|
27290
|
+
for (const name of entries) {
|
|
27291
|
+
if (name.endsWith(".json")) {
|
|
27292
|
+
const id = name.replace(/\.graph\.json$/, "").replace(/\.json$/, "");
|
|
27293
|
+
if (id)
|
|
27294
|
+
summaries.push({ id });
|
|
27234
27295
|
}
|
|
27235
|
-
return summaries;
|
|
27236
|
-
} catch (err) {
|
|
27237
|
-
if (err.code === "ENOENT")
|
|
27238
|
-
return [];
|
|
27239
|
-
throw err;
|
|
27240
27296
|
}
|
|
27297
|
+
return summaries;
|
|
27241
27298
|
}
|
|
27242
27299
|
async function loadGraph(id) {
|
|
27243
27300
|
const dir = getGraphsDir();
|
|
27244
27301
|
try {
|
|
27245
|
-
|
|
27302
|
+
let entries;
|
|
27303
|
+
try {
|
|
27304
|
+
entries = await readdir4(dir);
|
|
27305
|
+
} catch (e) {
|
|
27306
|
+
if (e.code !== "ENOENT")
|
|
27307
|
+
throw e;
|
|
27308
|
+
entries = [];
|
|
27309
|
+
}
|
|
27246
27310
|
const file = entries.find((name) => name === `${id}.json` || name === `${id}.graph.json`);
|
|
27247
|
-
if (
|
|
27311
|
+
if (file) {
|
|
27312
|
+
const raw = await readFile7(join9(dir, file), "utf-8");
|
|
27313
|
+
const parsed = JSON.parse(raw);
|
|
27314
|
+
validateGraph(parsed);
|
|
27315
|
+
return parsed;
|
|
27316
|
+
}
|
|
27317
|
+
const seedDir = getSeedGraphsDir();
|
|
27318
|
+
const seedFile = `${id}.json`;
|
|
27319
|
+
try {
|
|
27320
|
+
const raw = await readFile7(join9(seedDir, seedFile), "utf-8");
|
|
27321
|
+
const parsed = JSON.parse(raw);
|
|
27322
|
+
validateGraph(parsed);
|
|
27323
|
+
await mkdir5(dir, { recursive: true });
|
|
27324
|
+
await writeFile4(join9(dir, seedFile), raw, "utf-8");
|
|
27325
|
+
return parsed;
|
|
27326
|
+
} catch {
|
|
27248
27327
|
return null;
|
|
27249
|
-
|
|
27250
|
-
const parsed = JSON.parse(raw);
|
|
27251
|
-
validateGraph(parsed);
|
|
27252
|
-
return parsed;
|
|
27328
|
+
}
|
|
27253
27329
|
} catch (err) {
|
|
27254
|
-
if (err.code === "ENOENT")
|
|
27255
|
-
return null;
|
|
27256
27330
|
console.error("[graphs] Failed to load graph:", err);
|
|
27257
27331
|
return null;
|
|
27258
27332
|
}
|
|
@@ -27264,6 +27338,22 @@ async function saveGraph(graph) {
|
|
|
27264
27338
|
const path = join9(dir, `${graph.id}.json`);
|
|
27265
27339
|
await writeFile4(path, JSON.stringify(graph, null, 2), "utf-8");
|
|
27266
27340
|
}
|
|
27341
|
+
async function deleteGraph(id) {
|
|
27342
|
+
if (!id || typeof id !== "string")
|
|
27343
|
+
throw new Error("Graph id required");
|
|
27344
|
+
const dir = getGraphsDir();
|
|
27345
|
+
let entries;
|
|
27346
|
+
try {
|
|
27347
|
+
entries = await readdir4(dir);
|
|
27348
|
+
} catch (err) {
|
|
27349
|
+
if (err.code !== "ENOENT")
|
|
27350
|
+
throw err;
|
|
27351
|
+
return;
|
|
27352
|
+
}
|
|
27353
|
+
const file = entries.find((name) => name === `${id}.json` || name === `${id}.graph.json`);
|
|
27354
|
+
if (file)
|
|
27355
|
+
await unlink2(join9(dir, file));
|
|
27356
|
+
}
|
|
27267
27357
|
function validateGraph(graph) {
|
|
27268
27358
|
if (!graph || typeof graph !== "object") {
|
|
27269
27359
|
throw new Error("Graph must be an object");
|
|
@@ -27278,6 +27368,7 @@ function validateGraph(graph) {
|
|
|
27278
27368
|
throw new Error("Graph.edges must be an array");
|
|
27279
27369
|
}
|
|
27280
27370
|
}
|
|
27371
|
+
var DEFAULT_GRAPH_MAX_TURNS_PER_NODE = 5;
|
|
27281
27372
|
function topologicalLevels(graph) {
|
|
27282
27373
|
const nodes = graph.nodes.map((n) => n.id);
|
|
27283
27374
|
const incoming = new Map;
|
|
@@ -27330,7 +27421,7 @@ function getPredecessors(graph) {
|
|
|
27330
27421
|
return pred;
|
|
27331
27422
|
}
|
|
27332
27423
|
async function runGraph(options) {
|
|
27333
|
-
const { graph, input } = options;
|
|
27424
|
+
const { graph, input, max_turns_per_node = DEFAULT_GRAPH_MAX_TURNS_PER_NODE } = options;
|
|
27334
27425
|
const levels = topologicalLevels(graph);
|
|
27335
27426
|
const predecessors = getPredecessors(graph);
|
|
27336
27427
|
const outputs = new Map;
|
|
@@ -27355,7 +27446,11 @@ async function runGraph(options) {
|
|
|
27355
27446
|
const taskInput = preds.length === 0 ? input : preds.map((p) => outputs.get(p) ?? "").filter(Boolean).join(`
|
|
27356
27447
|
|
|
27357
27448
|
`) || input;
|
|
27358
|
-
const result = await runAgent({
|
|
27449
|
+
const result = await runAgent({
|
|
27450
|
+
agent,
|
|
27451
|
+
task: taskInput,
|
|
27452
|
+
maxTurnsOverride: max_turns_per_node
|
|
27453
|
+
});
|
|
27359
27454
|
outputs.set(node.id, result.output || "");
|
|
27360
27455
|
return {
|
|
27361
27456
|
node_id: node.id,
|
|
@@ -27378,7 +27473,7 @@ async function runGraph(options) {
|
|
|
27378
27473
|
};
|
|
27379
27474
|
}
|
|
27380
27475
|
async function runGraphStream(options, onEvent) {
|
|
27381
|
-
const { graph, input } = options;
|
|
27476
|
+
const { graph, input, max_turns_per_node = DEFAULT_GRAPH_MAX_TURNS_PER_NODE } = options;
|
|
27382
27477
|
const levels = topologicalLevels(graph);
|
|
27383
27478
|
const predecessors = getPredecessors(graph);
|
|
27384
27479
|
const outputs = new Map;
|
|
@@ -27404,7 +27499,11 @@ async function runGraphStream(options, onEvent) {
|
|
|
27404
27499
|
const taskInput = preds.length === 0 ? input : preds.map((p) => outputs.get(p) ?? "").filter(Boolean).join(`
|
|
27405
27500
|
|
|
27406
27501
|
`) || input;
|
|
27407
|
-
const result = await runAgent({
|
|
27502
|
+
const result = await runAgent({
|
|
27503
|
+
agent,
|
|
27504
|
+
task: taskInput,
|
|
27505
|
+
maxTurnsOverride: max_turns_per_node
|
|
27506
|
+
});
|
|
27408
27507
|
outputs.set(node.id, result.output || "");
|
|
27409
27508
|
const payload = {
|
|
27410
27509
|
node_id: node.id,
|
|
@@ -27858,8 +27957,10 @@ async function handleSkillInstall(req) {
|
|
|
27858
27957
|
return jsonResponse({ error: "Provide path or url" }, 400);
|
|
27859
27958
|
}
|
|
27860
27959
|
const slug = typeof body.slug === "string" && body.slug.trim() !== "" ? body.slug.trim() : undefined;
|
|
27960
|
+
const version2 = typeof body.version === "string" && body.version.trim() !== "" ? body.version.trim() : undefined;
|
|
27961
|
+
const meta = version2 || slug ? { version: version2, source: slug ? "hub" : undefined } : undefined;
|
|
27861
27962
|
try {
|
|
27862
|
-
const result = hasPath ? await installSkillFromPath(body.path.trim()) : await installSkillFromUrl(body.url.trim(), slug);
|
|
27963
|
+
const result = hasPath ? await installSkillFromPath(body.path.trim()) : await installSkillFromUrl(body.url.trim(), slug, meta);
|
|
27863
27964
|
return jsonResponse({ skill: result }, 201);
|
|
27864
27965
|
} catch (err) {
|
|
27865
27966
|
const msg = errorMessage(err);
|
|
@@ -29099,14 +29200,14 @@ async function loadPlugins() {
|
|
|
29099
29200
|
// src/server.ts
|
|
29100
29201
|
init_config();
|
|
29101
29202
|
import { join as join13, dirname as dirname2, resolve as resolve4 } from "path";
|
|
29102
|
-
import { mkdirSync, existsSync as
|
|
29203
|
+
import { mkdirSync, existsSync as existsSync4 } from "fs";
|
|
29103
29204
|
import { mkdir as mkdir7 } from "fs/promises";
|
|
29104
29205
|
var PORT = parseInt(process.env.PORT ?? "3010", 10);
|
|
29105
29206
|
var DASHBOARD_DIST = (() => {
|
|
29106
29207
|
const root = join13(import.meta.dir, "..");
|
|
29107
29208
|
const a = resolve4(join13(root, "dashboard-dist"));
|
|
29108
29209
|
const b = resolve4(join13(root, "dashboard", "dist"));
|
|
29109
|
-
if (
|
|
29210
|
+
if (existsSync4(a) && existsSync4(join13(a, "index.html")))
|
|
29110
29211
|
return a;
|
|
29111
29212
|
return b;
|
|
29112
29213
|
})();
|
|
@@ -29198,7 +29299,7 @@ function broadcastEvent(event) {
|
|
|
29198
29299
|
}
|
|
29199
29300
|
}
|
|
29200
29301
|
function serveDashboard(pathname) {
|
|
29201
|
-
if (!
|
|
29302
|
+
if (!existsSync4(DASHBOARD_DIST) || !existsSync4(join13(DASHBOARD_DIST, "index.html"))) {
|
|
29202
29303
|
return null;
|
|
29203
29304
|
}
|
|
29204
29305
|
const decoded = decodeURIComponent(pathname);
|
|
@@ -29207,7 +29308,7 @@ function serveDashboard(pathname) {
|
|
|
29207
29308
|
}
|
|
29208
29309
|
const subpath = decoded === "/" ? "index.html" : decoded.slice(1);
|
|
29209
29310
|
const filePath = join13(DASHBOARD_DIST, subpath);
|
|
29210
|
-
if (
|
|
29311
|
+
if (existsSync4(filePath)) {
|
|
29211
29312
|
const file = Bun.file(filePath);
|
|
29212
29313
|
const ext = subpath.split(".").pop() ?? "";
|
|
29213
29314
|
const mime = {
|
|
@@ -29228,7 +29329,7 @@ function serveDashboard(pathname) {
|
|
|
29228
29329
|
});
|
|
29229
29330
|
}
|
|
29230
29331
|
const indexPath = join13(DASHBOARD_DIST, "index.html");
|
|
29231
|
-
if (
|
|
29332
|
+
if (existsSync4(indexPath)) {
|
|
29232
29333
|
return new Response(Bun.file(indexPath), {
|
|
29233
29334
|
headers: { "Content-Type": "text/html" }
|
|
29234
29335
|
});
|
|
@@ -29448,6 +29549,16 @@ function createRoutes() {
|
|
|
29448
29549
|
const msg = e instanceof Error ? e.message : String(e);
|
|
29449
29550
|
return jsonResponse({ error: msg }, 400);
|
|
29450
29551
|
}
|
|
29552
|
+
},
|
|
29553
|
+
DELETE: async (req) => {
|
|
29554
|
+
const id = decodeURIComponent(req.params.id);
|
|
29555
|
+
try {
|
|
29556
|
+
await deleteGraph(id);
|
|
29557
|
+
return Response.json({ ok: true }, { headers: CORS_HEADERS });
|
|
29558
|
+
} catch (e) {
|
|
29559
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
29560
|
+
return jsonResponse({ error: msg }, 400);
|
|
29561
|
+
}
|
|
29451
29562
|
}
|
|
29452
29563
|
},
|
|
29453
29564
|
"/api/memory/write": {
|
|
@@ -29668,7 +29779,7 @@ async function startServer() {
|
|
|
29668
29779
|
await loadPlugins();
|
|
29669
29780
|
await seedAgentsIfEmpty();
|
|
29670
29781
|
const dashboardSecret = await getDashboardSecret();
|
|
29671
|
-
const dashboardMissing = !
|
|
29782
|
+
const dashboardMissing = !existsSync4(DASHBOARD_DIST) || !existsSync4(join13(DASHBOARD_DIST, "index.html"));
|
|
29672
29783
|
if (dashboardMissing) {
|
|
29673
29784
|
console.warn(`[sulala] Dashboard not found at ${DASHBOARD_DIST}. From package root run: cd dashboard && npm run build. If using a global install, reinstall: bun install -g @sulala/agent-os@latest`);
|
|
29674
29785
|
}
|
package/package.json
CHANGED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: gmail
|
|
3
|
-
description: Send email via Gmail using your connected account. Uses Sulala Portal for the access token; includes a script to send email from the command line or automation. Requires the Sulala Portal skill for token. Use when the user wants to send email, compose mail, or automate Gmail sending.
|
|
4
|
-
metadata:
|
|
5
|
-
sulala:
|
|
6
|
-
requires:
|
|
7
|
-
skills:
|
|
8
|
-
- sulala-portal
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# Gmail (Sulala Portal)
|
|
12
|
-
|
|
13
|
-
Send email using your **Gmail account connected in the Sulala Portal**. This skill provides a **script**; use the **exec** tool to run it. No static send-email code in the loader; the skill is script + docs like YouTube Shorts automation.
|
|
14
|
-
|
|
15
|
-
## Overview
|
|
16
|
-
|
|
17
|
-
1. **Get an access token** from the Portal (list connections, then `POST connections/{connection_id}/use` for Gmail).
|
|
18
|
-
2. **Send email** by running the script via **exec** (see below).
|
|
19
|
-
|
|
20
|
-
## Getting the token (Portal)
|
|
21
|
-
|
|
22
|
-
- **Tool**: `sulala-portal_request`
|
|
23
|
-
- **List connections**: `GET` path `connections` (filter by `provider === "gmail"` in the response).
|
|
24
|
-
- **Get token**: `POST` path `connections/{id}/use`. Use the connection’s `connection_id` from the list (e.g. `conn_gmail_...`). If you get 404, use `connection_id` in the path instead of `id`. Response: `{ connectionId, provider, accessToken, scopes }`.
|
|
25
|
-
|
|
26
|
-
## Sending email (use exec tool + script — preferred)
|
|
27
|
-
|
|
28
|
-
Use the **exec** tool to run the skill script. No need to build RFC 2822 or base64url by hand.
|
|
29
|
-
|
|
30
|
-
1. Get **accessToken** from the Portal (`sulala-portal_request`: list connections, then `POST connections/{connection_id}/use` with the Gmail connection’s `connection_id`).
|
|
31
|
-
2. Call **exec** with:
|
|
32
|
-
- **skill_id**: `"gmail"` (so the command runs in this skill’s directory).
|
|
33
|
-
- **command**: `python3 scripts/send_email.py --token "<accessToken>" --to "recipient@example.com" --subject "Subject line" --body "Email body text."`
|
|
34
|
-
|
|
35
|
-
Example exec call:
|
|
36
|
-
|
|
37
|
-
```json
|
|
38
|
-
{
|
|
39
|
-
"skill_id": "gmail",
|
|
40
|
-
"command": "python3 scripts/send_email.py --token \"<paste accessToken from Portal>\" --to \"sai.ko@mothernode.com\" --subject \"test title\" --body \"test body\""
|
|
41
|
-
}
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
- **Token**: from `POST connections/{connection_id}/use` (see above).
|
|
45
|
-
- **To**, **Subject**, **Body**: recipient email, subject line, and body text. Escape quotes inside the command string as needed.
|
|
46
|
-
|
|
47
|
-
Full script options: [references/send-email.md](references/send-email.md).
|
|
48
|
-
|
|
49
|
-
## Skill layout (like YouTube Shorts automation)
|
|
50
|
-
|
|
51
|
-
- **scripts/send_email.py** — runnable script: `--token`, `--to`, `--subject`, `--body` or `--body-file`.
|
|
52
|
-
- **references/send-email.md** — how to run the script and get the token.
|
|
53
|
-
- **SKILL.md** — this file: overview, get token, send via exec + script.
|
|
54
|
-
|
|
55
|
-
No separate config: the Sulala Portal skill provides the gateway and token; this skill adds the script and docs.
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# Send email script
|
|
2
|
-
|
|
3
|
-
The Gmail skill includes a script that sends email using the Gmail API with an OAuth2 access token. Use it when you have a token (e.g. from Sulala Portal) and want to send from the command line or from automation.
|
|
4
|
-
|
|
5
|
-
## Prerequisites
|
|
6
|
-
|
|
7
|
-
- **Access token**: Obtain from Sulala Portal (`POST connections/{connection_id}/use` with your Gmail connection), or from your own OAuth flow. Token must have `https://www.googleapis.com/auth/gmail.send` scope.
|
|
8
|
-
- **Python 3.6+**: No extra pip packages; script uses only the standard library.
|
|
9
|
-
|
|
10
|
-
## Usage
|
|
11
|
-
|
|
12
|
-
From the skill directory (or with paths adjusted):
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
python3 scripts/send_email.py \
|
|
16
|
-
--token "YOUR_ACCESS_TOKEN" \
|
|
17
|
-
--to "recipient@example.com" \
|
|
18
|
-
--subject "Subject line" \
|
|
19
|
-
--body "Email body text."
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
With body from a file:
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
python3 scripts/send_email.py \
|
|
26
|
-
--token "YOUR_ACCESS_TOKEN" \
|
|
27
|
-
--to "recipient@example.com" \
|
|
28
|
-
--subject "Subject" \
|
|
29
|
-
--body-file path/to/body.txt
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Options
|
|
33
|
-
|
|
34
|
-
| Option | Required | Description |
|
|
35
|
-
|-------------|----------|--------------------------------------|
|
|
36
|
-
| `--token` | Yes | OAuth2 access token (Bearer). |
|
|
37
|
-
| `--to` | Yes | Recipient email address. |
|
|
38
|
-
| `--subject` | Yes | Email subject line. |
|
|
39
|
-
| `--body` | One of | Email body as a string. |
|
|
40
|
-
| `--body-file` | One of | Path to file containing body text. |
|
|
41
|
-
|
|
42
|
-
## Getting the token (Sulala Portal)
|
|
43
|
-
|
|
44
|
-
1. Call the Portal API to list connections: `GET connections` (filter for `provider === "gmail"`).
|
|
45
|
-
2. Call `POST connections/{connection_id}/use` with the Gmail connection’s `connection_id` (e.g. `conn_gmail_...`; use `connection_id` if `id` returns 404).
|
|
46
|
-
3. Use the `accessToken` from the response as `--token`.
|
|
47
|
-
|
|
48
|
-
## Troubleshooting
|
|
49
|
-
|
|
50
|
-
| Problem | Cause | Action |
|
|
51
|
-
|----------------------|--------------------------|---------------------------------|
|
|
52
|
-
| 401 Unauthorized | Token expired or invalid | Refresh or re-issue token. |
|
|
53
|
-
| Recipient required | Missing/invalid To | Ensure `--to` is a valid email. |
|
|
54
|
-
| Invalid To header | Bad To format | Use plain email, no extra text. |
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Send email via Gmail API using an OAuth access token.
|
|
3
|
-
|
|
4
|
-
Use when you have a token from Sulala Portal (connections/:id/use) or another source.
|
|
5
|
-
No local OAuth files required; pass the token on the command line.
|
|
6
|
-
|
|
7
|
-
Usage:
|
|
8
|
-
python3 send_email.py --token TOKEN --to recipient@example.com --subject "Subject" --body "Body text"
|
|
9
|
-
python3 send_email.py --token TOKEN --to recipient@example.com --subject "Subject" --body-file body.txt
|
|
10
|
-
|
|
11
|
-
Requires: Python 3.6+. No extra pip packages (uses urllib and base64 from stdlib).
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
import argparse
|
|
15
|
-
import base64
|
|
16
|
-
import json
|
|
17
|
-
import sys
|
|
18
|
-
import urllib.request
|
|
19
|
-
import urllib.error
|
|
20
|
-
|
|
21
|
-
GMAIL_SEND_URL = "https://gmail.googleapis.com/gmail/v1/users/me/messages/send"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def base64url_encode(data: bytes) -> str:
|
|
25
|
-
"""Encode bytes to base64url (RFC 4648): no +/ padding, use -_."""
|
|
26
|
-
b64 = base64.b64encode(data).decode("ascii")
|
|
27
|
-
return b64.replace("+", "-").replace("/", "_").rstrip("=")
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def build_rfc2822(to: str, subject: str, body: str) -> str:
|
|
31
|
-
"""Build a minimal RFC 2822 message (CRLF line endings)."""
|
|
32
|
-
lines = [
|
|
33
|
-
f"To: {to}",
|
|
34
|
-
f"Subject: {subject}",
|
|
35
|
-
"Content-Type: text/plain; charset=UTF-8",
|
|
36
|
-
"",
|
|
37
|
-
body,
|
|
38
|
-
]
|
|
39
|
-
return "\r\n".join(lines)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def send(token: str, to: str, subject: str, body: str) -> None:
|
|
43
|
-
rfc2822 = build_rfc2822(to, subject, body)
|
|
44
|
-
raw = base64url_encode(rfc2822.encode("utf-8"))
|
|
45
|
-
body_json = json.dumps({"raw": raw}).encode("utf-8")
|
|
46
|
-
|
|
47
|
-
req = urllib.request.Request(
|
|
48
|
-
GMAIL_SEND_URL,
|
|
49
|
-
data=body_json,
|
|
50
|
-
headers={
|
|
51
|
-
"Authorization": f"Bearer {token}",
|
|
52
|
-
"Content-Type": "application/json",
|
|
53
|
-
},
|
|
54
|
-
method="POST",
|
|
55
|
-
)
|
|
56
|
-
try:
|
|
57
|
-
with urllib.request.urlopen(req) as resp:
|
|
58
|
-
if resp.status != 200:
|
|
59
|
-
sys.stderr.write(f"Unexpected status: {resp.status}\n")
|
|
60
|
-
sys.exit(1)
|
|
61
|
-
print("Email sent successfully.")
|
|
62
|
-
except urllib.error.HTTPError as e:
|
|
63
|
-
sys.stderr.write(f"Gmail API error: {e.code} {e.reason}\n")
|
|
64
|
-
if e.fp:
|
|
65
|
-
body = e.fp.read().decode("utf-8", errors="replace")
|
|
66
|
-
sys.stderr.write(body)
|
|
67
|
-
sys.stderr.write("\n")
|
|
68
|
-
sys.exit(1)
|
|
69
|
-
except OSError as e:
|
|
70
|
-
sys.stderr.write(f"Request failed: {e}\n")
|
|
71
|
-
sys.exit(1)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def main() -> None:
|
|
75
|
-
ap = argparse.ArgumentParser(description="Send email via Gmail API with an access token.")
|
|
76
|
-
ap.add_argument("--token", required=True, help="OAuth2 access token (e.g. from Sulala Portal)")
|
|
77
|
-
ap.add_argument("--to", required=True, help="Recipient email address")
|
|
78
|
-
ap.add_argument("--subject", required=True, help="Email subject")
|
|
79
|
-
group = ap.add_mutually_exclusive_group(required=True)
|
|
80
|
-
group.add_argument("--body", help="Email body (plain text)")
|
|
81
|
-
group.add_argument("--body-file", help="Path to file containing email body")
|
|
82
|
-
args = ap.parse_args()
|
|
83
|
-
|
|
84
|
-
if args.body is not None:
|
|
85
|
-
body = args.body
|
|
86
|
-
else:
|
|
87
|
-
with open(args.body_file, "r", encoding="utf-8") as f:
|
|
88
|
-
body = f.read()
|
|
89
|
-
|
|
90
|
-
send(args.token, args.to, args.subject, body)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if __name__ == "__main__":
|
|
94
|
-
main()
|