vault-go 0.29.2 → 0.31.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/README.md +12 -0
- package/dist/agent-cost.d.ts +11 -0
- package/dist/agent-cost.js +116 -0
- package/dist/auth.js +29 -2
- package/dist/capabilities.d.ts +40 -24
- package/dist/capabilities.js +8 -6
- package/dist/capture-privacy.js +1 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +16 -1
- package/dist/cloud.d.ts +22 -0
- package/dist/cloud.js +131 -47
- package/dist/config.d.ts +2 -0
- package/dist/config.js +15 -1
- package/dist/crystal-memory-mirror.d.ts +14 -1
- package/dist/crystal-memory-mirror.js +24 -15
- package/dist/crystal-tools.d.ts +24 -1
- package/dist/crystal-tools.js +30 -5
- package/dist/grok-awareness.js +54 -1
- package/dist/hook-install.d.ts +4 -0
- package/dist/hook-install.js +41 -1
- package/dist/hook-queue.d.ts +18 -0
- package/dist/hook-queue.js +115 -3
- package/dist/hook-runner.js +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +27 -8
- package/dist/index.js +5 -1
- package/dist/installer.js +8 -1
- package/dist/legacy-brand.d.ts +2 -0
- package/dist/legacy-brand.js +5 -0
- package/dist/local-service.js +11 -0
- package/dist/memory-archive.d.ts +25 -0
- package/dist/memory-archive.js +65 -0
- package/dist/modes.d.ts +1 -1
- package/dist/modes.js +3 -1
- package/dist/opencode-plugin.d.ts +29 -1
- package/dist/opencode-plugin.js +36 -1
- package/dist/server.js +68 -0
- package/dist/skill-context.d.ts +4 -0
- package/dist/skill-context.js +16 -0
- package/dist/skill-download.d.ts +6 -0
- package/dist/skill-download.js +48 -0
- package/dist/skill-library.d.ts +8 -0
- package/dist/skill-library.js +32 -0
- package/dist/skill-local.d.ts +53 -0
- package/dist/skill-local.js +251 -0
- package/dist/skill-tools.d.ts +52 -0
- package/dist/skill-tools.js +356 -0
- package/dist/skills-cli.d.ts +31 -0
- package/dist/skills-cli.js +90 -0
- package/dist/transcript-config.d.ts +1 -1
- package/dist/transcript-config.js +4 -3
- package/dist/transcript-parser.js +1 -1
- package/dist/transcript-tools.js +1 -1
- package/package.json +19 -1
package/dist/opencode-plugin.js
CHANGED
|
@@ -8,7 +8,7 @@ function owner(home) {
|
|
|
8
8
|
process.env.VAULT_GO_TOKEN || loadTokens(home)?.accessToken || '',
|
|
9
9
|
])).digest('hex');
|
|
10
10
|
}
|
|
11
|
-
/** OpenCode 1.x plugin
|
|
11
|
+
/** OpenCode 1.x plugin: inject context and capture prompts, tool use and session end. */
|
|
12
12
|
export function createOpenCodePlugin(input, options = {}) {
|
|
13
13
|
const home = options.home ?? vaultHome();
|
|
14
14
|
const pending = new Map();
|
|
@@ -54,6 +54,13 @@ export function createOpenCodePlugin(input, options = {}) {
|
|
|
54
54
|
controller.abort();
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
async function capture(event, value) {
|
|
58
|
+
try {
|
|
59
|
+
const cloud = new VaultCloudClient(home, options.fetch ?? globalThis.fetch, false);
|
|
60
|
+
await executeHook('opencode', event, { cwd: input.directory, ...value }, cloud, home);
|
|
61
|
+
}
|
|
62
|
+
catch { /* capture must not stop the client */ }
|
|
63
|
+
}
|
|
57
64
|
return {
|
|
58
65
|
'experimental.chat.system.transform': async (request, output) => {
|
|
59
66
|
if (!request.sessionID || !input.directory)
|
|
@@ -69,5 +76,33 @@ export function createOpenCodePlugin(input, options = {}) {
|
|
|
69
76
|
output.system.push(result.context);
|
|
70
77
|
}
|
|
71
78
|
},
|
|
79
|
+
'chat.message': async (_input, output) => {
|
|
80
|
+
const prompt = (output.parts ?? []).filter(part => part.type === 'text' || !part.type).map(part => part.text ?? '').join('\n').trim();
|
|
81
|
+
if (prompt)
|
|
82
|
+
await capture('session-init', { session_id: _input.sessionID, prompt });
|
|
83
|
+
},
|
|
84
|
+
'tool.execute.before': async (toolInput, output) => {
|
|
85
|
+
if (toolInput.tool === 'read') {
|
|
86
|
+
await capture('file-context', { session_id: toolInput.sessionID, tool_name: 'read', tool_input: output.args ?? {} });
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
'tool.execute.after': async (toolInput, output) => {
|
|
90
|
+
if (!toolInput.tool || /vault[_-]go|vault[_-]mem/i.test(toolInput.tool))
|
|
91
|
+
return;
|
|
92
|
+
await capture('observation', {
|
|
93
|
+
session_id: toolInput.sessionID,
|
|
94
|
+
tool_name: toolInput.tool,
|
|
95
|
+
tool_use_id: toolInput.callID,
|
|
96
|
+
tool_input: output.args ?? {},
|
|
97
|
+
tool_response: output.output ?? null,
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
event: async ({ event }) => {
|
|
101
|
+
if (event?.type !== 'session.idle')
|
|
102
|
+
return;
|
|
103
|
+
const info = event.properties?.info;
|
|
104
|
+
const sessionId = info && typeof info === 'object' ? String(info.id ?? '') : '';
|
|
105
|
+
await capture('session-end', { session_id: sessionId, last_assistant_message: '' });
|
|
106
|
+
},
|
|
72
107
|
};
|
|
73
108
|
}
|
package/dist/server.js
CHANGED
|
@@ -8,11 +8,14 @@ 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';
|
|
14
15
|
import { getStatus, vaultHome } from './config.js';
|
|
15
16
|
import { createPrivateBackup, restorePrivateBackup } from './backup.js';
|
|
17
|
+
import { estimateAgentCost } from './agent-cost.js';
|
|
18
|
+
import { flattenArchive, groupArchive, recordsFromExport } from './memory-archive.js';
|
|
16
19
|
import { CONTEXT_ENGINE_IDS, generateContext, listContextEngines, parseEngineModel, selectContextEngine, selectEngineModel, selectedContextEngine, selectedEngineModel, } from './context-engines.js';
|
|
17
20
|
import packageJson from '../package.json' with { type: 'json' };
|
|
18
21
|
export const VERSION = packageJson.version;
|
|
@@ -55,6 +58,7 @@ export function createVaultGoServer(home = vaultHome(), cloud = new VaultCloudCl
|
|
|
55
58
|
registerVaultModes(server);
|
|
56
59
|
registerCrystalTools(server, home);
|
|
57
60
|
registerKnowledgeAgentTools(server, home, cloud);
|
|
61
|
+
registerSkillTools(server, home, cloud);
|
|
58
62
|
server.registerTool('vault_go_engines', {
|
|
59
63
|
title: 'Listar motores de contexto',
|
|
60
64
|
description: 'Lista os motores disponíveis para preparar contexto: Vault AI, Claude, OpenRouter, Gemini e ChatGPT/Codex. Não revela chaves.',
|
|
@@ -484,6 +488,70 @@ export function createVaultGoServer(home = vaultHome(), cloud = new VaultCloudCl
|
|
|
484
488
|
},
|
|
485
489
|
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: true },
|
|
486
490
|
}, async (input) => run(() => cloud.importMemories(input)));
|
|
491
|
+
const archiveRecord = z.object({
|
|
492
|
+
sourceId: z.string().trim().min(1).max(500),
|
|
493
|
+
kind: z.string().trim().min(1).max(100),
|
|
494
|
+
content: z.string().trim().min(1).max(200_000),
|
|
495
|
+
title: z.string().trim().max(500).optional(),
|
|
496
|
+
sessionId: z.string().uuid().optional(),
|
|
497
|
+
memoryType: z.string().trim().min(1).max(100).optional(),
|
|
498
|
+
facts: z.array(z.string().trim().min(1).max(2_000)).max(200).optional(),
|
|
499
|
+
concepts: z.array(z.string().trim().min(1).max(500)).max(200).optional(),
|
|
500
|
+
filesRead: z.array(z.string().trim().min(1).max(2_000)).max(500).optional(),
|
|
501
|
+
filesModified: z.array(z.string().trim().min(1).max(2_000)).max(500).optional(),
|
|
502
|
+
occurredAt: z.string().trim().max(40).optional(),
|
|
503
|
+
});
|
|
504
|
+
const archiveGroup = z.array(archiveRecord).max(1_000);
|
|
505
|
+
server.registerTool('vault_go_archive_export', {
|
|
506
|
+
title: 'Exportar arquivo em quatro grupos',
|
|
507
|
+
description: 'Exporta memórias da conta e as separa em observations, sessions, summaries e prompts.',
|
|
508
|
+
inputSchema: {
|
|
509
|
+
projectId: uuid.optional(),
|
|
510
|
+
limit: z.number().int().min(1).max(1_000).default(500),
|
|
511
|
+
},
|
|
512
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
513
|
+
}, async (input) => run(async () => groupArchive(recordsFromExport(await cloud.exportMemories(input)))));
|
|
514
|
+
server.registerTool('vault_go_archive_import', {
|
|
515
|
+
title: 'Importar arquivo em quatro grupos',
|
|
516
|
+
description: 'Importa observations, sessions, summaries e prompts para um projeto existente.',
|
|
517
|
+
inputSchema: {
|
|
518
|
+
projectId: uuid,
|
|
519
|
+
observations: archiveGroup.default([]),
|
|
520
|
+
sessions: archiveGroup.default([]),
|
|
521
|
+
summaries: archiveGroup.default([]),
|
|
522
|
+
prompts: archiveGroup.default([]),
|
|
523
|
+
},
|
|
524
|
+
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: true },
|
|
525
|
+
}, async (input) => run(() => {
|
|
526
|
+
const records = flattenArchive({
|
|
527
|
+
version: 1,
|
|
528
|
+
format: 'vault-memory-groups',
|
|
529
|
+
observations: input.observations,
|
|
530
|
+
sessions: input.sessions,
|
|
531
|
+
summaries: input.summaries,
|
|
532
|
+
prompts: input.prompts,
|
|
533
|
+
}).map(record => ({
|
|
534
|
+
sourceId: record.sourceId,
|
|
535
|
+
kind: record.kind,
|
|
536
|
+
content: record.content,
|
|
537
|
+
memoryType: record.memoryType ?? 'discovery',
|
|
538
|
+
facts: record.facts ?? [],
|
|
539
|
+
concepts: record.concepts ?? [],
|
|
540
|
+
filesRead: record.filesRead ?? [],
|
|
541
|
+
filesModified: record.filesModified ?? [],
|
|
542
|
+
...(record.title ? { title: record.title } : {}),
|
|
543
|
+
...(record.sessionId ? { sessionId: record.sessionId } : {}),
|
|
544
|
+
}));
|
|
545
|
+
if (!records.length)
|
|
546
|
+
throw new Error('O arquivo não tem registros.');
|
|
547
|
+
return cloud.importMemories({ projectId: input.projectId, records });
|
|
548
|
+
}));
|
|
549
|
+
server.registerTool('vault_go_agent_cost_report', {
|
|
550
|
+
title: 'Estimar custo de transcripts',
|
|
551
|
+
description: 'Lê usage em JSONL locais e devolve tokens e um valor estimado. Sem usage, o valor fica indisponível.',
|
|
552
|
+
inputSchema: { root: z.string().trim().min(1).max(2_000) },
|
|
553
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
554
|
+
}, async ({ root }) => run(async () => estimateAgentCost(root)));
|
|
487
555
|
server.registerTool('vault_go_embedding_status', {
|
|
488
556
|
title: 'Status da busca semântica',
|
|
489
557
|
description: 'Mostra provedor, modelo e fila de vetores sem revelar a chave.',
|
|
@@ -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,48 @@
|
|
|
1
|
+
const MAX_ARCHIVE_BYTES = 40 * 1024 * 1024;
|
|
2
|
+
function record(value) {
|
|
3
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
4
|
+
throw new Error('Resposta de download inválida.');
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
export async function downloadSkillArchive(cloud, id, versionId, fetcher = globalThis.fetch) {
|
|
8
|
+
if (!cloud.skillDownloadGrant)
|
|
9
|
+
throw new Error('Este Vault não oferece download de skills.');
|
|
10
|
+
const grant = record(await cloud.skillDownloadGrant(id, versionId));
|
|
11
|
+
const link = record(grant.grant);
|
|
12
|
+
const version = record(grant.version);
|
|
13
|
+
if (typeof link.url !== 'string' || typeof version.id !== 'string' || typeof version.archiveSha256 !== 'string')
|
|
14
|
+
throw new Error('Resposta de download inválida.');
|
|
15
|
+
const url = new URL(link.url);
|
|
16
|
+
if (url.protocol !== 'https:')
|
|
17
|
+
throw new Error('Download inseguro da skill: HTTPS é obrigatório.');
|
|
18
|
+
const response = await fetcher(url, { redirect: 'error', signal: AbortSignal.timeout(60_000) });
|
|
19
|
+
if (!response.ok)
|
|
20
|
+
throw new Error(`Download da skill falhou (HTTP ${response.status}).`);
|
|
21
|
+
const declared = Number(response.headers.get('content-length'));
|
|
22
|
+
if (declared > MAX_ARCHIVE_BYTES) {
|
|
23
|
+
await response.body?.cancel();
|
|
24
|
+
throw new Error('Pacote acima de 40 MiB.');
|
|
25
|
+
}
|
|
26
|
+
if (!response.body)
|
|
27
|
+
throw new Error('Download vazio da skill.');
|
|
28
|
+
const reader = response.body.getReader();
|
|
29
|
+
const chunks = [];
|
|
30
|
+
let total = 0;
|
|
31
|
+
try {
|
|
32
|
+
for (;;) {
|
|
33
|
+
const next = await reader.read();
|
|
34
|
+
if (next.done)
|
|
35
|
+
break;
|
|
36
|
+
total += next.value.byteLength;
|
|
37
|
+
if (total > MAX_ARCHIVE_BYTES) {
|
|
38
|
+
await reader.cancel();
|
|
39
|
+
throw new Error('Pacote acima de 40 MiB.');
|
|
40
|
+
}
|
|
41
|
+
chunks.push(next.value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
reader.releaseLock();
|
|
46
|
+
}
|
|
47
|
+
return { bytes: Buffer.concat(chunks, total), sha256: version.archiveSha256, versionId: version.id };
|
|
48
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { VaultMemoryApi } from './cloud.js';
|
|
2
|
+
export type AccountSkillSummary = {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
folderPath?: string | null;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
};
|
|
8
|
+
export declare function listAllSkillSummaries(cloud: VaultMemoryApi, query?: Record<string, unknown>): Promise<AccountSkillSummary[]>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export async function listAllSkillSummaries(cloud, query = {}) {
|
|
2
|
+
if (!cloud.skills)
|
|
3
|
+
throw new Error('Este Vault não oferece a biblioteca de skills.');
|
|
4
|
+
const output = [];
|
|
5
|
+
const cursors = new Set();
|
|
6
|
+
let cursor;
|
|
7
|
+
for (let page = 0; page < 20; page++) {
|
|
8
|
+
const response = await cloud.skills({ ...query, limit: 100, ...(cursor ? { cursor } : {}) });
|
|
9
|
+
if (!response || typeof response !== 'object' || Array.isArray(response))
|
|
10
|
+
throw new Error('Resposta inválida da biblioteca de skills.');
|
|
11
|
+
const body = response;
|
|
12
|
+
if (!Array.isArray(body.skills))
|
|
13
|
+
throw new Error('Resposta inválida da biblioteca de skills.');
|
|
14
|
+
for (const item of body.skills) {
|
|
15
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
16
|
+
throw new Error('Resposta inválida da biblioteca de skills.');
|
|
17
|
+
const skill = item;
|
|
18
|
+
if (typeof skill.id !== 'string' || typeof skill.name !== 'string' ||
|
|
19
|
+
(skill.folderPath !== null && skill.folderPath !== undefined && typeof skill.folderPath !== 'string')) {
|
|
20
|
+
throw new Error('Resposta inválida da biblioteca de skills.');
|
|
21
|
+
}
|
|
22
|
+
output.push(skill);
|
|
23
|
+
}
|
|
24
|
+
if (body.nextCursor == null)
|
|
25
|
+
return output;
|
|
26
|
+
if (typeof body.nextCursor !== 'string' || !body.nextCursor || cursors.has(body.nextCursor))
|
|
27
|
+
throw new Error('Paginação inválida da biblioteca de skills.');
|
|
28
|
+
cursor = body.nextCursor;
|
|
29
|
+
cursors.add(cursor);
|
|
30
|
+
}
|
|
31
|
+
throw new Error('Biblioteca de skills excedeu o limite de páginas.');
|
|
32
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
/** Caminhos do manifesto de bundle (várias skills num tar): o Vault e o herdado. */
|
|
6
|
+
export declare const SKILL_BUNDLE_MANIFEST_PATHS: readonly string[];
|
|
7
|
+
export declare class SkillLocalError extends Error {
|
|
8
|
+
code: string;
|
|
9
|
+
constructor(code: string, message: string);
|
|
10
|
+
}
|
|
11
|
+
export interface LocalSkillFile {
|
|
12
|
+
path: string;
|
|
13
|
+
content: string;
|
|
14
|
+
encoding: 'utf8' | 'base64';
|
|
15
|
+
executable: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface TarEntry {
|
|
18
|
+
path: string;
|
|
19
|
+
bytes: Buffer;
|
|
20
|
+
mode: number;
|
|
21
|
+
}
|
|
22
|
+
export interface InstalledSkill {
|
|
23
|
+
root: string;
|
|
24
|
+
dir: string;
|
|
25
|
+
skillId: string;
|
|
26
|
+
versionId: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function inspectSkillDestination(root: string, dirName: string, skillId: string): {
|
|
29
|
+
status: 'missing' | 'managed' | 'conflict';
|
|
30
|
+
path: string;
|
|
31
|
+
reason?: 'unmanaged' | 'foreign';
|
|
32
|
+
};
|
|
33
|
+
/** name/description do frontmatter; valida só o formato mínimo (o servidor valida o resto). */
|
|
34
|
+
export declare function frontmatter(md: string): {
|
|
35
|
+
name: string;
|
|
36
|
+
description: string;
|
|
37
|
+
} | null;
|
|
38
|
+
/** Lê uma pasta de skill para publicar; symlinks e pastas de ferramentas ficam de fora. */
|
|
39
|
+
export declare function readSourceDir(directory: string): LocalSkillFile[];
|
|
40
|
+
/** Leitor ustar mínimo: só arquivos regulares; link, dispositivo ou caminho perigoso recusa o pacote inteiro. */
|
|
41
|
+
export declare function readTarGz(archive: Uint8Array): TarEntry[];
|
|
42
|
+
export declare function installSkillArchive(input: {
|
|
43
|
+
bytes: Uint8Array;
|
|
44
|
+
expectedSha256: string;
|
|
45
|
+
dirName: string;
|
|
46
|
+
root: string;
|
|
47
|
+
skillId: string;
|
|
48
|
+
versionId: string;
|
|
49
|
+
}): {
|
|
50
|
+
path: string;
|
|
51
|
+
};
|
|
52
|
+
/** Skills instaladas pelo Vault nas raízes informadas. */
|
|
53
|
+
export declare function installedSkills(roots: string[]): InstalledSkill[];
|
|
@@ -0,0 +1,251 @@
|
|
|
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
|
+
import { LEGACY_SKILL_BUNDLE_MANIFEST_PATH } from './legacy-brand.js';
|
|
10
|
+
export const SKILL_MARKER = '.vault-skill.json';
|
|
11
|
+
export const SKILL_FILE_MAX_BYTES = 4 * 1024 * 1024;
|
|
12
|
+
export const SKILL_MAX_FILES = 512;
|
|
13
|
+
export const SKILL_TOTAL_MAX_BYTES = 32 * 1024 * 1024;
|
|
14
|
+
const EXPANDED_MAX_BYTES = 40 * 1024 * 1024;
|
|
15
|
+
const IGNORED = new Set(['.git', 'node_modules', '.DS_Store', SKILL_MARKER]);
|
|
16
|
+
const NAME = /^[a-z0-9][a-z0-9.-]{0,63}$/;
|
|
17
|
+
/** Caminhos do manifesto de bundle (várias skills num tar): o Vault e o herdado. */
|
|
18
|
+
export const SKILL_BUNDLE_MANIFEST_PATHS = ['dev.vault.skill-sharing/manifest.json', LEGACY_SKILL_BUNDLE_MANIFEST_PATH];
|
|
19
|
+
export class SkillLocalError extends Error {
|
|
20
|
+
code;
|
|
21
|
+
constructor(code, message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.code = code;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
27
|
+
export function inspectSkillDestination(root, dirName, skillId) {
|
|
28
|
+
if (!NAME.test(dirName) || dirName.includes('..'))
|
|
29
|
+
throw new SkillLocalError('skill_install_invalid', `Nome de pasta inválido: ${dirName}`);
|
|
30
|
+
const path = join(resolve(root), dirName);
|
|
31
|
+
let target;
|
|
32
|
+
try {
|
|
33
|
+
target = lstatSync(path);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code === 'ENOENT')
|
|
37
|
+
return { status: 'missing', path };
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
if (!target.isDirectory() || target.isSymbolicLink())
|
|
41
|
+
return { status: 'conflict', path, reason: 'unmanaged' };
|
|
42
|
+
const markerPath = join(path, SKILL_MARKER);
|
|
43
|
+
try {
|
|
44
|
+
const marker = lstatSync(markerPath);
|
|
45
|
+
if (!marker.isFile() || marker.isSymbolicLink())
|
|
46
|
+
return { status: 'conflict', path, reason: 'unmanaged' };
|
|
47
|
+
const data = JSON.parse(readFileSync(markerPath, 'utf8'));
|
|
48
|
+
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
49
|
+
return { status: 'conflict', path, reason: 'unmanaged' };
|
|
50
|
+
const skillIdInMarker = data.skillId;
|
|
51
|
+
return skillIdInMarker === skillId
|
|
52
|
+
? { status: 'managed', path }
|
|
53
|
+
: { status: 'conflict', path, reason: typeof skillIdInMarker === 'string' ? 'foreign' : 'unmanaged' };
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error.code === 'ENOENT' || error instanceof SyntaxError)
|
|
57
|
+
return { status: 'conflict', path, reason: 'unmanaged' };
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** name/description do frontmatter; valida só o formato mínimo (o servidor valida o resto). */
|
|
62
|
+
export function frontmatter(md) {
|
|
63
|
+
const block = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(md)?.[1];
|
|
64
|
+
if (!block)
|
|
65
|
+
return null;
|
|
66
|
+
const field = (key) => {
|
|
67
|
+
const raw = new RegExp(`^${key}:[ \\t]*(.+)$`, 'm').exec(block)?.[1]?.trim();
|
|
68
|
+
return raw ? raw.replace(/^(['"])(.*)\1$/, '$2').trim() : '';
|
|
69
|
+
};
|
|
70
|
+
const name = field('name'), description = field('description');
|
|
71
|
+
return name && description ? { name, description } : null;
|
|
72
|
+
}
|
|
73
|
+
function isText(bytes) {
|
|
74
|
+
if (bytes.includes(0))
|
|
75
|
+
return false;
|
|
76
|
+
try {
|
|
77
|
+
new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Lê uma pasta de skill para publicar; symlinks e pastas de ferramentas ficam de fora. */
|
|
85
|
+
export function readSourceDir(directory) {
|
|
86
|
+
const base = resolve(directory);
|
|
87
|
+
if (!lstatSync(base).isDirectory())
|
|
88
|
+
throw new SkillLocalError('source_invalid', `Não é uma pasta: ${directory}`);
|
|
89
|
+
const files = [];
|
|
90
|
+
let total = 0;
|
|
91
|
+
const walk = (dir) => {
|
|
92
|
+
for (const name of readdirSync(dir).sort()) {
|
|
93
|
+
if (IGNORED.has(name))
|
|
94
|
+
continue;
|
|
95
|
+
const full = join(dir, name);
|
|
96
|
+
const stat = lstatSync(full);
|
|
97
|
+
if (stat.isSymbolicLink())
|
|
98
|
+
continue;
|
|
99
|
+
if (stat.isDirectory()) {
|
|
100
|
+
walk(full);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!stat.isFile())
|
|
104
|
+
continue;
|
|
105
|
+
if (files.length >= SKILL_MAX_FILES)
|
|
106
|
+
throw new SkillLocalError('payload_too_large', `A skill passa de ${SKILL_MAX_FILES} arquivos`);
|
|
107
|
+
if (stat.size > SKILL_FILE_MAX_BYTES)
|
|
108
|
+
throw new SkillLocalError('payload_too_large', `Arquivo acima de 4 MiB: ${relative(base, full)}`);
|
|
109
|
+
total += stat.size;
|
|
110
|
+
if (total > SKILL_TOTAL_MAX_BYTES)
|
|
111
|
+
throw new SkillLocalError('payload_too_large', 'Skill acima de 32 MiB');
|
|
112
|
+
const bytes = readFileSync(full);
|
|
113
|
+
const text = isText(bytes);
|
|
114
|
+
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 });
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
walk(base);
|
|
118
|
+
return files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
119
|
+
}
|
|
120
|
+
function safePath(path) {
|
|
121
|
+
if (!path || path.startsWith('/') || path.includes('\\') || path.includes('\0') || /^[A-Za-z]:/.test(path))
|
|
122
|
+
throw new SkillLocalError('skill_package_invalid', `Caminho inválido no pacote: ${path}`);
|
|
123
|
+
const segments = path.split('/');
|
|
124
|
+
if (segments.some(s => s === '' || s === '.' || s === '..'))
|
|
125
|
+
throw new SkillLocalError('skill_package_invalid', `Caminho inválido no pacote: ${path}`);
|
|
126
|
+
return path;
|
|
127
|
+
}
|
|
128
|
+
/** Leitor ustar mínimo: só arquivos regulares; link, dispositivo ou caminho perigoso recusa o pacote inteiro. */
|
|
129
|
+
export function readTarGz(archive) {
|
|
130
|
+
let tar;
|
|
131
|
+
try {
|
|
132
|
+
tar = gunzipSync(archive, { maxOutputLength: EXPANDED_MAX_BYTES });
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
throw new SkillLocalError('skill_package_invalid', 'Pacote corrompido');
|
|
136
|
+
}
|
|
137
|
+
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'); };
|
|
138
|
+
const entries = [];
|
|
139
|
+
let offset = 0;
|
|
140
|
+
while (offset + 512 <= tar.length) {
|
|
141
|
+
const header = tar.subarray(offset, offset + 512);
|
|
142
|
+
if (header.every(b => b === 0))
|
|
143
|
+
break;
|
|
144
|
+
const type = String.fromCharCode(header[156]);
|
|
145
|
+
if (type !== '0' && type !== '\0')
|
|
146
|
+
throw new SkillLocalError('skill_package_invalid', 'Pacote contém entrada que não é arquivo regular');
|
|
147
|
+
const name = field(header, 0, 100), prefix = field(header, 345, 155);
|
|
148
|
+
const size = parseInt(field(header, 124, 12).trim() || '0', 8), mode = parseInt(field(header, 100, 8).trim() || '0', 8);
|
|
149
|
+
if (!Number.isSafeInteger(size) || size < 0 || offset + 512 + size > tar.length)
|
|
150
|
+
throw new SkillLocalError('skill_package_invalid', 'Pacote corrompido');
|
|
151
|
+
if (entries.length > SKILL_MAX_FILES + 2)
|
|
152
|
+
throw new SkillLocalError('skill_package_invalid', 'Pacote com entradas demais');
|
|
153
|
+
entries.push({ path: safePath(prefix ? `${prefix}/${name}` : name), bytes: Buffer.from(tar.subarray(offset + 512, offset + 512 + size)), mode });
|
|
154
|
+
offset += 512 + Math.ceil(size / 512) * 512;
|
|
155
|
+
}
|
|
156
|
+
return entries;
|
|
157
|
+
}
|
|
158
|
+
/** Confere o manifest.json do formato do Vault contra os bytes: nada fora dele, nada faltando, digest correto. */
|
|
159
|
+
function verifiedFiles(entries) {
|
|
160
|
+
const integrity = (detail) => new SkillLocalError('skill_install_integrity', `Falha de integridade do pacote: ${detail}`);
|
|
161
|
+
const byPath = new Map(entries.map(e => [e.path, e]));
|
|
162
|
+
if (SKILL_BUNDLE_MANIFEST_PATHS.some(path => byPath.has(path)))
|
|
163
|
+
throw new SkillLocalError('skill_install_unsupported', 'Pacotes com várias skills (bundle) devem ser instalados pelo Vault Desktop');
|
|
164
|
+
const raw = byPath.get('manifest.json');
|
|
165
|
+
if (!raw)
|
|
166
|
+
throw integrity('manifest.json ausente');
|
|
167
|
+
let manifest;
|
|
168
|
+
try {
|
|
169
|
+
manifest = JSON.parse(raw.bytes.toString('utf8'));
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
throw integrity('manifest.json inválido');
|
|
173
|
+
}
|
|
174
|
+
const files = Array.isArray(manifest.files) ? manifest.files : [];
|
|
175
|
+
if (!files.some(f => f.path === 'SKILL.md'))
|
|
176
|
+
throw integrity('SKILL.md ausente');
|
|
177
|
+
const digest = createHash('sha256').update(JSON.stringify(files.map(f => ({ path: f.path, executable: f.executable, classification: f.classification, identitySha256: f.identitySha256 })))).digest('hex');
|
|
178
|
+
if (digest !== manifest.packageDigest)
|
|
179
|
+
throw integrity('digest do manifesto');
|
|
180
|
+
if (entries.length !== files.length + 1)
|
|
181
|
+
throw integrity('entradas fora do manifesto');
|
|
182
|
+
return files.map(file => {
|
|
183
|
+
const entry = byPath.get(safePath(file.path));
|
|
184
|
+
if (!entry || entry.bytes.length !== file.size || sha256(entry.bytes) !== file.sha256)
|
|
185
|
+
throw integrity(file.path);
|
|
186
|
+
return { file, entry };
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
export function installSkillArchive(input) {
|
|
190
|
+
if (sha256(input.bytes) !== input.expectedSha256)
|
|
191
|
+
throw new SkillLocalError('skill_install_integrity', 'Falha de integridade do pacote: sha256');
|
|
192
|
+
const files = verifiedFiles(readTarGz(input.bytes));
|
|
193
|
+
const root = resolve(input.root);
|
|
194
|
+
const destination = inspectSkillDestination(root, input.dirName, input.skillId);
|
|
195
|
+
const target = destination.path;
|
|
196
|
+
if (destination.status === 'conflict')
|
|
197
|
+
throw new SkillLocalError('skill_install_conflict', destination.reason === 'foreign'
|
|
198
|
+
? `${target} pertence a outra skill Vault; nada foi alterado`
|
|
199
|
+
: `${target} já existe e não foi instalada pelo Vault; nada foi alterado`);
|
|
200
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
201
|
+
const token = `${process.pid}-${randomBytes(4).toString('hex')}`;
|
|
202
|
+
const staging = join(root, `.${input.dirName}.vault-tmp-${token}`);
|
|
203
|
+
const previous = join(root, `.${input.dirName}.vault-old-${token}`);
|
|
204
|
+
try {
|
|
205
|
+
for (const { file, entry } of files) {
|
|
206
|
+
const destination = resolve(staging, file.path);
|
|
207
|
+
if (!destination.startsWith(staging + sep))
|
|
208
|
+
throw new SkillLocalError('skill_package_invalid', `Caminho inválido no pacote: ${file.path}`);
|
|
209
|
+
mkdirSync(dirname(destination), { recursive: true, mode: 0o700 });
|
|
210
|
+
writeFileSync(destination, entry.bytes, { mode: file.executable ? 0o755 : 0o644 });
|
|
211
|
+
}
|
|
212
|
+
writeFileSync(join(staging, SKILL_MARKER), `${JSON.stringify({ skillId: input.skillId, versionId: input.versionId, installedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o644 });
|
|
213
|
+
const replacing = existsSync(target);
|
|
214
|
+
if (replacing)
|
|
215
|
+
renameSync(target, previous);
|
|
216
|
+
try {
|
|
217
|
+
renameSync(staging, target);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (replacing)
|
|
221
|
+
renameSync(previous, target);
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
if (replacing)
|
|
225
|
+
rmSync(previous, { recursive: true, force: true });
|
|
226
|
+
return { path: target };
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
rmSync(staging, { recursive: true, force: true });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** Skills instaladas pelo Vault nas raízes informadas. */
|
|
233
|
+
export function installedSkills(roots) {
|
|
234
|
+
const out = [];
|
|
235
|
+
for (const root of roots) {
|
|
236
|
+
if (!existsSync(root))
|
|
237
|
+
continue;
|
|
238
|
+
for (const dir of readdirSync(root).sort()) {
|
|
239
|
+
const marker = join(root, dir, SKILL_MARKER);
|
|
240
|
+
if (dir.startsWith('.') || !existsSync(marker))
|
|
241
|
+
continue;
|
|
242
|
+
try {
|
|
243
|
+
const data = JSON.parse(readFileSync(marker, 'utf8'));
|
|
244
|
+
if (typeof data.skillId === 'string' && typeof data.versionId === 'string')
|
|
245
|
+
out.push({ root, dir, skillId: data.skillId, versionId: data.versionId });
|
|
246
|
+
}
|
|
247
|
+
catch { /* marcador corrompido: tratado como não gerenciado */ }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
@@ -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;
|