@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.
Files changed (39) hide show
  1. package/dashboard-dist/assets/index-BZYG7rCd.js +75 -0
  2. package/dashboard-dist/assets/index-CAOgf_FY.css +1 -0
  3. package/dashboard-dist/assets/index-CVI9FAmG.css +1 -0
  4. package/dashboard-dist/assets/index-DdMu_Z6v.js +75 -0
  5. package/dashboard-dist/index.html +2 -2
  6. package/data/agents/crm_hubspot_agent.json +11 -0
  7. package/data/agents/research_agent.json +1 -1
  8. package/data/agents/social_media_agent.json +1 -1
  9. package/data/agents/source_verify_agent.json +10 -0
  10. package/data/agents/writer_agent.json +1 -1
  11. package/data/skills/bluesky/SKILL.md +1 -1
  12. package/data/skills/content-writing/SKILL.md +32 -0
  13. package/data/skills/date/SKILL.md +1 -1
  14. package/data/skills/fetch/SKILL.md +1 -1
  15. package/data/skills/file-search/SKILL.md +1 -1
  16. package/data/skills/file-stats/SKILL.md +1 -1
  17. package/data/skills/git/SKILL.md +1 -1
  18. package/data/skills/hash/SKILL.md +1 -1
  19. package/data/skills/jq/SKILL.md +1 -1
  20. package/data/skills/markdown-to-html/SKILL.md +1 -1
  21. package/data/skills/qr-code/SKILL.md +1 -1
  22. package/data/skills/rss/SKILL.md +1 -1
  23. package/data/skills/translate/SKILL.md +1 -1
  24. package/data/skills/weather/SKILL.md +1 -1
  25. package/data/skills/web-search/SKILL.md +1 -1
  26. package/data/skills/webhook/SKILL.md +1 -1
  27. package/dist/cli.js +234 -69
  28. package/dist/index.js +168 -57
  29. package/package.json +1 -1
  30. package/data/skills/gmail/SKILL.md +0 -55
  31. package/data/skills/gmail/references/send-email.md +0 -54
  32. package/data/skills/gmail/scripts/send_email.py +0 -94
  33. package/data/skills/youtube/SKILL.md +0 -91
  34. package/data/skills/youtube/config.schema.json +0 -11
  35. package/data/skills/youtube/package.json +0 -8
  36. package/data/skills/youtube/references/youtube-upload.md +0 -65
  37. package/data/skills/youtube/requirements.txt +0 -3
  38. package/data/skills/youtube/scripts/youtube_upload.js +0 -200
  39. 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 proc2 = Bun.spawn({ cmd: ["unzip", "-q", "-o", zipPath, "-d", tmpDir], stdout: "ignore", stderr: "pipe" });
19943
- const exit2 = await proc2.exited;
19944
- if (exit2 !== 0) {
19945
- const err = await new Response(proc2.stderr).text();
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: id2, sourcePath: sourcePath2 } = await chooseSkillRootFromExtract(tmpDir, "archive.zip");
19949
- const destId2 = explicitId != null && explicitId.trim() !== "" ? slugToSkillId(explicitId) : id2;
19950
- await cp(sourcePath2, join6(skillsDir, destId2), { recursive: true });
19951
- return { id: destId2 };
19952
- }
19953
- const tarPath = join6(tmpDir, "archive.tar.gz");
19954
- await writeFile3(tarPath, new Uint8Array(buf));
19955
- const proc = Bun.spawn({ cmd: ["tar", "-xzf", tarPath, "-C", tmpDir], stdout: "ignore", stderr: "pipe" });
19956
- const exit = await proc.exited;
19957
- if (exit !== 0) {
19958
- const err = await new Response(proc.stderr).text();
19959
- throw new Error(`tar extract failed: ${err}`);
19960
- }
19961
- const { id, sourcePath } = await chooseSkillRootFromExtract(tmpDir, "archive.tar.gz");
19962
- const destId = explicitId != null && explicitId.trim() !== "" ? slugToSkillId(explicitId) : id;
19963
- await cp(sourcePath, join6(skillsDir, destId), { recursive: true });
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
- const entries = await readdir4(dir);
27227
- const summaries = [];
27228
- for (const name of entries) {
27229
- if (name.endsWith(".json")) {
27230
- const id = name.replace(/\.graph\.json$/, "").replace(/\.json$/, "");
27231
- if (id)
27232
- summaries.push({ id });
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
- const entries = await readdir4(dir);
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 (!file)
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
- const raw = await readFile7(join9(dir, file), "utf-8");
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({ agent, task: taskInput });
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({ agent, task: taskInput });
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 existsSync3 } from "fs";
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 (existsSync3(a) && existsSync3(join13(a, "index.html")))
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 (!existsSync3(DASHBOARD_DIST) || !existsSync3(join13(DASHBOARD_DIST, "index.html"))) {
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 (existsSync3(filePath)) {
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 (existsSync3(indexPath)) {
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 = !existsSync3(DASHBOARD_DIST) || !existsSync3(join13(DASHBOARD_DIST, "index.html"));
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@sulala/agent-os",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -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()