@lazyingart/agintiflow 0.20.47 → 0.20.49

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.
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import {
6
+ buildSkillPackFromMarkdown,
7
+ buildSkillMeshServiceUnit,
8
+ enableSkillMeshSkill,
9
+ installSkillPack,
10
+ listInstalledSkillMeshSkills,
11
+ loadSkillMeshConfig,
12
+ setSkillMeshMode,
13
+ startSkillMeshRelay,
14
+ submitSkillPack,
15
+ syncSkillMesh,
16
+ validateSkillPack,
17
+ } from "../src/skillmesh.js";
18
+ import { listSkills, selectSkillsForGoal } from "../src/skill-library.js";
19
+
20
+ function assert(condition, message) {
21
+ if (!condition) throw new Error(message);
22
+ }
23
+
24
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "aginti-skillmesh-"));
25
+ process.env.AGINTIFLOW_HOME = path.join(tempRoot, "home");
26
+
27
+ const skillMarkdown = [
28
+ "---",
29
+ "id: mesh-android-screenshot",
30
+ "label: Mesh Android Screenshot",
31
+ "description: Capture Android emulator screenshots safely and save them in the current workspace.",
32
+ "triggers:",
33
+ " - android screenshot",
34
+ " - emulator capture",
35
+ "tools:",
36
+ " - shell",
37
+ "---",
38
+ "",
39
+ "When asked to capture an Android emulator screenshot, inspect available devices, run `adb exec-out screencap -p`,",
40
+ "save to a descriptive workspace path, then verify the PNG exists before reporting success.",
41
+ "",
42
+ ].join("\n");
43
+
44
+ const pack = await buildSkillPackFromMarkdown(skillMarkdown, { valueScore: 95 });
45
+ const validation = validateSkillPack(pack);
46
+ assert(validation.packHash === pack.packHash, "pack hash should validate");
47
+
48
+ const installedDisabled = await installSkillPack(pack, { enabled: false });
49
+ assert(installedDisabled.installedSkills.includes("mesh-android-screenshot"), "pack did not install skill");
50
+ assert(!listSkills({ includeBody: false }).some((skill) => skill.id === "mesh-android-screenshot"), "disabled mesh skill should not load");
51
+
52
+ await enableSkillMeshSkill("mesh-android-screenshot", true);
53
+ assert(listSkills({ includeBody: false }).some((skill) => skill.id === "mesh-android-screenshot"), "enabled mesh skill should load");
54
+ assert(
55
+ selectSkillsForGoal("please take an android emulator screenshot", { limit: 6 }).some((skill) => skill.id === "mesh-android-screenshot"),
56
+ "enabled mesh skill should be selectable"
57
+ );
58
+
59
+ const installed = await listInstalledSkillMeshSkills();
60
+ assert(installed.some((skill) => skill.id === "mesh-android-screenshot"), "installed mesh skill should list");
61
+
62
+ await setSkillMeshMode("off");
63
+ assert(!listSkills({ includeBody: false }).some((skill) => skill.id === "mesh-android-screenshot"), "off mode should suppress mesh skills");
64
+ await setSkillMeshMode("share");
65
+ const config = await loadSkillMeshConfig();
66
+ assert(config.mode === "share", "share mode should persist");
67
+ const serviceUnit = buildSkillMeshServiceUnit({
68
+ dataDir: path.join(tempRoot, "relay-service"),
69
+ runScript: path.join(tempRoot, "relay-service", "aginti-skill-relay.run.sh"),
70
+ scope: "system",
71
+ user: "aginti-test",
72
+ });
73
+ assert(serviceUnit.includes("Restart=on-failure"), "systemd unit should restart relay");
74
+ assert(serviceUnit.includes("WantedBy=multi-user.target"), "systemd unit should start at boot in system scope");
75
+ assert(serviceUnit.includes("ExecStart="), "systemd unit should include ExecStart");
76
+
77
+ const secretMarkdown = skillMarkdown.replace("mesh-android-screenshot", "mesh-secret-test") + "\nOPENAI_API_KEY=sk-thisShouldBeRejected1234567890\n";
78
+ let rejected = false;
79
+ try {
80
+ await buildSkillPackFromMarkdown(secretMarkdown);
81
+ } catch {
82
+ rejected = true;
83
+ }
84
+ assert(rejected, "secret-like skill pack should be rejected");
85
+
86
+ const relay = await startSkillMeshRelay({ host: "127.0.0.1", port: 0, dataDir: path.join(tempRoot, "relay") });
87
+ try {
88
+ const submit = await submitSkillPack(pack, relay.url);
89
+ assert(submit.ok && submit.packHash === pack.packHash, "relay submit failed");
90
+ const health = await fetch(`${relay.url}/health`).then((response) => response.json());
91
+ assert(health.ok && health.acceptsRawSessions === false, "relay health should advertise safe contract");
92
+ const feed = await fetch(`${relay.url}/feed.json`).then((response) => response.json());
93
+ assert(feed.packs.some((item) => item.packHash === pack.packHash), "relay feed should include submitted pack");
94
+ const nodes = await fetch(`${relay.url}/nodes.json`).then((response) => response.json());
95
+ assert(Array.isArray(nodes.nodes), "relay should expose a node list");
96
+
97
+ process.env.AGINTIFLOW_HOME = path.join(tempRoot, "client-home");
98
+ const sync = await syncSkillMesh({ nodeUrl: relay.url, install: true });
99
+ assert(sync.ok && sync.downloaded.length === 1, "client sync should download one pack");
100
+ assert(!listSkills({ includeBody: false }).some((skill) => skill.id === "mesh-android-screenshot"), "synced community skill should install disabled");
101
+ } finally {
102
+ await relay.close();
103
+ }
104
+
105
+ process.env.AGINTIFLOW_HOME = path.join(tempRoot, "unreachable-home");
106
+ const unreachable = await syncSkillMesh({ nodeUrl: "http://127.0.0.1:9" });
107
+ assert(unreachable.ok && unreachable.skipped && unreachable.unreachable, "unreachable sync should fail soft");
108
+
109
+ console.log(
110
+ JSON.stringify(
111
+ {
112
+ ok: true,
113
+ checks: [
114
+ "signed-pack",
115
+ "secret-rejection",
116
+ "install-disabled-by-default",
117
+ "enable-mesh-skill",
118
+ "relay-submit-feed",
119
+ "relay-node-list",
120
+ "metadata-sync",
121
+ "soft-unreachable-node",
122
+ "systemd-service-unit",
123
+ ],
124
+ },
125
+ null,
126
+ 2
127
+ )
128
+ );
package/src/cli.js CHANGED
@@ -33,6 +33,7 @@ import { listSkills, selectSkillsForGoal } from "./skill-library.js";
33
33
  import { languageLabel, resolveLanguage } from "./i18n.js";
