@capekai/core 1.0.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/package.json +105 -0
- package/src/adapters/ai-sdk.ts +84 -0
- package/src/compaction/contracts.ts +82 -0
- package/src/compaction/executor.ts +161 -0
- package/src/compaction/policy.ts +318 -0
- package/src/compaction/recovery.ts +139 -0
- package/src/compaction/task.ts +540 -0
- package/src/configuration/contracts.ts +58 -0
- package/src/configuration/defaults.ts +27 -0
- package/src/configuration/runtime.ts +42 -0
- package/src/configuration/single-model.ts +75 -0
- package/src/context/assembler.ts +112 -0
- package/src/context/index.ts +2 -0
- package/src/context/sources.ts +119 -0
- package/src/context/workspace.ts +63 -0
- package/src/core/agent.ts +401 -0
- package/src/core/build-tools.ts +139 -0
- package/src/core/chat-handler.ts +858 -0
- package/src/core/error-handling.ts +18 -0
- package/src/core/fork.ts +103 -0
- package/src/core/interrupt.ts +192 -0
- package/src/core/message-utils.ts +261 -0
- package/src/core/model-utils.ts +149 -0
- package/src/core/part-utils.ts +88 -0
- package/src/core/provider-utils.ts +67 -0
- package/src/core/revert.ts +46 -0
- package/src/core/step-handlers.ts +157 -0
- package/src/core/stream/finalization.ts +65 -0
- package/src/core/stream/stream-config.ts +82 -0
- package/src/core/stream-handlers.ts +242 -0
- package/src/core/structured-output.ts +68 -0
- package/src/core/tool-builders/agent-tools.ts +71 -0
- package/src/core/tool-builders/external-tools.ts +179 -0
- package/src/core/tool-builders/types.ts +16 -0
- package/src/core/tool-builders/workspace-tools.ts +293 -0
- package/src/core/tool-capabilities.ts +65 -0
- package/src/goals/evaluator.ts +171 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/loop.ts +167 -0
- package/src/goals/service.ts +39 -0
- package/src/index.ts +10 -0
- package/src/internal/ask-authority.ts +29 -0
- package/src/internal/composition.ts +44 -0
- package/src/internal/configuration.ts +22 -0
- package/src/internal/execution.ts +108 -0
- package/src/internal/hosts.ts +64 -0
- package/src/internal/plugins.ts +71 -0
- package/src/internal/providers.ts +32 -0
- package/src/internal/sandbox.ts +19 -0
- package/src/internal/tools.ts +48 -0
- package/src/internal/workspace.ts +25 -0
- package/src/kernel/diagnostics.ts +249 -0
- package/src/kernel/errors.ts +120 -0
- package/src/kernel/events.ts +82 -0
- package/src/kernel/index.ts +72 -0
- package/src/kernel/kernel.ts +62 -0
- package/src/kernel/lifecycle.ts +72 -0
- package/src/kernel/plugin.ts +218 -0
- package/src/kernel/registry.ts +493 -0
- package/src/kernel/scope.ts +776 -0
- package/src/kernel/service-key.ts +19 -0
- package/src/kernel/types.ts +317 -0
- package/src/memory/index.ts +2 -0
- package/src/memory/memory-tool.ts +75 -0
- package/src/memory/registry.ts +172 -0
- package/src/permission/ask-user-api.ts +70 -0
- package/src/permission/contracts.ts +135 -0
- package/src/permission/permission-request-manager.ts +58 -0
- package/src/permission/policy.ts +277 -0
- package/src/permission/runtime.ts +612 -0
- package/src/plugins/compaction-policy.ts +46 -0
- package/src/plugins/compose.ts +171 -0
- package/src/plugins/context-sections.ts +246 -0
- package/src/plugins/default-agent-driver.ts +14 -0
- package/src/plugins/facade-plugins.ts +129 -0
- package/src/plugins/goal-domain.ts +82 -0
- package/src/plugins/legacy-system-message.ts +152 -0
- package/src/plugins/loaded-tools.ts +23 -0
- package/src/plugins/memory-domain.ts +264 -0
- package/src/plugins/orchestrator-session.ts +29 -0
- package/src/plugins/permission-policy.ts +49 -0
- package/src/plugins/retry-policy.ts +28 -0
- package/src/plugins/scheduler-domain.ts +192 -0
- package/src/plugins/service-keys.ts +294 -0
- package/src/plugins/session-search-domain.ts +238 -0
- package/src/plugins/skills-domain.ts +272 -0
- package/src/plugins/subagent-domain.ts +287 -0
- package/src/plugins/tool-catalog.ts +78 -0
- package/src/plugins/tool-output-policy.ts +52 -0
- package/src/plugins/value-plugins.ts +150 -0
- package/src/plugins/workflow-domain.ts +198 -0
- package/src/plugins/workspace-policy.ts +37 -0
- package/src/providers/registry.ts +63 -0
- package/src/providers/types.ts +44 -0
- package/src/retry/policy.ts +282 -0
- package/src/retry/stream-chat.ts +312 -0
- package/src/runtime/agent-runtime.ts +83 -0
- package/src/runtime/default-agent-driver.ts +23 -0
- package/src/runtime/domain-tool-source.ts +156 -0
- package/src/runtime/events.ts +61 -0
- package/src/runtime/host-dependencies.ts +71 -0
- package/src/runtime/host-guidance.ts +22 -0
- package/src/runtime/host-layout.ts +23 -0
- package/src/runtime/host.ts +129 -0
- package/src/runtime/standalone-host.ts +118 -0
- package/src/sandbox/controller.ts +204 -0
- package/src/sandbox/model.ts +305 -0
- package/src/sandbox/provider.ts +53 -0
- package/src/sandbox/types.ts +110 -0
- package/src/scheduler/host.ts +22 -0
- package/src/scheduler/scheduler-tool.ts +172 -0
- package/src/session-search/host.ts +56 -0
- package/src/session-search/index.ts +23 -0
- package/src/session-search/session-search-tool.ts +151 -0
- package/src/skills/index.ts +3 -0
- package/src/skills/registry.ts +63 -0
- package/src/skills/skill-manage-tool.ts +205 -0
- package/src/skills/skill-tool.ts +42 -0
- package/src/storage/contracts.ts +159 -0
- package/src/storage/memory.ts +321 -0
- package/src/storage/options.ts +75 -0
- package/src/storage/runtime.ts +115 -0
- package/src/storage/sqlite-tool-output-artifacts.ts +106 -0
- package/src/storage/sqlite.ts +321 -0
- package/src/storage/tool-output-artifacts.ts +75 -0
- package/src/storage.ts +31 -0
- package/src/subagent/child-session.ts +282 -0
- package/src/subagent/guidance.ts +8 -0
- package/src/subagent/policy.ts +198 -0
- package/src/subagent/task-tool.ts +584 -0
- package/src/tool-output/contracts.ts +111 -0
- package/src/tool-output/policy.ts +410 -0
- package/src/tool.ts +1 -0
- package/src/tools/executor.ts +258 -0
- package/src/tools/install-manifest.ts +40 -0
- package/src/tools/llm-api.ts +77 -0
- package/src/tools/registry.ts +206 -0
- package/src/tools/tool-artifact.ts +182 -0
- package/src/tools/tool-source.ts +53 -0
- package/src/utils/errors.ts +334 -0
- package/src/utils/strip-visualization.ts +50 -0
- package/src/workflow/decomposer.ts +139 -0
- package/src/workflow/execution.ts +523 -0
- package/src/workflow/orchestrator-session.ts +161 -0
- package/src/workflow/synthesizer.ts +130 -0
- package/src/workspace/contracts.ts +135 -0
- package/src/workspace/policy.ts +327 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import type { InstallManifest } from '@capekai/tool';
|
|
4
|
+
|
|
5
|
+
export type { InstallManifest };
|
|
6
|
+
|
|
7
|
+
const INSTALL_MANIFEST = '.install-manifest.json';
|
|
8
|
+
|
|
9
|
+
function isInstallManifest(data: unknown): data is InstallManifest {
|
|
10
|
+
if (typeof data !== 'object' || data === null) return false;
|
|
11
|
+
const value = data as Record<string, unknown>;
|
|
12
|
+
return typeof value.toolName === 'string'
|
|
13
|
+
&& (value.toolVersion === null || typeof value.toolVersion === 'string')
|
|
14
|
+
&& typeof value.installedAt === 'string'
|
|
15
|
+
&& typeof value.entry === 'string'
|
|
16
|
+
&& typeof value.runtime === 'string';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readInstallManifest(toolsDir: string, toolName: string): InstallManifest | null {
|
|
20
|
+
const manifestPath = join(toolsDir, toolName, INSTALL_MANIFEST);
|
|
21
|
+
if (!existsSync(manifestPath)) return null;
|
|
22
|
+
try {
|
|
23
|
+
const parsed: unknown = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
24
|
+
if (!isInstallManifest(parsed)) {
|
|
25
|
+
console.warn(`[install-manifest] Invalid manifest for ${toolName}: missing required fields`);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return parsed;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function writeInstallManifest(toolDir: string, manifest: InstallManifest): void {
|
|
35
|
+
writeFileSync(getManifestPath(toolDir), JSON.stringify(manifest, null, 2));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getManifestPath(toolDir: string): string {
|
|
39
|
+
return join(toolDir, INSTALL_MANIFEST);
|
|
40
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { streamText, streamObject } from 'ai';
|
|
2
|
+
import { jsonSchema } from 'ai';
|
|
3
|
+
import type { ModelMessage } from 'ai';
|
|
4
|
+
import type { LlmApi } from '@capekai/tool'
|
|
5
|
+
import { LlmTextOptions, LlmStructuredOptions, LlmImage } from '@capekai/tool';
|
|
6
|
+
import { getModelWithMetadata } from '../core/model-utils';
|
|
7
|
+
|
|
8
|
+
export function createLlmApi(defaultModelId?: string, defaultProviderId?: string, sessionId?: string): LlmApi {
|
|
9
|
+
return {
|
|
10
|
+
async generateText(options: LlmTextOptions): Promise<string> {
|
|
11
|
+
const { model, omitMaxOutputTokens, providerOptions, useProviderInstructions } = await getModelWithMetadata({
|
|
12
|
+
modelId: options.model || defaultModelId,
|
|
13
|
+
providerId: options.provider || defaultProviderId,
|
|
14
|
+
systemPrompt: options.system,
|
|
15
|
+
sessionId,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
let userContent: string | Array<{ type: 'text'; text: string } | { type: 'image'; image: string | Uint8Array; mimeType: string }> = options.prompt;
|
|
19
|
+
|
|
20
|
+
if (options.image) {
|
|
21
|
+
const images: LlmImage[] = Array.isArray(options.image) ? options.image : [options.image];
|
|
22
|
+
userContent = [
|
|
23
|
+
{ type: 'text', text: options.prompt },
|
|
24
|
+
...images.map((img) => ({
|
|
25
|
+
type: 'image' as const,
|
|
26
|
+
image: img.data,
|
|
27
|
+
mimeType: img.mediaType,
|
|
28
|
+
})),
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const stream = streamText({
|
|
33
|
+
model,
|
|
34
|
+
system: (!options.system || useProviderInstructions) ? undefined : options.system,
|
|
35
|
+
messages: [{ role: 'user' as const, content: userContent }] as ModelMessage[],
|
|
36
|
+
maxOutputTokens: omitMaxOutputTokens ? undefined : (options.maxTokens ?? 4096),
|
|
37
|
+
providerOptions: providerOptions as unknown as Parameters<typeof streamText>[0]['providerOptions'],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return stream.text;
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
async generateStructured<T = unknown>(options: LlmStructuredOptions): Promise<T> {
|
|
44
|
+
const { model, omitMaxOutputTokens, providerOptions, useProviderInstructions } = await getModelWithMetadata({
|
|
45
|
+
modelId: options.model || defaultModelId,
|
|
46
|
+
providerId: options.provider || defaultProviderId,
|
|
47
|
+
systemPrompt: options.system,
|
|
48
|
+
sessionId,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
let userContent: string | Array<{ type: 'text'; text: string } | { type: 'image'; image: string | Uint8Array; mimeType: string }> = options.prompt;
|
|
52
|
+
|
|
53
|
+
if (options.image) {
|
|
54
|
+
const images: LlmImage[] = Array.isArray(options.image) ? options.image : [options.image];
|
|
55
|
+
userContent = [
|
|
56
|
+
{ type: 'text', text: options.prompt },
|
|
57
|
+
...images.map((img) => ({
|
|
58
|
+
type: 'image' as const,
|
|
59
|
+
image: img.data,
|
|
60
|
+
mimeType: img.mediaType,
|
|
61
|
+
})),
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const result = streamObject({
|
|
66
|
+
model,
|
|
67
|
+
system: (!options.system || useProviderInstructions) ? undefined : options.system,
|
|
68
|
+
messages: [{ role: 'user' as const, content: userContent }] as ModelMessage[],
|
|
69
|
+
schema: jsonSchema(options.schema),
|
|
70
|
+
maxOutputTokens: omitMaxOutputTokens ? undefined : (options.maxTokens ?? 4096),
|
|
71
|
+
providerOptions: providerOptions as unknown as Parameters<typeof streamObject>[0]['providerOptions'],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return result.object as Promise<T>;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { readdir } from 'fs/promises';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { watch } from 'fs';
|
|
5
|
+
import { basename, dirname, join, resolve, relative } from 'path';
|
|
6
|
+
import type { ToolDefinition } from '@capekai/tool'
|
|
7
|
+
import { LoadedTool } from '@capekai/tool';
|
|
8
|
+
import { readInstallManifest } from './install-manifest';
|
|
9
|
+
|
|
10
|
+
export interface ToolRegistryResolver {
|
|
11
|
+
get(name: string): LoadedTool | null;
|
|
12
|
+
list(): LoadedTool[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const toolsCache: Map<string, LoadedTool> = new Map();
|
|
16
|
+
const scopedResolver = new AsyncLocalStorage<ToolRegistryResolver>();
|
|
17
|
+
let lastScanTime = 0;
|
|
18
|
+
const CACHE_TTL = 60000;
|
|
19
|
+
|
|
20
|
+
let watcher: ReturnType<typeof watch> | null = null;
|
|
21
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
22
|
+
let watcherToolsPath: string | null = null;
|
|
23
|
+
let defaultToolsPath: string | null = null;
|
|
24
|
+
|
|
25
|
+
export function withToolRegistryResolver<T>(resolver: ToolRegistryResolver, callback: () => T): T {
|
|
26
|
+
return scopedResolver.run(resolver, callback);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** True when an ambient scoped tool resolver is installed. The legacy
|
|
30
|
+
* unscoped execution path keeps its unconditional builtin-tool injection;
|
|
31
|
+
* scoped compositions resolve every tool, including retrieve-tool-output,
|
|
32
|
+
* through their resolver instead. */
|
|
33
|
+
export function hasScopedToolRegistryResolver(): boolean {
|
|
34
|
+
return scopedResolver.getStore() !== undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function configureToolsPath(path?: string): void {
|
|
38
|
+
defaultToolsPath = path ? resolve(path) : null;
|
|
39
|
+
clearCache();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getDefaultToolsPath(): string | null {
|
|
43
|
+
return defaultToolsPath;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function loadToolModule(toolDir: string): Promise<LoadedTool | null> {
|
|
47
|
+
const toolName = basename(toolDir);
|
|
48
|
+
const toolsBasePath = dirname(toolDir);
|
|
49
|
+
const manifest = readInstallManifest(toolsBasePath, toolName);
|
|
50
|
+
let modulePath: string | null = null;
|
|
51
|
+
|
|
52
|
+
if (manifest?.entry) {
|
|
53
|
+
const manifestEntryPath = join(toolDir, manifest.entry);
|
|
54
|
+
if (existsSync(manifestEntryPath)) modulePath = manifestEntryPath;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!modulePath) {
|
|
58
|
+
const toolJsPath = join(toolDir, 'tool.js');
|
|
59
|
+
const toolTsPath = join(toolDir, 'tool.ts');
|
|
60
|
+
if (existsSync(toolJsPath)) modulePath = toolJsPath;
|
|
61
|
+
else if (existsSync(toolTsPath)) modulePath = toolTsPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!modulePath) return null;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const module = await import(modulePath);
|
|
68
|
+
if (!module.definition || typeof module.execute !== 'function') {
|
|
69
|
+
console.warn(`Tool at ${toolDir} missing required exports (definition, execute)`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const definition: ToolDefinition = module.definition;
|
|
73
|
+
if (!definition.name || !definition.inputSchema) {
|
|
74
|
+
console.warn(`Tool at ${toolDir} has invalid definition (missing name or inputSchema)`);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return { definition, execute: module.execute, path: toolDir };
|
|
78
|
+
} catch (error: unknown) {
|
|
79
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
+
if (message.includes('Cannot find module')) console.warn(`Tool at ${toolDir} has missing dependencies: ${message}`);
|
|
81
|
+
else if (message.includes('SyntaxError') || message.includes('Unexpected')) console.warn(`Tool at ${toolDir} has a syntax/load error: ${message}`);
|
|
82
|
+
else console.warn(`Failed to load tool module at ${toolDir}:`, error);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function invalidateToolAtPath(filePath: string): void {
|
|
88
|
+
if (!watcherToolsPath) return;
|
|
89
|
+
const relativePath = relative(watcherToolsPath, filePath);
|
|
90
|
+
const toolDir = relativePath.split(/[\\/]/)[0];
|
|
91
|
+
if (!toolDir || toolDir === '.' || toolDir === '..') return;
|
|
92
|
+
const toolPath = join(watcherToolsPath, toolDir);
|
|
93
|
+
const key = [...toolsCache.keys()].find((name) => toolsCache.get(name)?.path === toolPath);
|
|
94
|
+
if (key) toolsCache.delete(key);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function scheduleInvalidation(filePath: string): void {
|
|
98
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
99
|
+
debounceTimer = setTimeout(() => {
|
|
100
|
+
invalidateToolAtPath(filePath);
|
|
101
|
+
debounceTimer = null;
|
|
102
|
+
}, 100);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function watchTools(toolsPath: string | null = getDefaultToolsPath()): void {
|
|
106
|
+
if (!toolsPath) return;
|
|
107
|
+
if (watcher) stopWatching();
|
|
108
|
+
const absoluteToolsPath = resolve(toolsPath);
|
|
109
|
+
watcherToolsPath = absoluteToolsPath;
|
|
110
|
+
try {
|
|
111
|
+
watcher = watch(absoluteToolsPath, { recursive: true }, (_event, filename) => {
|
|
112
|
+
if (filename) scheduleInvalidation(join(absoluteToolsPath, filename));
|
|
113
|
+
});
|
|
114
|
+
watcher.on('error', (error) => console.warn(`Tool watcher error: ${error}`));
|
|
115
|
+
} catch {
|
|
116
|
+
console.warn(`Failed to start tool watcher for: ${absoluteToolsPath}`);
|
|
117
|
+
watcherToolsPath = null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function stopWatching(): void {
|
|
122
|
+
if (debounceTimer) {
|
|
123
|
+
clearTimeout(debounceTimer);
|
|
124
|
+
debounceTimer = null;
|
|
125
|
+
}
|
|
126
|
+
if (watcher) {
|
|
127
|
+
watcher.close();
|
|
128
|
+
watcher = null;
|
|
129
|
+
}
|
|
130
|
+
watcherToolsPath = null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function scanTools(toolsPath: string | null = getDefaultToolsPath()): Promise<LoadedTool[]> {
|
|
134
|
+
const tools: LoadedTool[] = [];
|
|
135
|
+
if (toolsPath) {
|
|
136
|
+
const absoluteToolsPath = resolve(toolsPath);
|
|
137
|
+
try {
|
|
138
|
+
const entries = await readdir(absoluteToolsPath, { withFileTypes: true });
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
if (!entry.isDirectory() || entry.name.endsWith('.staging') || entry.name.endsWith('.previous')) continue;
|
|
141
|
+
const loaded = await loadToolModule(join(absoluteToolsPath, entry.name));
|
|
142
|
+
if (loaded) tools.push(loaded);
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
console.warn(`Tools directory not found: ${absoluteToolsPath}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
toolsCache.clear();
|
|
150
|
+
for (const loaded of tools) toolsCache.set(loaded.definition.name, loaded);
|
|
151
|
+
lastScanTime = Date.now();
|
|
152
|
+
return tools;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let ensureScannedPromise: Promise<void> | null = null;
|
|
156
|
+
|
|
157
|
+
function ensureScanned(): Promise<void> {
|
|
158
|
+
if (Date.now() - lastScanTime < CACHE_TTL) return Promise.resolve();
|
|
159
|
+
if (ensureScannedPromise === null) {
|
|
160
|
+
ensureScannedPromise = scanTools().then(() => undefined).finally(() => {
|
|
161
|
+
ensureScannedPromise = null;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return ensureScannedPromise;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function getTool(name: string): Promise<LoadedTool | null> {
|
|
168
|
+
const resolver = scopedResolver.getStore();
|
|
169
|
+
if (resolver) return resolver.get(name);
|
|
170
|
+
await ensureScanned();
|
|
171
|
+
return toolsCache.get(name) ?? null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function listTools(): Promise<ToolDefinition[]> {
|
|
175
|
+
const resolver = scopedResolver.getStore();
|
|
176
|
+
if (resolver) return resolver.list().map((loaded) => loaded.definition);
|
|
177
|
+
await ensureScanned();
|
|
178
|
+
return [...toolsCache.values()].map((loaded) => loaded.definition);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Synchronous cache read of one installed tool. Ignores any ambient
|
|
182
|
+
* scoped resolver: this is the dir-scan branch of `getTool`, for
|
|
183
|
+
* resolver implementations that layer installed tools beneath their
|
|
184
|
+
* own contributions. Returns null when the cache has not been scanned
|
|
185
|
+
* (or the name is unknown); callers own ensuring a scan happened. */
|
|
186
|
+
export function getInstalledTool(name: string): LoadedTool | null {
|
|
187
|
+
return toolsCache.get(name) ?? null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Synchronous snapshot of the installed-tools cache. Ignores any
|
|
191
|
+
* ambient scoped resolver, like `getInstalledTool`. Empty when the
|
|
192
|
+
* cache has not been scanned. */
|
|
193
|
+
export function listInstalledTools(): LoadedTool[] {
|
|
194
|
+
return [...toolsCache.values()];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** True when the installed-tools cache is empty because no scan has
|
|
198
|
+
* run yet (as opposed to a genuinely empty tools directory). */
|
|
199
|
+
export function hasUnscannedToolCache(): boolean {
|
|
200
|
+
return lastScanTime === 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function clearCache(): void {
|
|
204
|
+
toolsCache.clear();
|
|
205
|
+
lastScanTime = 0;
|
|
206
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { createReadStream, existsSync, mkdirSync, readdirSync } from 'fs';
|
|
2
|
+
import { join, basename, dirname } from 'path';
|
|
3
|
+
import * as tar from 'tar';
|
|
4
|
+
import { BlobReader, ZipReader, BlobWriter } from '@zip.js/zip.js';
|
|
5
|
+
import { createHash } from 'crypto';
|
|
6
|
+
|
|
7
|
+
export class ArtifactError extends Error {
|
|
8
|
+
constructor(message: string, public readonly stage: string) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'ArtifactError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DownloadResult {
|
|
15
|
+
archivePath: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function downloadArtifact(
|
|
19
|
+
url: string,
|
|
20
|
+
destDir: string,
|
|
21
|
+
): Promise<DownloadResult> {
|
|
22
|
+
mkdirSync(destDir, { recursive: true });
|
|
23
|
+
|
|
24
|
+
const urlPath = new URL(url).pathname;
|
|
25
|
+
const archiveName = basename(urlPath) || 'artifact';
|
|
26
|
+
const archivePath = join(destDir, archiveName);
|
|
27
|
+
|
|
28
|
+
let response: Response;
|
|
29
|
+
try {
|
|
30
|
+
response = await fetch(url);
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new ArtifactError(
|
|
33
|
+
`HTTP ${response.status}: ${response.statusText}`,
|
|
34
|
+
'download',
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const buffer = await response.arrayBuffer();
|
|
38
|
+
await Bun.write(archivePath, buffer);
|
|
39
|
+
} catch (err: unknown) {
|
|
40
|
+
if (err instanceof ArtifactError) throw err;
|
|
41
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
42
|
+
throw new ArtifactError(`Download failed: ${message}`, 'download');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { archivePath };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function verifyChecksum(
|
|
49
|
+
archivePath: string,
|
|
50
|
+
expectedSha256: string | undefined,
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
if (!expectedSha256) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (expectedSha256.trim() === '') {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const hash = createHash('sha256');
|
|
61
|
+
const stream = createReadStream(archivePath);
|
|
62
|
+
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
stream.on('data', (data) => hash.update(data));
|
|
65
|
+
stream.on('end', () => {
|
|
66
|
+
const actual = hash.digest('hex');
|
|
67
|
+
if (actual !== expectedSha256) {
|
|
68
|
+
reject(
|
|
69
|
+
new ArtifactError(
|
|
70
|
+
`Checksum mismatch: expected ${expectedSha256}, got ${actual}`,
|
|
71
|
+
'checksum',
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
} else {
|
|
75
|
+
resolve();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
stream.on('error', (err) =>
|
|
79
|
+
reject(new ArtifactError(`Checksum read failed: ${err.message}`, 'checksum')),
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function extractArtifact(
|
|
85
|
+
archivePath: string,
|
|
86
|
+
destDir: string,
|
|
87
|
+
): Promise<string> {
|
|
88
|
+
const lower = archivePath.toLowerCase();
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
if (lower.endsWith('.tar.gz') || lower.endsWith('.tgz')) {
|
|
92
|
+
await extractTarGz(archivePath, destDir);
|
|
93
|
+
} else if (lower.endsWith('.zip')) {
|
|
94
|
+
await extractZip(archivePath, destDir);
|
|
95
|
+
} else {
|
|
96
|
+
throw new ArtifactError(
|
|
97
|
+
`Unsupported archive format: ${archivePath}`,
|
|
98
|
+
'extract',
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
} catch (err: unknown) {
|
|
102
|
+
if (err instanceof ArtifactError) throw err;
|
|
103
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
104
|
+
throw new ArtifactError(`Extraction failed: ${message}`, 'extract');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const extractedRoot = findExtractedRoot(destDir);
|
|
108
|
+
return extractedRoot;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function extractTarGz(tarPath: string, destDir: string): Promise<void> {
|
|
112
|
+
await tar.x({
|
|
113
|
+
file: tarPath,
|
|
114
|
+
cwd: destDir,
|
|
115
|
+
strip: 0,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function extractZip(zipPath: string, destDir: string): Promise<void> {
|
|
120
|
+
const file = Bun.file(zipPath);
|
|
121
|
+
const blob = new Blob([await file.arrayBuffer()]);
|
|
122
|
+
const reader = new ZipReader(new BlobReader(blob));
|
|
123
|
+
|
|
124
|
+
const entries = await reader.getEntries();
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
const entryPath = join(destDir, entry.filename);
|
|
127
|
+
|
|
128
|
+
if (entry.directory) {
|
|
129
|
+
mkdirSync(entryPath, { recursive: true });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
mkdirSync(dirname(entryPath), { recursive: true });
|
|
134
|
+
const writer = new BlobWriter();
|
|
135
|
+
await entry.getData(writer);
|
|
136
|
+
const data = await writer.getData();
|
|
137
|
+
await Bun.write(entryPath, data);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await reader.close();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function findExtractedRoot(destDir: string): string {
|
|
144
|
+
const entries = readdirSync(destDir, { withFileTypes: true });
|
|
145
|
+
const nonMetaEntries = entries.filter(
|
|
146
|
+
(e) => !e.name.startsWith('.') && e.name !== 'archive' && !e.name.endsWith('.tar.gz') && !e.name.endsWith('.tgz') && !e.name.endsWith('.zip'),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
if (nonMetaEntries.length === 1 && nonMetaEntries[0].isDirectory()) {
|
|
150
|
+
return join(destDir, nonMetaEntries[0].name);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return destDir;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface ArtifactValidation {
|
|
157
|
+
entrypoint: string;
|
|
158
|
+
hasPackageJson: boolean;
|
|
159
|
+
hasVersion: boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function validateArtifactStructure(
|
|
163
|
+
extractedRoot: string,
|
|
164
|
+
entry: string,
|
|
165
|
+
): ArtifactValidation {
|
|
166
|
+
const entryPath = join(extractedRoot, entry);
|
|
167
|
+
if (!existsSync(entryPath)) {
|
|
168
|
+
throw new ArtifactError(
|
|
169
|
+
`Entrypoint not found: ${entry}`,
|
|
170
|
+
'validate',
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const hasPackageJson = existsSync(join(extractedRoot, 'package.json'));
|
|
175
|
+
const hasVersion = existsSync(join(extractedRoot, 'VERSION'));
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
entrypoint: entryPath,
|
|
179
|
+
hasPackageJson,
|
|
180
|
+
hasVersion,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import type { Tool } from 'ai';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Per-workspace dynamic tool discovery seam. The workspacePath is a scoping
|
|
6
|
+
* key for discovery (for example, which MCP servers are configured for that
|
|
7
|
+
* workspace), not a directory tools are stored in: installed tool packages
|
|
8
|
+
* resolve through the tools directory registry, not through this contract.
|
|
9
|
+
*/
|
|
10
|
+
export interface WorkspaceToolDiscovery {
|
|
11
|
+
initializeWorkspace?(workspacePath: string): Promise<void>;
|
|
12
|
+
discoverTools?(workspacePath: string, sessionId?: string): Promise<Record<string, Tool>>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const defaultWorkspaceToolDiscovery: Required<WorkspaceToolDiscovery> = {
|
|
16
|
+
async initializeWorkspace(): Promise<void> {},
|
|
17
|
+
async discoverTools(): Promise<Record<string, Tool>> {
|
|
18
|
+
return {};
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let workspaceToolDiscovery: WorkspaceToolDiscovery = defaultWorkspaceToolDiscovery;
|
|
23
|
+
const scopedWorkspaceToolDiscovery = new AsyncLocalStorage<WorkspaceToolDiscovery>();
|
|
24
|
+
|
|
25
|
+
function activeWorkspaceToolDiscovery(): WorkspaceToolDiscovery {
|
|
26
|
+
return scopedWorkspaceToolDiscovery.getStore() ?? workspaceToolDiscovery;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function withWorkspaceToolDiscovery<T>(
|
|
30
|
+
discovery: WorkspaceToolDiscovery,
|
|
31
|
+
callback: () => T,
|
|
32
|
+
): T {
|
|
33
|
+
return scopedWorkspaceToolDiscovery.run(discovery, callback);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function configureWorkspaceToolDiscovery(discovery?: WorkspaceToolDiscovery): void {
|
|
37
|
+
workspaceToolDiscovery = discovery ?? defaultWorkspaceToolDiscovery;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getWorkspaceToolDiscovery(): WorkspaceToolDiscovery {
|
|
41
|
+
return activeWorkspaceToolDiscovery();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function initializeWorkspaceDiscovery(workspacePath: string): Promise<void> {
|
|
45
|
+
await activeWorkspaceToolDiscovery().initializeWorkspace?.(workspacePath);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function discoverWorkspaceTools(
|
|
49
|
+
workspacePath: string,
|
|
50
|
+
sessionId?: string,
|
|
51
|
+
): Promise<Record<string, Tool>> {
|
|
52
|
+
return await activeWorkspaceToolDiscovery().discoverTools?.(workspacePath, sessionId) ?? {};
|
|
53
|
+
}
|