@almyty/skills 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -7,7 +7,14 @@ export interface GatewayInfo {
7
7
  id: string;
8
8
  name: string;
9
9
  type: string;
10
+ /** The gateway's address, e.g. /petstore; the install ref resolves by it first. */
11
+ endpoint?: string;
10
12
  }
13
+ /**
14
+ * The gateway segment of an install ref: the address slug, else the name
15
+ * as a clean slug (a-z0-9, single dashes), the two forms the server resolves.
16
+ */
17
+ export declare function gatewayRefSlug(gw: Pick<GatewayInfo, 'name' | 'endpoint'>): string;
11
18
  export interface ParsedRef {
12
19
  type: 'gateway' | 'skill' | 'search' | 'uuid';
13
20
  orgSlug?: string;
package/dist/client.js CHANGED
@@ -1,3 +1,13 @@
1
+ /**
2
+ * The gateway segment of an install ref: the address slug, else the name
3
+ * as a clean slug (a-z0-9, single dashes), the two forms the server resolves.
4
+ */
5
+ export function gatewayRefSlug(gw) {
6
+ const fromEndpoint = (gw.endpoint ?? '').replace(/^\/+/, '');
7
+ if (fromEndpoint)
8
+ return fromEndpoint;
9
+ return (gw.name ?? '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|(?<!-)-+$/g, '');
10
+ }
1
11
  export function parseRef(ref) {
2
12
  if (!ref)
3
13
  return { type: 'search', raw: ref };
@@ -85,12 +95,14 @@ export class AlmytyClient {
85
95
  };
86
96
  }
87
97
  async listGateways() {
88
- const data = await this.request('/gateways');
98
+ // Gateways are MCP, UTCP and Skills; an agent's channels are not listed.
99
+ const data = await this.request('/gateways?kind=tool');
89
100
  const gateways = data?.data?.gateways || data?.data?.data || data?.data || [];
90
101
  return (Array.isArray(gateways) ? gateways : []).map((gw) => ({
91
102
  id: gw.id,
92
103
  name: gw.name,
93
104
  type: gw.type,
105
+ endpoint: gw.endpoint ?? undefined,
94
106
  }));
95
107
  }
96
108
  async searchSkills(query) {
@@ -173,7 +185,7 @@ export class AlmytyClient {
173
185
  }
174
186
  if (trimmed.length > room)
175
187
  trimmed = trimmed.slice(-room);
176
- return `${gw}-${trimmed.replace(/^-+|-+$/g, '')}`;
188
+ return `${gw}-${trimmed.replace(/^-+|(?<!-)-+$/g, '')}`;
177
189
  };
178
190
  const match = allTools.find((gt) => {
179
191
  const t = gt.tool || gt;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { resolveCredentials } from './auth.js';
3
- import { AlmytyClient, parseRef } from './client.js';
3
+ import { AlmytyClient, gatewayRefSlug, parseRef } from './client.js';
4
4
  import { getAllTargets } from './agents.js';
5
5
  import { installSkills, removeSkills, listInstalledSkills } from './installer.js';
6
6
  import { loadConfig, resolveTargets } from './config.js';
@@ -101,7 +101,7 @@ async function main() {
101
101
  }
102
102
  console.log('\nYour gateways:\n');
103
103
  for (const gw of gateways) {
104
- const slug = gw.name.toLowerCase().replace(/\s+/g, '-');
104
+ const slug = gatewayRefSlug(gw);
105
105
  console.log(` ${gw.name}`);
106
106
  console.log(` Type: ${gw.type}`);
107
107
  console.log(` Use: npx @almyty/skills install <org>/${slug}`);
@@ -11,6 +11,18 @@
11
11
  */
12
12
  import type { SkillFile } from './client.js';
13
13
  import type { AgentTarget } from './agents.js';
14
+ /** Marker line we expect inside every SKILL.md frontmatter we wrote.
15
+ * The leading run stops at a line break: as `^\s*` in a multiline regex
16
+ * every line start rescanned all the blank lines after it, and 100 KB of
17
+ * them in a SKILL.md (its content is the gateway's) took seven seconds.
18
+ * A match that crossed blank lines also matches from the last of them. */
19
+ export declare const ALMYTY_MARKER: RegExp;
20
+ /**
21
+ * A SKILL.md is a page of markdown; anything bigger than this is not a
22
+ * skill the backend generated and is refused rather than written into an
23
+ * editor's config.
24
+ */
25
+ export declare const MAX_SKILL_BYTES: number;
14
26
  export interface InstallResult {
15
27
  agent: string;
16
28
  skillsDir: string;
package/dist/installer.js CHANGED
@@ -9,13 +9,17 @@
9
9
  * and `installed` now reads the `metadata.author: almyty` line
10
10
  * already present in every SKILL.md we generate.
11
11
  */
12
- import { writeFileSync, mkdirSync, existsSync, readdirSync, rmSync, readFileSync } from 'fs';
12
+ import { writeFileSync, mkdirSync, existsSync, readdirSync, rmSync, readFileSync, lstatSync } from 'fs';
13
13
  import { join, resolve, sep } from 'path';
14
14
  /** Legacy prefix from <=v1.0.9 — older installs still use this dir
15
15
  * name. Kept here so `remove`/`installed` continue to find them. */
16
16
  const LEGACY_SKILL_PREFIX = 'almyty-';
17
- /** Marker line we expect inside every SKILL.md frontmatter we wrote. */
18
- const ALMYTY_MARKER = /^\s*author:\s*almyty\s*$/m;
17
+ /** Marker line we expect inside every SKILL.md frontmatter we wrote.
18
+ * The leading run stops at a line break: as `^\s*` in a multiline regex
19
+ * every line start rescanned all the blank lines after it, and 100 KB of
20
+ * them in a SKILL.md (its content is the gateway's) took seven seconds.
21
+ * A match that crossed blank lines also matches from the last of them. */
22
+ export const ALMYTY_MARKER = /^[^\S\n\r\u2028\u2029]*author:\s*almyty\s*$/m;
19
23
  /**
20
24
  * Skill names come from the backend/gateway and are used to build
21
25
  * filesystem paths under skillsDir. A malicious or compromised gateway
@@ -40,6 +44,26 @@ function isInside(baseDir, candidate) {
40
44
  const target = resolve(candidate);
41
45
  return target === base || target.startsWith(base + sep);
42
46
  }
47
+ /**
48
+ * A SKILL.md is a page of markdown; anything bigger than this is not a
49
+ * skill the backend generated and is refused rather than written into an
50
+ * editor's config.
51
+ */
52
+ export const MAX_SKILL_BYTES = 1024 * 1024;
53
+ /**
54
+ * True when the path exists and is a symbolic link. The name check and
55
+ * `isInside` work on the path string; a link already sitting at
56
+ * `<skillsDir>/<name>` (or at its SKILL.md) would still send the write
57
+ * somewhere else, so links are refused rather than followed.
58
+ */
59
+ function isSymlink(path) {
60
+ try {
61
+ return lstatSync(path).isSymbolicLink();
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
43
67
  /**
44
68
  * Strip the legacy `name: almyty-<x>` line in the SKILL.md frontmatter
45
69
  * to `name: <x>` so the agent-visible identifier is the skill's own
@@ -100,6 +124,14 @@ export function installSkills(skills, target, options = {}) {
100
124
  console.warn(`Skipping skill whose path escapes the skills directory: ${skill.name}`);
101
125
  continue;
102
126
  }
127
+ if (isSymlink(skillDir) || isSymlink(skillFile)) {
128
+ console.warn(`Skipping skill whose path is a symbolic link: ${skill.name}`);
129
+ continue;
130
+ }
131
+ if (typeof skill.content !== 'string' || Buffer.byteLength(skill.content, 'utf-8') > MAX_SKILL_BYTES) {
132
+ console.warn(`Skipping skill with missing or oversized content: ${skill.name}`);
133
+ continue;
134
+ }
103
135
  if (existsSync(skillFile))
104
136
  overwritten++;
105
137
  if (dryRun) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almyty/skills",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -35,7 +35,7 @@
35
35
  "author": "almyty",
36
36
  "license": "Apache-2.0",
37
37
  "dependencies": {
38
- "@almyty/client": "^1.2.0",
38
+ "@almyty/client": "^1.5.1",
39
39
  "@clack/prompts": "^1.2.0"
40
40
  },
41
41
  "devDependencies": {