Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/studio/mocks/node-polyfills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const promises = {
stat: async () => ({ isDirectory: () => false, isFile: () => false }),
mkdir: async () => {},
rm: async () => {},
access: async () => { throw new Error('ENOENT: no such file or directory (polyfill)'); },
};

// os polyfills
Expand Down Expand Up @@ -81,8 +82,8 @@ realpathSync.native = () => '';
export const constants = {};
export const lstat = async () => ({ isDirectory: () => false, isFile: () => false, isSymbolicLink: () => false });
export const stat = async () => ({ isDirectory: () => false, isFile: () => false, isSymbolicLink: () => false });
export const access = () => {};
export const accessSync = () => {};
export const access = async () => { throw new Error('ENOENT: no such file or directory (polyfill)'); };
export const accessSync = () => { throw new Error('ENOENT: no such file or directory (polyfill)'); };
export const mkdir = () => {};
export const mkdirSync = () => {};
export const rmdir = () => {};
Expand Down
20 changes: 18 additions & 2 deletions apps/studio/src/components/CodeExporter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Code2, Copy, Check, Database, Layout, Workflow, Bot, AppWindow } from 'lucide-react';
import { Code2, Copy, Check, Database, Layout, Workflow, Bot, AppWindow, Wrench } from 'lucide-react';

// ─── Types ──────────────────────────────────────────────────────────

type ExportType = 'object' | 'view' | 'flow' | 'agent' | 'app';
type ExportType = 'object' | 'view' | 'flow' | 'agent' | 'tool' | 'app';

export interface CodeExporterProps {
type: ExportType;
Expand All @@ -24,6 +24,7 @@ const TYPE_LABELS: Record<ExportType, { label: string; icon: React.ElementType }
view: { label: 'View', icon: Layout },
flow: { label: 'Flow', icon: Workflow },
agent: { label: 'Agent', icon: Bot },
tool: { label: 'Tool', icon: Wrench },
app: { label: 'App', icon: AppWindow },
};

Expand Down Expand Up @@ -135,11 +136,26 @@ function generateAppCode(def: Record<string, unknown>, name?: string): string {
return lines.join('\n');
}

function generateToolCode(def: Record<string, unknown>, name?: string): string {
const toolName = name || (def.name as string) || 'my_tool';
const lines = [
"import { defineStack } from '@objectstack/spec';",
'',
'export default defineStack({',
' tools: {',
` ${toolName}: ${formatValue(def, 2)},`,
' },',
'});',
];
return lines.join('\n');
}

const CODE_GENERATORS: Record<ExportType, (def: Record<string, unknown>, name?: string) => string> = {
object: generateObjectCode,
view: generateViewCode,
flow: generateFlowCode,
agent: generateAgentCode,
tool: generateToolCode,
app: generateAppCode,
};

