chore: bump version to 2.80.27 and update core features

This commit is contained in:
g1nation
2026-05-09 01:16:12 +09:00
parent 5ffb472d22
commit 3220a126fd
41 changed files with 4457 additions and 72 deletions
+145
View File
@@ -0,0 +1,145 @@
import * as fs from 'fs';
import * as path from 'path';
import { isInside } from '../lib/paths';
import { logError, logInfo } from '../utils';
/**
* External-tool skill injection.
*
* Connect_origin's `/api/skill-inject` writes Python tool scripts into a
* per-agent `tools/` folder, on the assumption the agent can `<run_command>`
* them. ConnectAI doesn't run Python tools — its agent skills are markdown
* documents loaded by the sidebar (`<workspace>/.agent/skills/<name>.md`).
*
* So this service injects **markdown skills** (the ConnectAI primitive), not
* .py scripts. The endpoint shape stays similar (name / displayName /
* description / content / source) so the same external integrations
* (EZER / Agent University) can target either project with a thin adapter.
*
* What lands on disk per inject call:
* .agent/skills/<safeName>.md — markdown body
* .agent/skills/<safeName>.meta.json — injectedAt + injectedFrom + display fields
*
* The `.md` is what the existing sidebar `_sendAgentsList` already discovers.
* The sidecar `.meta.json` is read by future UI surfaces (provenance badges)
* but is invisible to the legacy loader, so back-compat is preserved.
*/
export interface SkillInjectionRequest {
/** Required. Slug-style identifier; will be sanitized to filesystem-safe form. */
name: string;
/** Required. The markdown body of the skill (system-prompt content). */
content: string;
/** Optional. Human-friendly display name. Falls back to sanitized `name`. */
displayName?: string;
/** Optional. One-line description shown in UI hints. */
description?: string;
/** Optional. External-source tag, e.g. `"ezer"` / `"agent-university"`. */
source?: string;
}
export interface SkillInjectionResult {
/** Sanitized name actually used on disk. */
safeName: string;
/** Absolute path to the written markdown file. */
filePath: string;
/** Absolute path to the sidecar metadata file. */
metaPath: string;
}
export class SkillInjectionError extends Error {
constructor(message: string, public readonly code: string) {
super(message);
this.name = 'SkillInjectionError';
}
}
export interface ISkillInjectionService {
inject(req: SkillInjectionRequest): Promise<SkillInjectionResult>;
}
export interface SkillInjectionDeps {
/** Resolves the skills directory at the time of each call (must be absolute). */
resolveSkillsDir: () => string;
/** Optional fs override for unit tests; defaults to node:fs. */
fsImpl?: Pick<typeof fs, 'existsSync' | 'mkdirSync' | 'writeFileSync'>;
/** Optional notification hook called on successful injection. */
onInjected?: (result: SkillInjectionResult, req: SkillInjectionRequest) => void;
}
/** Conservative skill-name sanitizer. Rejects empty / overlong / shell-unfriendly inputs. */
export function sanitizeSkillName(raw: string): string {
if (typeof raw !== 'string') return '';
const trimmed = raw.trim();
if (!trimmed) return '';
// Strip extensions if caller passed "foo.md" by mistake
const noExt = trimmed.replace(/\.(md|markdown)$/i, '');
// Allow only ASCII alphanumerics, `_`, `-`, `.` — block path separators,
// shell metas, and unicode that could confuse filesystems.
const safe = noExt.replace(/[^a-zA-Z0-9_.\-]/g, '_').replace(/^[._]+|[._]+$/g, '');
return safe.slice(0, 80);
}
export class FileSystemSkillInjectionService implements ISkillInjectionService {
private readonly _fs: NonNullable<SkillInjectionDeps['fsImpl']>;
constructor(private readonly deps: SkillInjectionDeps) {
this._fs = deps.fsImpl ?? fs;
}
async inject(req: SkillInjectionRequest): Promise<SkillInjectionResult> {
const safeName = sanitizeSkillName(req.name);
if (!safeName) {
throw new SkillInjectionError(
'Skill name is empty after sanitization. Use ASCII letters, digits, "_", "-", or "." only.',
'INVALID_NAME'
);
}
if (typeof req.content !== 'string' || !req.content.trim()) {
throw new SkillInjectionError('Skill content must be a non-empty markdown string.', 'EMPTY_CONTENT');
}
const skillsDir = this.deps.resolveSkillsDir();
if (!skillsDir) {
throw new SkillInjectionError(
'Agent skills directory is not available. Open a workspace folder first.',
'NO_SKILLS_DIR'
);
}
const targetMd = path.join(skillsDir, `${safeName}.md`);
const targetMeta = path.join(skillsDir, `${safeName}.meta.json`);
if (!isInside(skillsDir, targetMd) || !isInside(skillsDir, targetMeta)) {
// Defense in depth: sanitizer should already block traversal, but the
// path math runs again here in case skillsDir resolves to something
// surprising (symlink, weird casing on Windows, etc.).
throw new SkillInjectionError('Refusing to write outside the skills directory.', 'PATH_ESCAPE');
}
try {
if (!this._fs.existsSync(skillsDir)) {
this._fs.mkdirSync(skillsDir, { recursive: true });
}
this._fs.writeFileSync(targetMd, req.content, 'utf8');
const meta = {
name: safeName,
displayName: (req.displayName || '').trim() || safeName,
description: (req.description || '').trim() || '',
injectedAt: new Date().toISOString(),
injectedFrom: (req.source || '').trim() || 'external',
};
this._fs.writeFileSync(targetMeta, JSON.stringify(meta, null, 2), 'utf8');
} catch (e: any) {
const msg = e?.message ?? String(e);
logError('Skill injection write failed.', { safeName, skillsDir, error: msg });
throw new SkillInjectionError(`Failed to write skill files: ${msg}`, 'WRITE_FAILED');
}
const result: SkillInjectionResult = { safeName, filePath: targetMd, metaPath: targetMeta };
logInfo('Skill injected.', { safeName, source: req.source, skillsDir });
this.deps.onInjected?.(result, req);
return result;
}
}