vault-go 0.29.1 → 0.30.0
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/capabilities.d.ts +10 -0
- package/dist/capabilities.js +1 -0
- package/dist/cloud.d.ts +22 -0
- package/dist/cloud.js +33 -0
- package/dist/crystal-memory-mirror.js +9 -1
- package/dist/hooks.js +10 -0
- package/dist/server.js +2 -0
- package/dist/skill-context.d.ts +4 -0
- package/dist/skill-context.js +16 -0
- package/dist/skill-local.d.ts +46 -0
- package/dist/skill-local.js +214 -0
- package/dist/skill-tools.d.ts +52 -0
- package/dist/skill-tools.js +369 -0
- package/package.json +7 -1
package/dist/capabilities.d.ts
CHANGED
|
@@ -112,6 +112,11 @@ export declare const CAPABILITIES: {
|
|
|
112
112
|
readonly status: "available";
|
|
113
113
|
readonly tools: readonly ["vault_go_modes", "vault_go_mode"];
|
|
114
114
|
readonly difference: "Setup writes managed SKILL.md files for Claude, Grok and shared agent skill directories. mem-search uses vault_go_search_index → vault_go_timeline → vault_go_observations → vault_go_tool_uses. babysit is a session loop with gh, not a Vault background daemon.";
|
|
115
|
+
}, {
|
|
116
|
+
readonly upstream: readonly ["private skills library"];
|
|
117
|
+
readonly status: "available";
|
|
118
|
+
readonly tools: readonly ["vault_go_skills", "vault_go_skill_folders", "vault_go_skill_read", "vault_go_skill_file", "vault_go_skill_save", "vault_go_skill_organize", "vault_go_skill_install", "vault_go_skill_sync"];
|
|
119
|
+
readonly requires: "Account skill packages organized by folders and linked to projects (/v1/skills). Saving publishes a new immutable version; deleting is only available in the Vault apps. Local install verifies sha256 and manifest digest and never replaces a skill the Vault did not install.";
|
|
115
120
|
}];
|
|
116
121
|
};
|
|
117
122
|
/** API tools verify by default. Resources pass verify=false and never perform network I/O. */
|
|
@@ -228,6 +233,11 @@ export declare function getCapabilities(home: string, cloud: VaultMemoryApi, ver
|
|
|
228
233
|
readonly status: "available";
|
|
229
234
|
readonly tools: readonly ["vault_go_modes", "vault_go_mode"];
|
|
230
235
|
readonly difference: "Setup writes managed SKILL.md files for Claude, Grok and shared agent skill directories. mem-search uses vault_go_search_index → vault_go_timeline → vault_go_observations → vault_go_tool_uses. babysit is a session loop with gh, not a Vault background daemon.";
|
|
236
|
+
}, {
|
|
237
|
+
readonly upstream: readonly ["private skills library"];
|
|
238
|
+
readonly status: "available";
|
|
239
|
+
readonly tools: readonly ["vault_go_skills", "vault_go_skill_folders", "vault_go_skill_read", "vault_go_skill_file", "vault_go_skill_save", "vault_go_skill_organize", "vault_go_skill_install", "vault_go_skill_sync"];
|
|
240
|
+
readonly requires: "Account skill packages organized by folders and linked to projects (/v1/skills). Saving publishes a new immutable version; deleting is only available in the Vault apps. Local install verifies sha256 and manifest digest and never replaces a skill the Vault did not install.";
|
|
231
241
|
}];
|
|
232
242
|
}>;
|
|
233
243
|
export declare const WORKFLOW: {
|
package/dist/capabilities.js
CHANGED
|
@@ -29,6 +29,7 @@ export const CAPABILITIES = {
|
|
|
29
29
|
{ upstream: ['transcript watchers'], status: 'available', tools: ['vault_go_transcripts_status', 'vault_go_transcripts_configure', 'vault_go_transcripts_poll'], difference: 'Persistent, consented and resumable watchers are implemented for Claude and Codex transcripts. Other client transcript formats are not claimed.' },
|
|
30
30
|
{ upstream: ['Grok awareness push'], status: 'available', tools: ['vault_go_grok_awareness_status', 'vault_go_grok_awareness_configure', 'vault_go_grok_awareness_push'], requires: 'Explicit consent, an allowlisted agent UUID and a local agent-data root. Monthly awareness logs are bounded, private and separate from Telegram.' },
|
|
31
31
|
{ upstream: ['development/report/presentation skills', 'babysit', 'mem-search'], status: 'available', tools: ['vault_go_modes', 'vault_go_mode'], difference: 'Setup writes managed SKILL.md files for Claude, Grok and shared agent skill directories. mem-search uses vault_go_search_index → vault_go_timeline → vault_go_observations → vault_go_tool_uses. babysit is a session loop with gh, not a Vault background daemon.' },
|
|
32
|
+
{ upstream: ['private skills library'], status: 'available', tools: ['vault_go_skills', 'vault_go_skill_folders', 'vault_go_skill_read', 'vault_go_skill_file', 'vault_go_skill_save', 'vault_go_skill_organize', 'vault_go_skill_install', 'vault_go_skill_sync'], requires: 'Account skill packages organized by folders and linked to projects (/v1/skills). Saving publishes a new immutable version; deleting is only available in the Vault apps. Local install verifies sha256 and manifest digest and never replaces a skill the Vault did not install.' },
|
|
32
33
|
],
|
|
33
34
|
};
|
|
34
35
|
function unavailableVerification() {
|
package/dist/cloud.d.ts
CHANGED
|
@@ -52,6 +52,17 @@ export interface VaultMemoryApi {
|
|
|
52
52
|
rebuildKnowledgeBase(id: string): Promise<unknown>;
|
|
53
53
|
queryKnowledgeBase(id: string, input: Record<string, unknown>): Promise<unknown>;
|
|
54
54
|
deleteKnowledgeBase(id: string): Promise<unknown>;
|
|
55
|
+
skills?(query: Record<string, unknown>): Promise<unknown>;
|
|
56
|
+
skillFolders?(): Promise<unknown>;
|
|
57
|
+
skill?(id: string, version?: string): Promise<unknown>;
|
|
58
|
+
skillFile?(id: string, path: string, version?: string): Promise<unknown>;
|
|
59
|
+
createSkill?(input: Record<string, unknown>, idempotencyKey?: string): Promise<unknown>;
|
|
60
|
+
createSkillVersion?(id: string, input: Record<string, unknown>, idempotencyKey?: string): Promise<unknown>;
|
|
61
|
+
updateSkill?(id: string, input: Record<string, unknown>): Promise<unknown>;
|
|
62
|
+
setSkillProjects?(id: string, projectIds: string[]): Promise<unknown>;
|
|
63
|
+
createSkillFolder?(input: Record<string, unknown>): Promise<unknown>;
|
|
64
|
+
updateSkillFolder?(id: string, input: Record<string, unknown>): Promise<unknown>;
|
|
65
|
+
skillDownloadGrant?(id: string, versionId?: string): Promise<unknown>;
|
|
55
66
|
}
|
|
56
67
|
export declare class VaultCloudClient implements VaultMemoryApi {
|
|
57
68
|
private readonly home;
|
|
@@ -101,6 +112,17 @@ export declare class VaultCloudClient implements VaultMemoryApi {
|
|
|
101
112
|
rebuildKnowledgeBase(id: string): Promise<unknown>;
|
|
102
113
|
queryKnowledgeBase(id: string, input: Record<string, unknown>): Promise<unknown>;
|
|
103
114
|
deleteKnowledgeBase(id: string): Promise<unknown>;
|
|
115
|
+
skills(query: Record<string, unknown>): Promise<unknown>;
|
|
116
|
+
skillFolders(): Promise<unknown>;
|
|
117
|
+
skill(id: string, version?: string): Promise<unknown>;
|
|
118
|
+
skillFile(id: string, path: string, version?: string): Promise<unknown>;
|
|
119
|
+
createSkill(input: Record<string, unknown>, idempotencyKey?: string): Promise<unknown>;
|
|
120
|
+
createSkillVersion(id: string, input: Record<string, unknown>, idempotencyKey?: string): Promise<unknown>;
|
|
121
|
+
updateSkill(id: string, input: Record<string, unknown>): Promise<unknown>;
|
|
122
|
+
setSkillProjects(id: string, projectIds: string[]): Promise<unknown>;
|
|
123
|
+
createSkillFolder(input: Record<string, unknown>): Promise<unknown>;
|
|
124
|
+
updateSkillFolder(id: string, input: Record<string, unknown>): Promise<unknown>;
|
|
125
|
+
skillDownloadGrant(id: string, versionId?: string): Promise<unknown>;
|
|
104
126
|
updatePreferences(input: Record<string, unknown>): Promise<unknown>;
|
|
105
127
|
memory(id: string): Promise<unknown>;
|
|
106
128
|
toolUses(input: Record<string, unknown>): Promise<unknown>;
|
package/dist/cloud.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { recordActivity } from './activity.js';
|
|
2
2
|
import { loadConfig, loadTokens, saveTokens, validateApiUrl, vaultHome, } from './config.js';
|
|
3
3
|
const KNOWLEDGE_BASE_RESPONSE_MAX_BYTES = 8 * 1024 * 1024;
|
|
4
|
+
const SKILL_RESPONSE_MAX_BYTES = 8 * 1024 * 1024;
|
|
4
5
|
const KNOWLEDGE_BASE_RESPONSE_LIMIT_ERROR = 'A base de conhecimento excede o limite de resposta de 8 MiB.';
|
|
5
6
|
const observationTypes = new Set(['decision', 'discovery', 'fix', 'feature', 'refactor', 'other']);
|
|
6
7
|
const retentionClasses = new Set(['transient', 'supporting', 'durable', 'critical']);
|
|
@@ -204,6 +205,36 @@ export class VaultCloudClient {
|
|
|
204
205
|
method: 'DELETE',
|
|
205
206
|
});
|
|
206
207
|
}
|
|
208
|
+
async skills(query) { return this.request('/v1/skills', { query }); }
|
|
209
|
+
async skillFolders() { return this.request('/v1/skills/folders'); }
|
|
210
|
+
async skill(id, version) {
|
|
211
|
+
return this.request(`/v1/skills/${encodeURIComponent(id)}`, { query: { version } });
|
|
212
|
+
}
|
|
213
|
+
async skillFile(id, path, version) {
|
|
214
|
+
const encoded = path.split('/').map(encodeURIComponent).join('/');
|
|
215
|
+
return this.request(`/v1/skills/${encodeURIComponent(id)}/files/${encoded}`, { query: { version }, responseMaxBytes: SKILL_RESPONSE_MAX_BYTES });
|
|
216
|
+
}
|
|
217
|
+
async createSkill(input, idempotencyKey) {
|
|
218
|
+
return this.request('/v1/skills', { method: 'POST', body: input, timeoutMs: 60_000, ...(idempotencyKey ? { headers: { 'idempotency-key': idempotencyKey } } : {}) });
|
|
219
|
+
}
|
|
220
|
+
async createSkillVersion(id, input, idempotencyKey) {
|
|
221
|
+
return this.request(`/v1/skills/${encodeURIComponent(id)}/versions`, { method: 'POST', body: input, timeoutMs: 60_000, ...(idempotencyKey ? { headers: { 'idempotency-key': idempotencyKey } } : {}) });
|
|
222
|
+
}
|
|
223
|
+
async updateSkill(id, input) {
|
|
224
|
+
return this.request(`/v1/skills/${encodeURIComponent(id)}`, { method: 'PATCH', body: input });
|
|
225
|
+
}
|
|
226
|
+
async setSkillProjects(id, projectIds) {
|
|
227
|
+
return this.request(`/v1/skills/${encodeURIComponent(id)}/projects`, { method: 'PUT', body: { projectIds } });
|
|
228
|
+
}
|
|
229
|
+
async createSkillFolder(input) {
|
|
230
|
+
return this.request('/v1/skills/folders', { method: 'POST', body: input });
|
|
231
|
+
}
|
|
232
|
+
async updateSkillFolder(id, input) {
|
|
233
|
+
return this.request(`/v1/skills/folders/${encodeURIComponent(id)}`, { method: 'PATCH', body: input });
|
|
234
|
+
}
|
|
235
|
+
async skillDownloadGrant(id, versionId) {
|
|
236
|
+
return this.request(`/v1/skills/${encodeURIComponent(id)}/download-grants`, { method: 'POST', body: versionId ? { versionId } : {} });
|
|
237
|
+
}
|
|
207
238
|
async updatePreferences(input) {
|
|
208
239
|
return this.request('/preferences', { method: 'PATCH', body: input });
|
|
209
240
|
}
|
|
@@ -286,6 +317,8 @@ export class VaultCloudClient {
|
|
|
286
317
|
const headers = new Headers({ accept: 'application/json' });
|
|
287
318
|
if (options.body)
|
|
288
319
|
headers.set('content-type', 'application/json');
|
|
320
|
+
for (const [name, value] of Object.entries(options.headers ?? {}))
|
|
321
|
+
headers.set(name, value);
|
|
289
322
|
if (options.authenticated !== false) {
|
|
290
323
|
headers.set('authorization', `Bearer ${await this.accessToken(options.retried)}`);
|
|
291
324
|
}
|
|
@@ -23,7 +23,15 @@ export async function resolveCrystalProjectFolder(cloud, projectId) {
|
|
|
23
23
|
if (!Array.isArray(projects))
|
|
24
24
|
return null;
|
|
25
25
|
const found = projects.find((item) => item && typeof item === 'object' && item.id === projectId);
|
|
26
|
-
|
|
26
|
+
const folder = crystalProjectFolder(found);
|
|
27
|
+
if (!folder)
|
|
28
|
+
return null;
|
|
29
|
+
// Why: same-named projects used to share one Crystal folder and mix their
|
|
30
|
+
// memories. The oldest keeps the plain name (existing folders stay put); newer
|
|
31
|
+
// namesakes get a stable suffix from their id. Projects arrive newest first.
|
|
32
|
+
const namesakes = projects.filter((item) => crystalProjectFolder(item)?.toLowerCase() === folder.toLowerCase());
|
|
33
|
+
const oldest = namesakes.at(-1);
|
|
34
|
+
return oldest?.id === projectId ? folder : `${folder}-${projectId.slice(0, 8)}`;
|
|
27
35
|
}
|
|
28
36
|
export async function mirrorCrystalMemory(input) {
|
|
29
37
|
if (!shouldMirrorCrystalMemory(input.job))
|
package/dist/hooks.js
CHANGED
|
@@ -10,6 +10,7 @@ import { vaultHome } from "./config.js";
|
|
|
10
10
|
import { enqueueGeneration } from "./hook-queue.js";
|
|
11
11
|
import { transcriptCaptureEnabled } from './transcript-config.js';
|
|
12
12
|
import { loadProjectContext } from './context-chain.js';
|
|
13
|
+
import { skillContextSection } from './skill-context.js';
|
|
13
14
|
export const HOOK_EVENTS = [
|
|
14
15
|
"context",
|
|
15
16
|
"session-init",
|
|
@@ -246,6 +247,15 @@ export async function executeHook(adapter, event, value, cloud = new VaultCloudC
|
|
|
246
247
|
})
|
|
247
248
|
: { context: await loadProjectContext(home, cloud, { workspace, primary: { id: project.id }, projects, ...limits }) });
|
|
248
249
|
let context = string(result.context).slice(0, limits.maxChars);
|
|
250
|
+
if (event === "context") {
|
|
251
|
+
// Skills do projeto entram depois da memória e só no espaço que sobrar; falha aqui nunca derruba o contexto.
|
|
252
|
+
try {
|
|
253
|
+
const skills = await skillContextSection(cloud, project.id);
|
|
254
|
+
if (skills && context.length + skills.length + 2 <= limits.maxChars)
|
|
255
|
+
context = context ? `${context}\n\n${skills}` : skills;
|
|
256
|
+
}
|
|
257
|
+
catch { /* biblioteca de skills indisponível */ }
|
|
258
|
+
}
|
|
249
259
|
try {
|
|
250
260
|
context = applyCustomModeContext(home, context, limits.maxChars);
|
|
251
261
|
}
|
package/dist/server.js
CHANGED
|
@@ -8,6 +8,7 @@ import { registerVaultModes } from './modes.js';
|
|
|
8
8
|
import { registerCrystalTools } from './crystal-tools.js';
|
|
9
9
|
import { registerModeIntegrations } from './mode-integrations.js';
|
|
10
10
|
import { registerKnowledgeAgentTools } from './knowledge-tools.js';
|
|
11
|
+
import { registerSkillTools } from './skill-tools.js';
|
|
11
12
|
import { redact } from './hooks.js';
|
|
12
13
|
import { createProjectScope } from './project-scope.js';
|
|
13
14
|
import { VaultCloudClient } from './cloud.js';
|
|
@@ -55,6 +56,7 @@ export function createVaultGoServer(home = vaultHome(), cloud = new VaultCloudCl
|
|
|
55
56
|
registerVaultModes(server);
|
|
56
57
|
registerCrystalTools(server, home);
|
|
57
58
|
registerKnowledgeAgentTools(server, home, cloud);
|
|
59
|
+
registerSkillTools(server, home, cloud);
|
|
58
60
|
server.registerTool('vault_go_engines', {
|
|
59
61
|
title: 'Listar motores de contexto',
|
|
60
62
|
description: 'Lista os motores disponíveis para preparar contexto: Vault AI, Claude, OpenRouter, Gemini e ChatGPT/Codex. Não revela chaves.',
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { VaultMemoryApi } from './cloud.js';
|
|
2
|
+
export declare const clip: (text: string, max?: number) => string;
|
|
3
|
+
/** Seção curta para o contexto de início de sessão; vazia quando não há o que mostrar. */
|
|
4
|
+
export declare function skillContextSection(cloud: VaultMemoryApi, projectId: string, max?: number): Promise<string>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const clip = (text, max = 200) => (text.length > max ? `${text.slice(0, max - 1)}…` : text);
|
|
2
|
+
/** Seção curta para o contexto de início de sessão; vazia quando não há o que mostrar. */
|
|
3
|
+
export async function skillContextSection(cloud, projectId, max = 20) {
|
|
4
|
+
if (!cloud.skills)
|
|
5
|
+
return '';
|
|
6
|
+
const raw = await cloud.skills({ projectId, limit: max });
|
|
7
|
+
const result = raw && typeof raw === 'object' ? raw : {};
|
|
8
|
+
const skills = Array.isArray(result.skills) ? result.skills : [];
|
|
9
|
+
if (!skills.length)
|
|
10
|
+
return '';
|
|
11
|
+
return [
|
|
12
|
+
'## Skills privadas do Vault',
|
|
13
|
+
...skills.map(s => `- ${s.name}${s.folderPath ? ` (${s.folderPath})` : ''} — ${clip(s.description, 160)}`),
|
|
14
|
+
'Use `vault_go_skill_read skill="<nome>"` para carregar uma skill quando ela se aplicar à tarefa.',
|
|
15
|
+
].join('\n');
|
|
16
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export declare const SKILL_MARKER = ".vault-skill.json";
|
|
2
|
+
export declare const SKILL_FILE_MAX_BYTES: number;
|
|
3
|
+
export declare const SKILL_MAX_FILES = 512;
|
|
4
|
+
export declare const SKILL_TOTAL_MAX_BYTES: number;
|
|
5
|
+
export declare class SkillLocalError extends Error {
|
|
6
|
+
code: string;
|
|
7
|
+
constructor(code: string, message: string);
|
|
8
|
+
}
|
|
9
|
+
export interface LocalSkillFile {
|
|
10
|
+
path: string;
|
|
11
|
+
content: string;
|
|
12
|
+
encoding: 'utf8' | 'base64';
|
|
13
|
+
executable: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface TarEntry {
|
|
16
|
+
path: string;
|
|
17
|
+
bytes: Buffer;
|
|
18
|
+
mode: number;
|
|
19
|
+
}
|
|
20
|
+
export interface InstalledSkill {
|
|
21
|
+
root: string;
|
|
22
|
+
dir: string;
|
|
23
|
+
skillId: string;
|
|
24
|
+
versionId: string;
|
|
25
|
+
}
|
|
26
|
+
/** name/description do frontmatter; valida só o formato mínimo (o servidor valida o resto). */
|
|
27
|
+
export declare function frontmatter(md: string): {
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
} | null;
|
|
31
|
+
/** Lê uma pasta de skill para publicar; symlinks e pastas de ferramentas ficam de fora. */
|
|
32
|
+
export declare function readSourceDir(directory: string): LocalSkillFile[];
|
|
33
|
+
/** Leitor ustar mínimo: só arquivos regulares; link, dispositivo ou caminho perigoso recusa o pacote inteiro. */
|
|
34
|
+
export declare function readTarGz(archive: Uint8Array): TarEntry[];
|
|
35
|
+
export declare function installSkillArchive(input: {
|
|
36
|
+
bytes: Uint8Array;
|
|
37
|
+
expectedSha256: string;
|
|
38
|
+
dirName: string;
|
|
39
|
+
root: string;
|
|
40
|
+
skillId: string;
|
|
41
|
+
versionId: string;
|
|
42
|
+
}): {
|
|
43
|
+
path: string;
|
|
44
|
+
};
|
|
45
|
+
/** Skills instaladas pelo Vault nas raízes informadas. */
|
|
46
|
+
export declare function installedSkills(roots: string[]): InstalledSkill[];
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Lado local da biblioteca de skills: ler uma pasta para publicar e instalar um
|
|
2
|
+
// pacote baixado. A instalação confere sha256, cada arquivo e o digest do
|
|
3
|
+
// manifesto, grava tudo numa pasta temporária irmã e só então troca de lugar.
|
|
4
|
+
// Uma pasta que o Vault não instalou (sem .vault-skill.json) nunca é tocada.
|
|
5
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
6
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
8
|
+
import { gunzipSync } from 'node:zlib';
|
|
9
|
+
export const SKILL_MARKER = '.vault-skill.json';
|
|
10
|
+
export const SKILL_FILE_MAX_BYTES = 4 * 1024 * 1024;
|
|
11
|
+
export const SKILL_MAX_FILES = 512;
|
|
12
|
+
export const SKILL_TOTAL_MAX_BYTES = 32 * 1024 * 1024;
|
|
13
|
+
const EXPANDED_MAX_BYTES = 40 * 1024 * 1024;
|
|
14
|
+
const IGNORED = new Set(['.git', 'node_modules', '.DS_Store', SKILL_MARKER]);
|
|
15
|
+
const NAME = /^[a-z0-9][a-z0-9.-]{0,63}$/;
|
|
16
|
+
export class SkillLocalError extends Error {
|
|
17
|
+
code;
|
|
18
|
+
constructor(code, message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.code = code;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
24
|
+
/** name/description do frontmatter; valida só o formato mínimo (o servidor valida o resto). */
|
|
25
|
+
export function frontmatter(md) {
|
|
26
|
+
const block = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(md)?.[1];
|
|
27
|
+
if (!block)
|
|
28
|
+
return null;
|
|
29
|
+
const field = (key) => {
|
|
30
|
+
const raw = new RegExp(`^${key}:[ \\t]*(.+)$`, 'm').exec(block)?.[1]?.trim();
|
|
31
|
+
return raw ? raw.replace(/^(['"])(.*)\1$/, '$2').trim() : '';
|
|
32
|
+
};
|
|
33
|
+
const name = field('name'), description = field('description');
|
|
34
|
+
return name && description ? { name, description } : null;
|
|
35
|
+
}
|
|
36
|
+
function isText(bytes) {
|
|
37
|
+
if (bytes.includes(0))
|
|
38
|
+
return false;
|
|
39
|
+
try {
|
|
40
|
+
new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Lê uma pasta de skill para publicar; symlinks e pastas de ferramentas ficam de fora. */
|
|
48
|
+
export function readSourceDir(directory) {
|
|
49
|
+
const base = resolve(directory);
|
|
50
|
+
if (!lstatSync(base).isDirectory())
|
|
51
|
+
throw new SkillLocalError('source_invalid', `Não é uma pasta: ${directory}`);
|
|
52
|
+
const files = [];
|
|
53
|
+
let total = 0;
|
|
54
|
+
const walk = (dir) => {
|
|
55
|
+
for (const name of readdirSync(dir).sort()) {
|
|
56
|
+
if (IGNORED.has(name))
|
|
57
|
+
continue;
|
|
58
|
+
const full = join(dir, name);
|
|
59
|
+
const stat = lstatSync(full);
|
|
60
|
+
if (stat.isSymbolicLink())
|
|
61
|
+
continue;
|
|
62
|
+
if (stat.isDirectory()) {
|
|
63
|
+
walk(full);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!stat.isFile())
|
|
67
|
+
continue;
|
|
68
|
+
if (files.length >= SKILL_MAX_FILES)
|
|
69
|
+
throw new SkillLocalError('payload_too_large', `A skill passa de ${SKILL_MAX_FILES} arquivos`);
|
|
70
|
+
if (stat.size > SKILL_FILE_MAX_BYTES)
|
|
71
|
+
throw new SkillLocalError('payload_too_large', `Arquivo acima de 4 MiB: ${relative(base, full)}`);
|
|
72
|
+
total += stat.size;
|
|
73
|
+
if (total > SKILL_TOTAL_MAX_BYTES)
|
|
74
|
+
throw new SkillLocalError('payload_too_large', 'Skill acima de 32 MiB');
|
|
75
|
+
const bytes = readFileSync(full);
|
|
76
|
+
const text = isText(bytes);
|
|
77
|
+
files.push({ path: relative(base, full).split(sep).join('/'), content: text ? bytes.toString('utf8') : bytes.toString('base64'), encoding: text ? 'utf8' : 'base64', executable: (stat.mode & 0o111) !== 0 });
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
walk(base);
|
|
81
|
+
return files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
82
|
+
}
|
|
83
|
+
function safePath(path) {
|
|
84
|
+
if (!path || path.startsWith('/') || path.includes('\\') || path.includes('\0') || /^[A-Za-z]:/.test(path))
|
|
85
|
+
throw new SkillLocalError('skill_package_invalid', `Caminho inválido no pacote: ${path}`);
|
|
86
|
+
const segments = path.split('/');
|
|
87
|
+
if (segments.some(s => s === '' || s === '.' || s === '..'))
|
|
88
|
+
throw new SkillLocalError('skill_package_invalid', `Caminho inválido no pacote: ${path}`);
|
|
89
|
+
return path;
|
|
90
|
+
}
|
|
91
|
+
/** Leitor ustar mínimo: só arquivos regulares; link, dispositivo ou caminho perigoso recusa o pacote inteiro. */
|
|
92
|
+
export function readTarGz(archive) {
|
|
93
|
+
let tar;
|
|
94
|
+
try {
|
|
95
|
+
tar = gunzipSync(archive, { maxOutputLength: EXPANDED_MAX_BYTES });
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
throw new SkillLocalError('skill_package_invalid', 'Pacote corrompido');
|
|
99
|
+
}
|
|
100
|
+
const field = (block, offset, length) => { const raw = block.subarray(offset, offset + length); const end = raw.indexOf(0); return (end === -1 ? raw : raw.subarray(0, end)).toString('utf8'); };
|
|
101
|
+
const entries = [];
|
|
102
|
+
let offset = 0;
|
|
103
|
+
while (offset + 512 <= tar.length) {
|
|
104
|
+
const header = tar.subarray(offset, offset + 512);
|
|
105
|
+
if (header.every(b => b === 0))
|
|
106
|
+
break;
|
|
107
|
+
const type = String.fromCharCode(header[156]);
|
|
108
|
+
if (type !== '0' && type !== '\0')
|
|
109
|
+
throw new SkillLocalError('skill_package_invalid', 'Pacote contém entrada que não é arquivo regular');
|
|
110
|
+
const name = field(header, 0, 100), prefix = field(header, 345, 155);
|
|
111
|
+
const size = parseInt(field(header, 124, 12).trim() || '0', 8), mode = parseInt(field(header, 100, 8).trim() || '0', 8);
|
|
112
|
+
if (!Number.isSafeInteger(size) || size < 0 || offset + 512 + size > tar.length)
|
|
113
|
+
throw new SkillLocalError('skill_package_invalid', 'Pacote corrompido');
|
|
114
|
+
if (entries.length > SKILL_MAX_FILES + 2)
|
|
115
|
+
throw new SkillLocalError('skill_package_invalid', 'Pacote com entradas demais');
|
|
116
|
+
entries.push({ path: safePath(prefix ? `${prefix}/${name}` : name), bytes: Buffer.from(tar.subarray(offset + 512, offset + 512 + size)), mode });
|
|
117
|
+
offset += 512 + Math.ceil(size / 512) * 512;
|
|
118
|
+
}
|
|
119
|
+
return entries;
|
|
120
|
+
}
|
|
121
|
+
/** Confere o manifest.json do formato do Vault contra os bytes: nada fora dele, nada faltando, digest correto. */
|
|
122
|
+
function verifiedFiles(entries) {
|
|
123
|
+
const integrity = (detail) => new SkillLocalError('skill_install_integrity', `Falha de integridade do pacote: ${detail}`);
|
|
124
|
+
const byPath = new Map(entries.map(e => [e.path, e]));
|
|
125
|
+
if (byPath.has('dev.orca.skill-sharing/manifest.json'))
|
|
126
|
+
throw new SkillLocalError('skill_install_unsupported', 'Pacotes com várias skills (bundle) devem ser instalados pelo Vault Desktop');
|
|
127
|
+
const raw = byPath.get('manifest.json');
|
|
128
|
+
if (!raw)
|
|
129
|
+
throw integrity('manifest.json ausente');
|
|
130
|
+
let manifest;
|
|
131
|
+
try {
|
|
132
|
+
manifest = JSON.parse(raw.bytes.toString('utf8'));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
throw integrity('manifest.json inválido');
|
|
136
|
+
}
|
|
137
|
+
const files = Array.isArray(manifest.files) ? manifest.files : [];
|
|
138
|
+
if (!files.some(f => f.path === 'SKILL.md'))
|
|
139
|
+
throw integrity('SKILL.md ausente');
|
|
140
|
+
const digest = createHash('sha256').update(JSON.stringify(files.map(f => ({ path: f.path, executable: f.executable, classification: f.classification, identitySha256: f.identitySha256 })))).digest('hex');
|
|
141
|
+
if (digest !== manifest.packageDigest)
|
|
142
|
+
throw integrity('digest do manifesto');
|
|
143
|
+
if (entries.length !== files.length + 1)
|
|
144
|
+
throw integrity('entradas fora do manifesto');
|
|
145
|
+
return files.map(file => {
|
|
146
|
+
const entry = byPath.get(safePath(file.path));
|
|
147
|
+
if (!entry || entry.bytes.length !== file.size || sha256(entry.bytes) !== file.sha256)
|
|
148
|
+
throw integrity(file.path);
|
|
149
|
+
return { file, entry };
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
export function installSkillArchive(input) {
|
|
153
|
+
if (!NAME.test(input.dirName) || input.dirName.includes('..'))
|
|
154
|
+
throw new SkillLocalError('skill_install_invalid', `Nome de pasta inválido: ${input.dirName}`);
|
|
155
|
+
if (sha256(input.bytes) !== input.expectedSha256)
|
|
156
|
+
throw new SkillLocalError('skill_install_integrity', 'Falha de integridade do pacote: sha256');
|
|
157
|
+
const files = verifiedFiles(readTarGz(input.bytes));
|
|
158
|
+
const root = resolve(input.root);
|
|
159
|
+
const target = join(root, input.dirName);
|
|
160
|
+
if (existsSync(target) && !existsSync(join(target, SKILL_MARKER))) {
|
|
161
|
+
throw new SkillLocalError('skill_install_conflict', `${target} já existe e não foi instalada pelo Vault; nada foi alterado`);
|
|
162
|
+
}
|
|
163
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
164
|
+
const token = `${process.pid}-${randomBytes(4).toString('hex')}`;
|
|
165
|
+
const staging = join(root, `.${input.dirName}.vault-tmp-${token}`);
|
|
166
|
+
const previous = join(root, `.${input.dirName}.vault-old-${token}`);
|
|
167
|
+
try {
|
|
168
|
+
for (const { file, entry } of files) {
|
|
169
|
+
const destination = resolve(staging, file.path);
|
|
170
|
+
if (!destination.startsWith(staging + sep))
|
|
171
|
+
throw new SkillLocalError('skill_package_invalid', `Caminho inválido no pacote: ${file.path}`);
|
|
172
|
+
mkdirSync(dirname(destination), { recursive: true, mode: 0o700 });
|
|
173
|
+
writeFileSync(destination, entry.bytes, { mode: file.executable ? 0o755 : 0o644 });
|
|
174
|
+
}
|
|
175
|
+
writeFileSync(join(staging, SKILL_MARKER), `${JSON.stringify({ skillId: input.skillId, versionId: input.versionId, installedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o644 });
|
|
176
|
+
const replacing = existsSync(target);
|
|
177
|
+
if (replacing)
|
|
178
|
+
renameSync(target, previous);
|
|
179
|
+
try {
|
|
180
|
+
renameSync(staging, target);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
if (replacing)
|
|
184
|
+
renameSync(previous, target);
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
if (replacing)
|
|
188
|
+
rmSync(previous, { recursive: true, force: true });
|
|
189
|
+
return { path: target };
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
rmSync(staging, { recursive: true, force: true });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** Skills instaladas pelo Vault nas raízes informadas. */
|
|
196
|
+
export function installedSkills(roots) {
|
|
197
|
+
const out = [];
|
|
198
|
+
for (const root of roots) {
|
|
199
|
+
if (!existsSync(root))
|
|
200
|
+
continue;
|
|
201
|
+
for (const dir of readdirSync(root).sort()) {
|
|
202
|
+
const marker = join(root, dir, SKILL_MARKER);
|
|
203
|
+
if (dir.startsWith('.') || !existsSync(marker))
|
|
204
|
+
continue;
|
|
205
|
+
try {
|
|
206
|
+
const data = JSON.parse(readFileSync(marker, 'utf8'));
|
|
207
|
+
if (typeof data.skillId === 'string' && typeof data.versionId === 'string')
|
|
208
|
+
out.push({ root, dir, skillId: data.skillId, versionId: data.versionId });
|
|
209
|
+
}
|
|
210
|
+
catch { /* marcador corrompido: tratado como não gerenciado */ }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { VaultMemoryApi } from './cloud.js';
|
|
3
|
+
import { skillContextSection } from './skill-context.js';
|
|
4
|
+
export { skillContextSection };
|
|
5
|
+
export declare const SKILL_TEXT_MAX: number;
|
|
6
|
+
export interface SkillToolOptions {
|
|
7
|
+
cwd?: () => string;
|
|
8
|
+
fetch?: typeof fetch;
|
|
9
|
+
userHome?: string;
|
|
10
|
+
maxChars?: number;
|
|
11
|
+
}
|
|
12
|
+
type Folder = {
|
|
13
|
+
id: string;
|
|
14
|
+
parentId: string | null;
|
|
15
|
+
name: string;
|
|
16
|
+
path: string;
|
|
17
|
+
depth: number;
|
|
18
|
+
skillCount: number;
|
|
19
|
+
};
|
|
20
|
+
type Summary = {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
folderId: string | null;
|
|
25
|
+
folderPath: string | null;
|
|
26
|
+
projectIds: string[];
|
|
27
|
+
scope?: string;
|
|
28
|
+
latestVersion: {
|
|
29
|
+
id: string;
|
|
30
|
+
} | null;
|
|
31
|
+
};
|
|
32
|
+
export declare function page(text: string, offset?: number, max?: number): {
|
|
33
|
+
text: string;
|
|
34
|
+
offset: number;
|
|
35
|
+
nextOffset: number | null;
|
|
36
|
+
truncated: boolean;
|
|
37
|
+
};
|
|
38
|
+
export declare function loadFolders(cloud: VaultMemoryApi): Promise<Folder[]>;
|
|
39
|
+
/** Caminho "a/b" → id da pasta ('' ou '/' = raiz → null). */
|
|
40
|
+
export declare function folderByPath(folders: Folder[], path: string): Folder | null | undefined;
|
|
41
|
+
/** Aceita o id da skill, `pasta/sub/nome` ou só `nome` (quando for único). */
|
|
42
|
+
export declare function resolveSkill(cloud: VaultMemoryApi, ref: string): Promise<Summary | {
|
|
43
|
+
id: string;
|
|
44
|
+
}>;
|
|
45
|
+
export declare function currentProject(cloud: VaultMemoryApi, cwd: string): Promise<{
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
} | null>;
|
|
49
|
+
export declare const SKILL_TARGETS: readonly ["claude", "grok", "agents"];
|
|
50
|
+
export type SkillTarget = typeof SKILL_TARGETS[number];
|
|
51
|
+
export declare function targetRoot(target: SkillTarget, userHome?: string): string;
|
|
52
|
+
export declare function registerSkillTools(server: McpServer, home: string, cloud: VaultMemoryApi, options?: SkillToolOptions): void;
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// Biblioteca privada de skills do Vault pelo MCP: listar, ler, salvar, organizar
|
|
2
|
+
// e instalar localmente. Excluir fica de fora de propósito (só nas interfaces).
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { findWorkspaceProject } from './hooks.js';
|
|
7
|
+
import { run } from './mcp-result.js';
|
|
8
|
+
import { clip, skillContextSection } from './skill-context.js';
|
|
9
|
+
import { frontmatter, installedSkills, installSkillArchive, readSourceDir } from './skill-local.js';
|
|
10
|
+
export { skillContextSection };
|
|
11
|
+
export const SKILL_TEXT_MAX = 64 * 1024;
|
|
12
|
+
const SOURCE = 'biblioteca privada de skills do usuário (Vault)';
|
|
13
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
14
|
+
const unsupported = () => { throw new Error('Este Vault ainda não oferece a biblioteca de skills. Atualize o vault-go ou o servidor.'); };
|
|
15
|
+
/** Método do cliente preso ao próprio objeto: o VaultCloudClient é uma classe e depende do this. */
|
|
16
|
+
function api(cloud, key) {
|
|
17
|
+
const fn = cloud[key];
|
|
18
|
+
if (typeof fn !== 'function')
|
|
19
|
+
return unsupported();
|
|
20
|
+
return fn.bind(cloud);
|
|
21
|
+
}
|
|
22
|
+
const record = (value) => (value && typeof value === 'object' && !Array.isArray(value) ? value : {});
|
|
23
|
+
export function page(text, offset = 0, max = SKILL_TEXT_MAX) {
|
|
24
|
+
const start = Math.max(0, Math.min(offset, text.length));
|
|
25
|
+
const end = Math.min(text.length, start + max);
|
|
26
|
+
return { text: text.slice(start, end), offset: start, nextOffset: end < text.length ? end : null, truncated: end < text.length };
|
|
27
|
+
}
|
|
28
|
+
export async function loadFolders(cloud) {
|
|
29
|
+
const folders = record(await api(cloud, 'skillFolders')()).folders;
|
|
30
|
+
if (!Array.isArray(folders))
|
|
31
|
+
throw new Error('Resposta de pastas inválida.');
|
|
32
|
+
return folders;
|
|
33
|
+
}
|
|
34
|
+
/** Caminho "a/b" → id da pasta ('' ou '/' = raiz → null). */
|
|
35
|
+
export function folderByPath(folders, path) {
|
|
36
|
+
const normalized = path.trim().replace(/^\/+|\/+$/g, '');
|
|
37
|
+
if (!normalized)
|
|
38
|
+
return null;
|
|
39
|
+
return folders.find(f => f.path.toLowerCase() === normalized.toLowerCase());
|
|
40
|
+
}
|
|
41
|
+
async function listAll(cloud, query) {
|
|
42
|
+
const out = [];
|
|
43
|
+
let cursor;
|
|
44
|
+
for (let i = 0; i < 20; i++) {
|
|
45
|
+
const result = record(await api(cloud, 'skills')({ ...query, limit: 100, ...(cursor ? { cursor } : {}) }));
|
|
46
|
+
out.push(...(Array.isArray(result.skills) ? result.skills : []));
|
|
47
|
+
if (typeof result.nextCursor !== 'string')
|
|
48
|
+
break;
|
|
49
|
+
cursor = result.nextCursor;
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/** Aceita o id da skill, `pasta/sub/nome` ou só `nome` (quando for único). */
|
|
54
|
+
export async function resolveSkill(cloud, ref) {
|
|
55
|
+
const value = ref.trim();
|
|
56
|
+
if (UUID.test(value))
|
|
57
|
+
return { id: value };
|
|
58
|
+
const parts = value.replace(/^\/+|\/+$/g, '').split('/');
|
|
59
|
+
const name = parts.pop().toLowerCase();
|
|
60
|
+
if (parts.length) {
|
|
61
|
+
const folder = folderByPath(await loadFolders(cloud), parts.join('/'));
|
|
62
|
+
if (!folder)
|
|
63
|
+
throw new Error(`Pasta não encontrada: ${parts.join('/')}`);
|
|
64
|
+
const found = (await listAll(cloud, { folderId: folder.id })).find(s => s.name.toLowerCase() === name);
|
|
65
|
+
if (!found)
|
|
66
|
+
throw new Error(`Skill não encontrada: ${value}`);
|
|
67
|
+
return found;
|
|
68
|
+
}
|
|
69
|
+
const matches = (await listAll(cloud, {})).filter(s => s.name.toLowerCase() === name);
|
|
70
|
+
if (matches.length === 1)
|
|
71
|
+
return matches[0];
|
|
72
|
+
if (!matches.length)
|
|
73
|
+
throw new Error(`Skill não encontrada: ${value}`);
|
|
74
|
+
throw new Error(`Mais de uma skill com esse nome; informe o caminho: ${matches.map(s => `${s.folderPath ? `${s.folderPath}/` : ''}${s.name}`).join(', ')}`);
|
|
75
|
+
}
|
|
76
|
+
export async function currentProject(cloud, cwd) {
|
|
77
|
+
try {
|
|
78
|
+
const project = await findWorkspaceProject(cloud, cwd);
|
|
79
|
+
return project && typeof project.id === 'string' ? { id: project.id, name: typeof project.name === 'string' ? project.name : '' } : null;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const ARCHIVE_MAX_BYTES = 40 * 1024 * 1024;
|
|
86
|
+
export const SKILL_TARGETS = ['claude', 'grok', 'agents'];
|
|
87
|
+
export function targetRoot(target, userHome = homedir()) {
|
|
88
|
+
return target === 'agents' ? join(userHome, '.agents', 'skills') : join(userHome, `.${target}`, 'skills');
|
|
89
|
+
}
|
|
90
|
+
/** Garante a pasta pelo caminho, criando os segmentos que faltarem; '' é a raiz (null). */
|
|
91
|
+
async function ensureFolder(cloud, path) {
|
|
92
|
+
const segments = path.trim().replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
|
|
93
|
+
if (!segments.length)
|
|
94
|
+
return null;
|
|
95
|
+
const folders = await loadFolders(cloud);
|
|
96
|
+
let parentId = null;
|
|
97
|
+
let prefix = '';
|
|
98
|
+
for (const name of segments) {
|
|
99
|
+
prefix = prefix ? `${prefix}/${name}` : name;
|
|
100
|
+
const existing = folderByPath(folders, prefix);
|
|
101
|
+
if (existing) {
|
|
102
|
+
parentId = existing.id;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const created = record(record(await api(cloud, 'createSkillFolder')({ name, parentId })).folder);
|
|
106
|
+
if (typeof created.id !== 'string')
|
|
107
|
+
throw new Error('Resposta de pasta inválida.');
|
|
108
|
+
folders.push(created);
|
|
109
|
+
parentId = created.id;
|
|
110
|
+
}
|
|
111
|
+
return parentId;
|
|
112
|
+
}
|
|
113
|
+
const union = (a, b) => [...new Set([...a, ...b])];
|
|
114
|
+
const compact = (s) => ({ id: s.id, name: s.name, description: clip(s.description), folder: s.folderPath, scope: s.scope ?? null, version: s.latestVersion?.id ?? null, projectIds: s.projectIds });
|
|
115
|
+
function tree(folders) {
|
|
116
|
+
return folders.map(f => `${' '.repeat(f.depth - 1)}${f.name} (${f.skillCount})`).join('\n');
|
|
117
|
+
}
|
|
118
|
+
export function registerSkillTools(server, home, cloud, options = {}) {
|
|
119
|
+
void home;
|
|
120
|
+
const cwd = options.cwd ?? (() => process.cwd());
|
|
121
|
+
const max = options.maxChars ?? SKILL_TEXT_MAX;
|
|
122
|
+
const read = { readOnlyHint: true, idempotentHint: true, openWorldHint: true };
|
|
123
|
+
const skillRef = z.string().trim().min(1).max(600).describe('Id da skill, "pasta/sub/nome" ou só o nome quando for único');
|
|
124
|
+
const version = z.string().trim().regex(/^[A-Za-z0-9_-]{1,128}$/).optional();
|
|
125
|
+
const offset = z.number().int().min(0).default(0);
|
|
126
|
+
server.registerTool('vault_go_skills', {
|
|
127
|
+
title: 'Listar skills privadas',
|
|
128
|
+
description: 'Lista a biblioteca privada de skills do usuário. Sem projectId, usa o projeto do diretório atual (skills do projeto primeiro, depois as globais). Use q para busca por texto e folder ("dev/review") para filtrar por pasta.',
|
|
129
|
+
inputSchema: {
|
|
130
|
+
q: z.string().trim().min(1).max(200).optional(), folder: z.string().trim().max(600).optional(), recursive: z.boolean().optional(),
|
|
131
|
+
projectId: z.string().trim().min(1).max(128).optional(), ignoreProject: z.boolean().optional(), limit: z.number().int().min(1).max(50).default(25),
|
|
132
|
+
cursor: z.string().max(200).optional(),
|
|
133
|
+
},
|
|
134
|
+
annotations: read,
|
|
135
|
+
}, async (input) => run(async () => {
|
|
136
|
+
const query = {};
|
|
137
|
+
if (input.q)
|
|
138
|
+
query.q = input.q;
|
|
139
|
+
if (input.folder !== undefined) {
|
|
140
|
+
const folder = folderByPath(await loadFolders(cloud), input.folder);
|
|
141
|
+
if (folder === undefined)
|
|
142
|
+
throw new Error(`Pasta não encontrada: ${input.folder}`);
|
|
143
|
+
query.folderId = folder ? folder.id : 'root';
|
|
144
|
+
if (input.recursive)
|
|
145
|
+
query.recursive = 'true';
|
|
146
|
+
}
|
|
147
|
+
const project = input.projectId ? { id: input.projectId, name: '' } : input.ignoreProject ? null : await currentProject(cloud, cwd());
|
|
148
|
+
if (project)
|
|
149
|
+
query.projectId = project.id;
|
|
150
|
+
query.limit = input.limit;
|
|
151
|
+
if (input.cursor)
|
|
152
|
+
query.cursor = input.cursor;
|
|
153
|
+
const result = record(await api(cloud, 'skills')(query));
|
|
154
|
+
const skills = (Array.isArray(result.skills) ? result.skills : []).map(compact);
|
|
155
|
+
return {
|
|
156
|
+
project, skills, nextCursor: result.nextCursor ?? null,
|
|
157
|
+
...(!project && !input.ignoreProject && !input.projectId ? { note: 'Nenhum projeto do Vault corresponde ao diretório atual; listando toda a biblioteca.' } : {}),
|
|
158
|
+
};
|
|
159
|
+
}));
|
|
160
|
+
server.registerTool('vault_go_skill_folders', {
|
|
161
|
+
title: 'Pastas de skills',
|
|
162
|
+
description: 'Mostra a árvore de pastas da biblioteca privada de skills, com a quantidade de skills em cada pasta.',
|
|
163
|
+
inputSchema: {}, annotations: read,
|
|
164
|
+
}, async () => run(async () => {
|
|
165
|
+
const folders = await loadFolders(cloud);
|
|
166
|
+
return { tree: tree(folders), folders };
|
|
167
|
+
}));
|
|
168
|
+
server.registerTool('vault_go_skill_read', {
|
|
169
|
+
title: 'Ler skill',
|
|
170
|
+
description: 'Carrega uma skill da biblioteca privada: o SKILL.md completo e a lista de arquivos. Siga o SKILL.md; abra arquivos auxiliares com vault_go_skill_file só quando ele mandar.',
|
|
171
|
+
inputSchema: { skill: skillRef, version, offset }, annotations: read,
|
|
172
|
+
}, async (input) => run(async () => {
|
|
173
|
+
const { id } = await resolveSkill(cloud, input.skill);
|
|
174
|
+
const skill = record(record(await api(cloud, 'skill')(id, input.version)).skill);
|
|
175
|
+
const md = page(typeof skill.skillMd === 'string' ? skill.skillMd : '', input.offset, max);
|
|
176
|
+
const files = (Array.isArray(skill.files) ? skill.files : []).map(f => ({ path: f.path, size: f.size, kind: f.classification }));
|
|
177
|
+
return {
|
|
178
|
+
source: SOURCE, id: skill.id, name: skill.name, description: skill.description, folder: skill.folderPath ?? null,
|
|
179
|
+
version: record(skill.version).id ?? null, projectIds: skill.projectIds ?? [],
|
|
180
|
+
instructions: 'Siga as instruções do SKILL.md. Abra um arquivo auxiliar com vault_go_skill_file somente quando o SKILL.md o referenciar e ele for necessário. Não execute scripts sem necessidade clara.',
|
|
181
|
+
skillMd: md.text, offset: md.offset, nextOffset: md.nextOffset, truncated: md.truncated, files,
|
|
182
|
+
};
|
|
183
|
+
}));
|
|
184
|
+
server.registerTool('vault_go_skill_file', {
|
|
185
|
+
title: 'Ler arquivo de skill',
|
|
186
|
+
description: 'Lê um arquivo auxiliar de uma skill (ex.: references/guia.md, scripts/run.py). Texto volta em UTF-8; binário em base64. Conteúdo grande é paginado com offset.',
|
|
187
|
+
inputSchema: { skill: skillRef, path: z.string().trim().min(1).max(1024), version, offset }, annotations: read,
|
|
188
|
+
}, async (input) => run(async () => {
|
|
189
|
+
const { id } = await resolveSkill(cloud, input.skill);
|
|
190
|
+
const file = record(await api(cloud, 'skillFile')(id, input.path, input.version));
|
|
191
|
+
const content = page(typeof file.content === 'string' ? file.content : '', input.offset, max);
|
|
192
|
+
return { source: SOURCE, path: file.path, encoding: file.encoding, size: file.size, executable: file.executable ?? false, version: file.version ?? null, content: content.text, offset: content.offset, nextOffset: content.nextOffset, truncated: content.truncated };
|
|
193
|
+
}));
|
|
194
|
+
const fileInput = z.object({
|
|
195
|
+
path: z.string().trim().min(1).max(1024), content: z.string(), encoding: z.enum(['utf8', 'base64']).optional(), executable: z.boolean().optional(),
|
|
196
|
+
});
|
|
197
|
+
const normalize = (files) => files.map(f => ({ path: f.path, content: f.content, encoding: f.encoding ?? 'utf8', ...(f.executable !== undefined ? { executable: f.executable } : {}) }));
|
|
198
|
+
const write = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
|
|
199
|
+
server.registerTool('vault_go_skill_save', {
|
|
200
|
+
title: 'Salvar skill',
|
|
201
|
+
description: 'Salva uma skill na biblioteca privada: cria se não existir nessa pasta, senão publica uma nova versão (as anteriores ficam guardadas). Envie files (SKILL.md com frontmatter name/description e arquivos auxiliares) ou sourceDir (pasta local). folder aceita caminho ("dev/review") e cria o que faltar. linkCurrentProject liga ao projeto do diretório atual.',
|
|
202
|
+
inputSchema: {
|
|
203
|
+
name: z.string().trim().min(1).max(64).optional(), description: z.string().trim().min(1).max(4096).optional(),
|
|
204
|
+
folder: z.string().trim().max(600).optional(), projectIds: z.array(z.string().trim().min(1).max(128)).max(50).optional(),
|
|
205
|
+
linkCurrentProject: z.boolean().optional(), files: z.array(fileInput).max(512).optional(), sourceDir: z.string().trim().min(1).max(2000).optional(),
|
|
206
|
+
patch: z.boolean().optional(), remove: z.array(z.string().trim().min(1).max(1024)).max(512).optional(),
|
|
207
|
+
baseVersion: z.string().trim().regex(/^[A-Za-z0-9_-]{1,128}$/).optional(), releaseNotes: z.string().max(10_000).optional(),
|
|
208
|
+
},
|
|
209
|
+
annotations: write,
|
|
210
|
+
}, async (input) => run(async () => {
|
|
211
|
+
if (!input.files && !input.sourceDir)
|
|
212
|
+
throw new Error('Informe files ou sourceDir.');
|
|
213
|
+
const files = input.sourceDir ? readSourceDir(input.sourceDir) : normalize(input.files ?? []);
|
|
214
|
+
const md = files.find(f => f.path === 'SKILL.md');
|
|
215
|
+
const front = md && md.encoding === 'utf8' ? frontmatter(md.content) : null;
|
|
216
|
+
if (!input.patch && !front)
|
|
217
|
+
throw new Error('SKILL.md na raiz precisa de frontmatter com name e description.');
|
|
218
|
+
if (input.patch && !input.baseVersion)
|
|
219
|
+
throw new Error('patch exige baseVersion.');
|
|
220
|
+
const name = input.name ?? front?.name;
|
|
221
|
+
if (!name)
|
|
222
|
+
throw new Error('Informe name (ou um SKILL.md com frontmatter).');
|
|
223
|
+
const folderId = input.folder !== undefined ? await ensureFolder(cloud, input.folder) : null;
|
|
224
|
+
const existing = (await listAll(cloud, { folderId: folderId ?? 'root' })).find(s => s.name.toLowerCase() === name.toLowerCase());
|
|
225
|
+
const project = input.linkCurrentProject ? await currentProject(cloud, cwd()) : null;
|
|
226
|
+
if (input.linkCurrentProject && !project)
|
|
227
|
+
throw new Error('Nenhum projeto do Vault corresponde ao diretório atual.');
|
|
228
|
+
const links = union(input.projectIds ?? [], project ? [project.id] : []);
|
|
229
|
+
const releaseNotes = input.releaseNotes ?? '';
|
|
230
|
+
const naming = { ...(input.name ? { name: input.name } : {}), ...(input.description ? { description: input.description } : {}) };
|
|
231
|
+
if (!existing) {
|
|
232
|
+
if (input.patch)
|
|
233
|
+
throw new Error(`Skill não encontrada para patch: ${name}`);
|
|
234
|
+
const created = record(await api(cloud, 'createSkill')({ files, folderId, releaseNotes, ...naming, ...(links.length ? { projectIds: links } : {}) }));
|
|
235
|
+
return { action: 'created', skill: compact(record(created.skill)) };
|
|
236
|
+
}
|
|
237
|
+
const body = input.patch
|
|
238
|
+
? { baseVersion: input.baseVersion, upsert: files, remove: input.remove ?? [], releaseNotes, ...(input.description ? { description: input.description } : {}) }
|
|
239
|
+
: { files, ...(input.baseVersion ? { baseVersion: input.baseVersion } : {}), releaseNotes, ...naming };
|
|
240
|
+
let updated = record(record(await api(cloud, 'createSkillVersion')(existing.id, body)).skill);
|
|
241
|
+
if (links.length)
|
|
242
|
+
updated = record(record(await api(cloud, 'setSkillProjects')(existing.id, union(existing.projectIds ?? [], links))).skill);
|
|
243
|
+
return { action: 'updated', skill: compact(updated) };
|
|
244
|
+
}));
|
|
245
|
+
server.registerTool('vault_go_skill_organize', {
|
|
246
|
+
title: 'Organizar skills',
|
|
247
|
+
description: 'Organiza a biblioteca privada: create_folder, move_folder, rename_folder, move_skill, rename_skill, link_projects e unlink_projects. Caminhos de pasta usam "/" e "" é a raiz. Sem projectIds, link/unlink usam o projeto do diretório atual. Não exclui nada.',
|
|
248
|
+
inputSchema: {
|
|
249
|
+
action: z.enum(['create_folder', 'move_folder', 'rename_folder', 'move_skill', 'rename_skill', 'link_projects', 'unlink_projects']),
|
|
250
|
+
skill: skillRef.optional(), folder: z.string().trim().max(600).optional(), targetFolder: z.string().trim().max(600).optional(),
|
|
251
|
+
newName: z.string().trim().min(1).max(64).optional(), description: z.string().trim().min(1).max(4096).optional(),
|
|
252
|
+
projectIds: z.array(z.string().trim().min(1).max(128)).max(50).optional(),
|
|
253
|
+
},
|
|
254
|
+
annotations: write,
|
|
255
|
+
}, async (input) => run(async () => {
|
|
256
|
+
const required = (value, field) => { if (value === undefined || value === null)
|
|
257
|
+
throw new Error(`${input.action} exige ${field}.`); return value; };
|
|
258
|
+
const existingFolder = async (path) => {
|
|
259
|
+
const folder = folderByPath(await loadFolders(cloud), path);
|
|
260
|
+
if (!folder)
|
|
261
|
+
throw new Error(`Pasta não encontrada: ${path}`);
|
|
262
|
+
return folder;
|
|
263
|
+
};
|
|
264
|
+
switch (input.action) {
|
|
265
|
+
case 'create_folder':
|
|
266
|
+
return { folderId: await ensureFolder(cloud, required(input.folder, 'folder')) };
|
|
267
|
+
case 'rename_folder': {
|
|
268
|
+
const folder = await existingFolder(required(input.folder, 'folder'));
|
|
269
|
+
return api(cloud, 'updateSkillFolder')(folder.id, { name: required(input.newName, 'newName') });
|
|
270
|
+
}
|
|
271
|
+
case 'move_folder': {
|
|
272
|
+
const folder = await existingFolder(required(input.folder, 'folder'));
|
|
273
|
+
return api(cloud, 'updateSkillFolder')(folder.id, { parentId: await ensureFolder(cloud, required(input.targetFolder, 'targetFolder')) });
|
|
274
|
+
}
|
|
275
|
+
case 'move_skill': {
|
|
276
|
+
const target = required(input.targetFolder, 'targetFolder');
|
|
277
|
+
const { id } = await resolveSkill(cloud, required(input.skill, 'skill'));
|
|
278
|
+
return api(cloud, 'updateSkill')(id, { folderId: await ensureFolder(cloud, target) });
|
|
279
|
+
}
|
|
280
|
+
case 'rename_skill': {
|
|
281
|
+
const { id } = await resolveSkill(cloud, required(input.skill, 'skill'));
|
|
282
|
+
return api(cloud, 'updateSkill')(id, { name: required(input.newName, 'newName'), ...(input.description ? { description: input.description } : {}) });
|
|
283
|
+
}
|
|
284
|
+
case 'link_projects':
|
|
285
|
+
case 'unlink_projects': {
|
|
286
|
+
const { id } = await resolveSkill(cloud, required(input.skill, 'skill'));
|
|
287
|
+
let ids = input.projectIds;
|
|
288
|
+
if (!ids?.length) {
|
|
289
|
+
const project = await currentProject(cloud, cwd());
|
|
290
|
+
if (!project)
|
|
291
|
+
throw new Error('Informe projectIds: nenhum projeto do Vault corresponde ao diretório atual.');
|
|
292
|
+
ids = [project.id];
|
|
293
|
+
}
|
|
294
|
+
const current = record(record(await api(cloud, 'skill')(id)).skill).projectIds ?? [];
|
|
295
|
+
const next = input.action === 'link_projects' ? union(current, ids) : current.filter(p => !ids.includes(p));
|
|
296
|
+
return api(cloud, 'setSkillProjects')(id, next);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}));
|
|
300
|
+
const userHome = options.userHome ?? homedir();
|
|
301
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
302
|
+
const targets = z.array(z.enum(SKILL_TARGETS)).min(1).max(3).default(['claude']);
|
|
303
|
+
async function download(id, versionId) {
|
|
304
|
+
const grant = record(await api(cloud, 'skillDownloadGrant')(id, versionId));
|
|
305
|
+
const url = record(grant.grant).url, version = record(grant.version);
|
|
306
|
+
if (typeof url !== 'string' || typeof version.archiveSha256 !== 'string' || typeof version.id !== 'string')
|
|
307
|
+
throw new Error('Resposta de download inválida.');
|
|
308
|
+
const response = await fetcher(url, { redirect: 'error', signal: AbortSignal.timeout(60_000) });
|
|
309
|
+
if (!response.ok)
|
|
310
|
+
throw new Error(`Download da skill falhou (HTTP ${response.status}).`);
|
|
311
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
312
|
+
if (bytes.length > ARCHIVE_MAX_BYTES)
|
|
313
|
+
throw new Error('Pacote acima de 40 MiB.');
|
|
314
|
+
return { bytes, sha256: version.archiveSha256, versionId: version.id };
|
|
315
|
+
}
|
|
316
|
+
async function installInto(id, name, roots, versionId) {
|
|
317
|
+
const archive = await download(id, versionId);
|
|
318
|
+
return roots.map(root => ({ root, versionId: archive.versionId, ...installSkillArchive({ bytes: archive.bytes, expectedSha256: archive.sha256, dirName: name, root, skillId: id, versionId: archive.versionId }) }));
|
|
319
|
+
}
|
|
320
|
+
server.registerTool('vault_go_skill_install', {
|
|
321
|
+
title: 'Instalar skill localmente',
|
|
322
|
+
description: 'Baixa uma skill da biblioteca privada, confere a integridade e instala em ~/.claude/skills (ou .grok/.agents). Nunca sobrescreve uma skill que o Vault não instalou.',
|
|
323
|
+
inputSchema: { skill: skillRef, version, targets },
|
|
324
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
325
|
+
}, async (input) => run(async () => {
|
|
326
|
+
const { id } = await resolveSkill(cloud, input.skill);
|
|
327
|
+
const detail = record(record(await api(cloud, 'skill')(id, input.version)).skill);
|
|
328
|
+
const name = String(detail.name);
|
|
329
|
+
const done = await installInto(id, name, input.targets.map(t => targetRoot(t, userHome)), input.version);
|
|
330
|
+
return { installed: done.map((d, i) => ({ target: input.targets[i], path: d.path, versionId: d.versionId })) };
|
|
331
|
+
}));
|
|
332
|
+
server.registerTool('vault_go_skill_sync', {
|
|
333
|
+
title: 'Sincronizar skills instaladas',
|
|
334
|
+
description: 'Atualiza as skills instaladas pelo Vault que têm versão nova. Com currentProject ou projectId, também instala as skills vinculadas ao projeto que ainda não estão instaladas.',
|
|
335
|
+
inputSchema: { currentProject: z.boolean().optional(), projectId: z.string().trim().min(1).max(128).optional(), targets },
|
|
336
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
337
|
+
}, async (input) => run(async () => {
|
|
338
|
+
const roots = SKILL_TARGETS.map(t => targetRoot(t, userHome));
|
|
339
|
+
const installed = installedSkills(roots);
|
|
340
|
+
const updated = [], missing = [], added = [];
|
|
341
|
+
for (const item of installed) {
|
|
342
|
+
let detail;
|
|
343
|
+
try {
|
|
344
|
+
detail = record(record(await api(cloud, 'skill')(item.skillId)).skill);
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
missing.push({ skill: item.dir, path: join(item.root, item.dir) });
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const latest = record(detail.latestVersion).id;
|
|
351
|
+
if (typeof latest !== 'string' || latest === item.versionId)
|
|
352
|
+
continue;
|
|
353
|
+
const [done] = await installInto(item.skillId, item.dir, [item.root], latest);
|
|
354
|
+
updated.push({ skill: detail.name, from: item.versionId, to: latest, path: done.path });
|
|
355
|
+
}
|
|
356
|
+
const projectId = input.projectId ?? (input.currentProject ? (await currentProject(cloud, cwd()))?.id : undefined);
|
|
357
|
+
if (input.currentProject && !projectId)
|
|
358
|
+
throw new Error('Nenhum projeto do Vault corresponde ao diretório atual.');
|
|
359
|
+
if (projectId) {
|
|
360
|
+
const have = new Set(installedSkills(roots).map(i => i.skillId));
|
|
361
|
+
const linked = (await listAll(cloud, { projectId })).filter(s => s.scope === 'project' && !have.has(s.id));
|
|
362
|
+
for (const skill of linked) {
|
|
363
|
+
const done = await installInto(skill.id, skill.name, input.targets.map(t => targetRoot(t, userHome)));
|
|
364
|
+
added.push(...done.map(d => ({ skill: skill.name, path: d.path, versionId: d.versionId })));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return { updated, installed: added, missing };
|
|
368
|
+
}));
|
|
369
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vault-go",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "MCP server and installer for Vault Memory: search, capture, and use account memory across AI clients.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,12 @@
|
|
|
26
26
|
"dist/knowledge-agent.d.ts",
|
|
27
27
|
"dist/knowledge-tools.js",
|
|
28
28
|
"dist/knowledge-tools.d.ts",
|
|
29
|
+
"dist/skill-context.js",
|
|
30
|
+
"dist/skill-context.d.ts",
|
|
31
|
+
"dist/skill-local.js",
|
|
32
|
+
"dist/skill-local.d.ts",
|
|
33
|
+
"dist/skill-tools.js",
|
|
34
|
+
"dist/skill-tools.d.ts",
|
|
29
35
|
"dist/activity.js",
|
|
30
36
|
"dist/activity.d.ts",
|
|
31
37
|
"dist/auth.js",
|