34
34
  import { maybeAutoUpdate } from "./auto-update.js";
35
35
  import { readHousekeepingSummary } from "./housekeeping.js";
36
+ import { handleSkillMeshCommand } from "./skillmesh.js";
36
37
  import fs from "node:fs/promises";
37
38
  import path from "node:path";
38
39
  import { fileURLToPath } from "node:url";
@@ -382,7 +383,7 @@ export function parseArgs(argv) {
382
383
 
383
384
  function printUsage() {
384
385
  console.log(
385
- 'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti update OR aginti models OR aginti skills [query] OR aginti housekeeping [--json] OR aginti auth [deepseek|openai|qwen|venice|grsai] OR aginti resume [--all-sessions] [latest|<session-id>] ["prompt"] OR aginti --remove-empty-sessions OR aginti --remove-sessions OR aginti queue <session-id> "message" OR aginti [--no-auto-update] [--language en|ja|zh-Hans|zh-Hant|ko|fr|es|ar|vi|de|ru] [--image] [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|qwen|venice|mock] [--model MODEL] [--route-model MODEL] [--main-model MODEL] [--spare-model MODEL --spare-reasoning medium] [--aux-provider grsai|venice --aux-model MODEL] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--parallel-scouts|--no-parallel-scouts --scout-count 1..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex --wrapper-model gpt-5.5] [--list-models|--list-routes] "your task"'
386
+ 'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti update OR aginti models OR aginti skills [query] OR aginti skillmesh [status|off|record|share|sync|serve|service] OR aginti housekeeping [--json] OR aginti auth [deepseek|openai|qwen|venice|grsai] OR aginti resume [--all-sessions] [latest|<session-id>] ["prompt"] OR aginti --remove-empty-sessions OR aginti --remove-sessions OR aginti queue <session-id> "message" OR aginti [--no-auto-update] [--language en|ja|zh-Hans|zh-Hant|ko|fr|es|ar|vi|de|ru] [--image] [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|qwen|venice|mock] [--model MODEL] [--route-model MODEL] [--main-model MODEL] [--spare-model MODEL --spare-reasoning medium] [--aux-provider grsai|venice --aux-model MODEL] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--parallel-scouts|--no-parallel-scouts --scout-count 1..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex --wrapper-model gpt-5.5] [--list-models|--list-routes] "your task"'
386
387
  );
387
388
  console.log(`Languages: ${["en", "ja", "zh-Hans", "zh-Hant", "ko", "fr", "es", "ar", "vi", "de", "ru"].map((code) => `${code}=${languageLabel(code)}`).join(", ")}`);
388
389
  }
@@ -496,7 +497,8 @@ function printSkills(query = "") {
496
497
  for (const skill of skills) {
497
498
  const triggers = skill.triggers?.length ? ` triggers=${skill.triggers.join(",")}` : "";
498
499
  const tools = skill.tools?.length ? ` tools=${skill.tools.join(",")}` : "";
499
- console.log(`${skill.id}: ${skill.label} - ${skill.description}${triggers}${tools}`);
500
+ const source = skill.source && skill.source !== "built-in" ? ` source=${skill.source}` : "";
501
+ console.log(`${skill.id}: ${skill.label} - ${skill.description}${triggers}${tools}${source}`);
500
502
  }
501
503
  }
502
504
 
@@ -1091,6 +1093,16 @@ export async function main(argv = process.argv.slice(2)) {
1091
1093
  return;
1092
1094
  }
1093
1095
 
1096
+ if (argv[0] === "skillmesh" || argv[0] === "skillsync" || argv[0] === "skill-sync" || argv[0] === "skill-share") {
1097
+ try {
1098
+ await handleSkillMeshCommand(argv.slice(1));
1099
+ } catch (error) {
1100
+ console.error(error instanceof Error ? error.message : String(error));
1101
+ process.exit(1);
1102
+ }
1103
+ return;
1104
+ }
1105
+
1094
1106
  if (argv[0] === "keys/status") {
1095
1107
  await handleKeyCommand(["status"]);
1096
1108
  return;
package/src/i18n.js CHANGED
@@ -134,6 +134,7 @@ const TRANSLATIONS = {
134
134
  helpResume: "Continue a saved session.",
135
135
  helpSessions: "List recent sessions in this project.",
136
136
  helpSkills: "List Markdown skills selected for a topic.",
137
+ helpSkillMesh: "Manage strict reviewed skill sharing.",
137
138
  helpProfile: "Set task profile, e.g. code, website, latex, maintenance.",
138
139
  helpWebSearch: "Enable or disable the web_search tool.",
139
140
  helpScouts: "Enable parallel DeepSeek scouts and set scout count.",
@@ -26,6 +26,13 @@ import {
26
26
  modelsForProviderGroup,
27
27
  } from "./model-routing.js";
28
28
  import { languageLabel, resolveLanguage, t } from "./i18n.js";
29
+ import {
30
+ formatSkillMeshStatus,
31
+ handleSkillMeshCommand,
32
+ loadSkillMeshConfig,
33
+ setSkillMeshMode,
34
+ skillMeshModeChoices,
35
+ } from "./skillmesh.js";
29
36
 
30
37
  const useColor = Boolean(input.isTTY && output.isTTY && process.env.AGINTIFLOW_NO_COLOR !== "1");
31
38
  const ansi = {
@@ -73,6 +80,8 @@ const SLASH_COMMANDS = [
73
80
  "/rename",
74
81
  "/skills",
75
82
  "/skill",
83
+ "/skillmesh",
84
+ "/skillsync",
76
85
  "/profile",
77
86
  "/web-search",
78
87
  "/scouts",
@@ -649,6 +658,7 @@ function printHelp() {
649
658
  ` ${command("/rename [title|auto]", "Rename the current session.", "helpRename")}`,
650
659
  ` ${command("/sessions", "List recent sessions in this project.", "helpSessions")}`,
651
660
  ` ${command("/skills [query]", "List Markdown skills selected for a topic.", "helpSkills")}`,
661
+ ` ${command("/skillmesh [status|off|record|share|sync]", "Manage strict reviewed skill sharing.", "helpSkillMesh")}`,
652
662
  ` ${command("/profile <name>", "Set task profile, e.g. code, website, latex, maintenance.", "helpProfile")}`,
653
663
  ` ${command("/web-search on|off", "Enable or disable the web_search tool.", "helpWebSearch")}`,
654
664
  ` ${command("/scouts on|off|<1-10>", "Enable parallel DeepSeek scouts and set scout count.", "helpScouts")}`,
@@ -2707,6 +2717,36 @@ async function handleCommand(line, state, packageDir) {
2707
2717
  );
2708
2718
  return true;
2709
2719
  }
2720
+ if (command === "skillmesh" || command === "skillsync") {
2721
+ if (!value && input.isTTY && output.isTTY && typeof input.setRawMode === "function") {
2722
+ const config = await loadSkillMeshConfig();
2723
+ const options = skillMeshModeChoices();
2724
+ const selected = await selectModelChoice({
2725
+ title: "Skill Mesh",
2726
+ subtitle: "Strict skill sharing. Up/Down selects, Enter confirms, Esc cancels.",
2727
+ options,
2728
+ initialIndex: Math.max(
2729
+ options.findIndex((item) => item.value === config.mode),
2730
+ 0
2731
+ ),
2732
+ escapeValue: null,
2733
+ });
2734
+ if (!selected) {
2735
+ printSystemLine(`skillmesh=${config.mode}`);
2736
+ return true;
2737
+ }
2738
+ const next = await setSkillMeshMode(selected.value);
2739
+ printSystemLine(`skillmesh=${next.mode}`);
2740
+ printAgentMessage(formatSkillMeshStatus(next));
2741
+ return true;
2742
+ }
2743
+ try {
2744
+ await handleSkillMeshCommand(value ? value.split(/\s+/).filter(Boolean) : ["status"]);
2745
+ } catch (error) {
2746
+ printAgentMessage(error instanceof Error ? error.message : String(error));
2747
+ }
2748
+ return true;
2749
+ }
2710
2750
  if (command === "profile") {
2711
2751
  state.taskProfile = normalizeTaskProfile(value || "auto");
2712
2752
  state.maxSteps = Math.max(state.maxSteps, defaultMaxStepsForProfile(state.taskProfile));
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { agintiflowHome } from "./session-index.js";
4
5
 
5
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
7
  const DEFAULT_SKILLS_DIR = path.resolve(__dirname, "..", "skills");
@@ -17,7 +18,7 @@ function parseScalar(value = "") {
17
18
  return trimmed;
18
19
  }
19
20
 
20
- function parseFrontmatter(text, filePath) {
21
+ export function parseFrontmatter(text, filePath) {
21
22
  const match = String(text || "").match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
22
23
  if (!match) throw new Error(`${filePath}: missing YAML frontmatter`);
23
24
  const meta = {};
@@ -55,7 +56,7 @@ function normalizeList(value) {
55
56
  .filter(Boolean);
56
57
  }
57
58
 
58
- function loadSkillFile(filePath) {
59
+ export function loadSkillFile(filePath) {
59
60
  const text = fs.readFileSync(filePath, "utf8");
60
61
  const { meta, body } = parseFrontmatter(text, filePath);
61
62
  for (const field of ["id", "label", "description"]) {
@@ -74,7 +75,7 @@ function loadSkillFile(filePath) {
74
75
  };
75
76
  }
76
77
 
77
- export function listSkills({ includeBody = false, skillsDir = DEFAULT_SKILLS_DIR } = {}) {
78
+ function loadSkillsFromDir({ includeBody = false, skillsDir = DEFAULT_SKILLS_DIR, source = "built-in" } = {}) {
78
79
  let dirEntries = [];
79
80
  try {
80
81
  dirEntries = fs.readdirSync(skillsDir, { withFileTypes: true });
@@ -88,11 +89,73 @@ export function listSkills({ includeBody = false, skillsDir = DEFAULT_SKILLS_DIR
88
89
  try {
89
90
  const skill = loadSkillFile(skillPath);
90
91
  if (!includeBody) delete skill.body;
92
+ skill.source = source;
91
93
  skills.push(skill);
92
94
  } catch {
93
95
  // Invalid local skill files are skipped so one bad skill does not break the agent.
94
96
  }
95
97
  }
98
+ return skills;
99
+ }
100
+
101
+ function skillMeshSkillsDir() {
102
+ return path.join(agintiflowHome(), "skillmesh", "skills");
103
+ }
104
+
105
+ function skillMeshEnabled() {
106
+ try {
107
+ const configPath = path.join(agintiflowHome(), "skillmesh", "config.json");
108
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
109
+ return String(config.mode || "share") !== "off";
110
+ } catch {
111
+ return true;
112
+ }
113
+ }
114
+
115
+ function loadEnabledSkillMeshSkills({ includeBody = false } = {}) {
116
+ if (!skillMeshEnabled()) return [];
117
+ const skillsDir = skillMeshSkillsDir();
118
+ let dirEntries = [];
119
+ try {
120
+ dirEntries = fs.readdirSync(skillsDir, { withFileTypes: true });
121
+ } catch {
122
+ return [];
123
+ }
124
+ const skills = [];
125
+ for (const entry of dirEntries) {
126
+ if (!entry.isDirectory()) continue;
127
+ const skillDir = path.join(skillsDir, entry.name);
128
+ const metadataPath = path.join(skillDir, "skillmesh.json");
129
+ try {
130
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
131
+ if (!metadata.enabled) continue;
132
+ const skill = loadSkillFile(path.join(skillDir, "SKILL.md"));
133
+ if (!includeBody) delete skill.body;
134
+ skill.source = "skillmesh";
135
+ skill.skillmesh = {
136
+ trustLevel: metadata.trustLevel || "community-reviewed",
137
+ packHash: metadata.packHash || "",
138
+ sourceFeed: metadata.sourceFeed || "",
139
+ };
140
+ skills.push(skill);
141
+ } catch {
142
+ // Invalid or unreviewed imported skills stay inert.
143
+ }
144
+ }
145
+ return skills;
146
+ }
147
+
148
+ export function listSkills({ includeBody = false, skillsDir = DEFAULT_SKILLS_DIR, includeSkillMesh = true } = {}) {
149
+ const skills = loadSkillsFromDir({ includeBody, skillsDir, source: skillsDir === DEFAULT_SKILLS_DIR ? "built-in" : "local" });
150
+ if (includeSkillMesh && skillsDir === DEFAULT_SKILLS_DIR) {
151
+ const existing = new Set(skills.map((skill) => skill.id));
152
+ for (const skill of loadEnabledSkillMeshSkills({ includeBody })) {
153
+ if (!existing.has(skill.id)) {
154
+ existing.add(skill.id);
155
+ skills.push(skill);
156
+ }
157
+ }
158
+ }
96
159
  return skills.sort((a, b) => a.id.localeCompare(b.id));
97
160
  }
98
161