Expand Down
10 changes: 9 additions & 1 deletion apps/studio/src/components/MetadataInspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Search, Copy, Check, ChevronRight, ChevronDown,
Zap, BarChart3, FileText, Workflow, Bot, Globe, BookOpen, Shield,
Zap, BarChart3, FileText, Workflow, Bot, Globe, BookOpen, Shield, Wrench,
type LucideIcon,
} from 'lucide-react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
Expand All @@ -34,8 +34,12 @@ const TYPE_ICONS: Record<string, LucideIcon> = {
dashboards: BarChart3,
reports: FileText,
flows: Workflow,
agent: Bot,
agents: Bot,
tool: Wrench,
tools: Wrench,
apis: Globe,
ragPipeline: BookOpen,
ragPipelines: BookOpen,
profiles: Shield,
sharingRules: Shield,
Expand All @@ -46,8 +50,12 @@ const TYPE_LABELS: Record<string, string> = {
dashboards: 'Dashboard',
reports: 'Report',
flows: 'Flow',
agent: 'Agent',
agents: 'Agent',
tool: 'Tool',
tools: 'Tool',
apis: 'API',
ragPipeline: 'RAG Pipeline',
ragPipelines: 'RAG Pipeline',
profiles: 'Profile',
sharingRules: 'Sharing Rule',
Expand Down
31 changes: 28 additions & 3 deletions apps/studio/src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
UserCog,
ChevronRight,
Settings,
Wrench,
type LucideIcon,
} from "lucide-react"
import { useState, useEffect, useCallback, useMemo } from "react"
Expand Down Expand Up @@ -94,7 +95,11 @@ const META_TYPE_HINTS: Record<string, { label: string; icon: LucideIcon }> = {
profiles: { label: 'Profiles', icon: Shield },
sharingRules: { label: 'Sharing Rules', icon: Shield },
policies: { label: 'Policies', icon: Shield },
agent: { label: 'Agents', icon: Bot },
agents: { label: 'Agents', icon: Bot },
tool: { label: 'Tools', icon: Wrench },
tools: { label: 'Tools', icon: Wrench },
ragPipeline: { label: 'RAG Pipelines', icon: BookOpen },
ragPipelines: { label: 'RAG Pipelines', icon: BookOpen },
apis: { label: 'APIs', icon: Globe },
connectors: { label: 'Connectors', icon: Link2 },
Expand Down Expand Up @@ -123,7 +128,7 @@ const PROTOCOL_GROUPS: ProtocolGroup[] = [
{ key: 'ui', label: 'UI', icon: AppWindow, types: ['app', 'apps', 'actions', 'views', 'pages', 'dashboards', 'reports', 'themes'] },
{ key: 'automation', label: 'Automation', icon: Workflow, types: ['flows', 'workflows', 'approvals', 'webhooks'] },
{ key: 'security', label: 'Security', icon: Shield, types: ['roles', 'permissions', 'profiles', 'sharingRules', 'policies'] },
{ key: 'ai', label: 'AI', icon: Bot, types: ['agents', 'ragPipelines'] },
{ key: 'ai', label: 'AI', icon: Bot, types: ['agent', 'agents', 'tool', 'tools', 'ragPipeline', 'ragPipelines'] },
{ key: 'api', label: 'API', icon: Globe, types: ['apis', 'connectors'] },
];

Expand Down Expand Up @@ -211,12 +216,32 @@ export function AppSidebar({
} else if (Array.isArray(typesResult)) {
types = typesResult as any;
}
setMetaTypes(types);

// Normalize types: prefer singular form (agent, tool) over plural (agents, tools)
// when both exist in PROTOCOL_GROUPS, since the singular REST endpoint merges
// SchemaRegistry items with MetadataService runtime items.
const groupSingulars = new Set(PROTOCOL_GROUPS.flatMap(g => g.types).filter(t => !t.endsWith('s')));
const normalized = types.map(t => {
if (t.endsWith('s') && groupSingulars.has(t.slice(0, -1))) {
return t.slice(0, -1); // agents → agent, tools → tool
}
return t;
});
// Also add group types that aren't covered at all by the server types
const groupTypes = PROTOCOL_GROUPS.flatMap(g => g.types);
const coveredSet = new Set(normalized);
const extraTypes = groupTypes.filter(t => {
if (coveredSet.has(t)) return false;
const variant = t.endsWith('s') ? t.slice(0, -1) : t + 's';
return !coveredSet.has(variant);
});
const allTypes = Array.from(new Set([...normalized, ...extraTypes]));
setMetaTypes(allTypes);

const packageId = selectedPackage?.manifest?.id;

const entries = await Promise.all(
types
allTypes
.filter(t => !HIDDEN_TYPES.has(t))
.map(async (type) => {
try {
Expand Down
44 changes: 41 additions & 3 deletions apps/studio/src/lib/create-broker-shim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,22 @@ export function createBrokerShim(kernel: any): BrokerShim {
}

if (service === 'metadata') {
// Get MetadataService for runtime-registered metadata (agents, tools, etc.)
const metadataService = kernel.context?.getService('metadata');

if (method === 'types') {
return { types: SchemaRegistry.getRegisteredTypes() };
// Combine types from both SchemaRegistry (static) and MetadataService (runtime)
const schemaTypes = SchemaRegistry.getRegisteredTypes();

// MetadataService exposes types through getRegisteredTypes() method
let runtimeTypes: string[] = [];
if (metadataService && typeof metadataService.getRegisteredTypes === 'function') {
runtimeTypes = await metadataService.getRegisteredTypes();
}

// Merge and deduplicate
const allTypes = Array.from(new Set([...schemaTypes, ...runtimeTypes]));
return { types: allTypes };
}
if (method === 'objects') {
const packageId = params.packageId;
Expand All @@ -206,9 +220,33 @@ export function createBrokerShim(kernel: any): BrokerShim {
}
return def || null;
}
// Generic metadata type: metadata.<type> → SchemaRegistry.listItems(type, packageId?)
// Generic metadata type: metadata.<type> → check both SchemaRegistry and MetadataService
const packageId = params.packageId;
const items = SchemaRegistry.listItems(method, packageId);

// Try SchemaRegistry first (static metadata from packages)
let items = SchemaRegistry.listItems(method, packageId);

// Also check MetadataService for runtime-registered metadata (agents, tools, etc.)
if (metadataService && typeof metadataService.list === 'function') {
try {
const runtimeItems = await metadataService.list(method);
if (runtimeItems && runtimeItems.length > 0) {
// Merge items, avoiding duplicates by name
const itemMap = new Map();
items.forEach((item: any) => itemMap.set(item.name, item));
runtimeItems.forEach((item: any) => {
if (item && typeof item === 'object' && 'name' in item) {
itemMap.set(item.name, item);
}
});
items = Array.from(itemMap.values());
}
} catch (err) {
// MetadataService.list might fail for unknown types, that's OK
console.debug(`[BrokerShim] MetadataService.list('${method}') failed:`, err);
}
}

if (items && items.length > 0) {
return { type: method, items };
}
Expand Down
27 changes: 27 additions & 0 deletions apps/studio/src/mocks/simulateBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,17 @@ export async function simulateBrowser() {
}
}),

// Metadata - Get all types (base route returns types)
http.get('http://localhost:3000/api/v1/meta', async () => {
console.log('[VirtualNetwork] GET /meta (types)');
try {
const result = await (kernel as any).broker.call('metadata.types', {});
return HttpResponse.json({ success: true, data: result });
} catch (err: any) {
return HttpResponse.json({ error: err.message }, { status: 500 });
}
}),

// Metadata - Objects List (Singular & Plural support)
http.get('http://localhost:3000/api/v1/meta/object', async () => {
console.log('[VirtualNetwork] GET /meta/object');
Expand Down Expand Up @@ -194,6 +205,22 @@ export async function simulateBrowser() {
} catch (err: any) {
return HttpResponse.json({ error: err.message }, { status: 500 });
}
}),

// Metadata - Generic type list (for agents, tools, etc.)
// This must come AFTER specific routes like /meta/object to avoid conflicts
http.get('http://localhost:3000/api/v1/meta/:type', async ({ params }) => {
// Skip if it's a specific route we already handled
if (params.type === 'object' || params.type === 'objects') {
return;
}
console.log(`[VirtualNetwork] GET /meta/${params.type}`);
try {
const result = await (kernel as any).broker.call(`metadata.${params.type}`, {});
return HttpResponse.json({ success: true, data: result });
} catch (err: any) {
return HttpResponse.json({ error: err.message }, { status: 500 });
}
})
];

Expand Down
14 changes: 8 additions & 6 deletions apps/studio/src/plugins/built-in/ai-plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import { defineStudioPlugin } from '@objectstack/spec/studio';
import type { StudioPlugin } from '../types';
import { Bot, BookOpen } from 'lucide-react';
import { Bot, BookOpen, Wrench } from 'lucide-react';

export const aiProtocolPlugin: StudioPlugin = {
manifest: defineStudioPlugin({
Expand All @@ -23,19 +23,21 @@ export const aiProtocolPlugin: StudioPlugin = {
key: 'ai',
label: 'AI',
icon: 'bot',
metadataTypes: ['agents', 'ragPipelines'],
metadataTypes: ['agent', 'tool', 'ragPipeline'],
order: 50,
},
],
metadataIcons: [
{ metadataType: 'agents', label: 'Agents', icon: 'bot' },
{ metadataType: 'ragPipelines', label: 'RAG Pipelines', icon: 'book-open' },
{ metadataType: 'agent', label: 'Agents', icon: 'bot' },
{ metadataType: 'tool', label: 'Tools', icon: 'wrench' },
{ metadataType: 'ragPipeline', label: 'RAG Pipelines', icon: 'book-open' },
],
},
}),

activate(api) {
api.registerMetadataIcon('agents', Bot, 'Agents');
api.registerMetadataIcon('ragPipelines', BookOpen, 'RAG Pipelines');
api.registerMetadataIcon('agent', Bot, 'Agents');
api.registerMetadataIcon('tool', Wrench, 'Tools');
api.registerMetadataIcon('ragPipeline', BookOpen, 'RAG Pipelines');
},
};
2 changes: 2 additions & 0 deletions apps/studio/src/plugins/built-in/default-plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const METADATA_TO_EXPORT_TYPE: Record<string, CodeExporterProps['type']> = {
flows: 'flow',
agent: 'agent',
agents: 'agent',
tool: 'tool',
tools: 'tool',
app: 'app',
apps: 'app',
};
Expand Down
Loading
Loading