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
16 changes: 16 additions & 0 deletions .changeset/migrate-api-client-npm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"perstack": patch
"@perstack/runtime": patch
---

Migrate @perstack/api-client to npm version (^0.0.51)

BREAKING CHANGES:
- Remove `perstack publish` command (new API uses draft-based publish model)
- Remove `perstack status` command (no status update in new API)
- Remove `perstack tag` command (no tag update in new API)
- Remove `perstack unpublish` command (CLI no longer supports registry operations)

Changes:
- Update API calls from `client.registry.experts.*` to `client.experts.*`
- Adapt response handling for new API structure where experts are nested under `definition.experts`
1 change: 0 additions & 1 deletion apps/create-expert/src/lib/agents-md-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ Perstack is a package manager and runtime for agent-first development. It enable
Key concepts:
- **Experts**: Modular micro-agents defined in TOML
- **Runtime**: Executes Experts with isolation, observability, and sandbox support
- **Registry**: Public registry for sharing and reusing Experts

## Project Configuration

Expand Down
8 changes: 0 additions & 8 deletions apps/perstack/bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,8 @@ import { Command } from "commander"
import packageJson from "../package.json" with { type: "json" }
import { installCommand } from "../src/install.js"
import { logCommand } from "../src/log.js"
import { publishCommand } from "../src/publish.js"
import { runCommand } from "../src/run.js"
import { startCommand } from "../src/start.js"
import { statusCommand } from "../src/status.js"
import { tagCommand } from "../src/tag.js"
import { unpublishCommand } from "../src/unpublish.js"

const program = new Command()
.name(packageJson.name)
Expand All @@ -19,8 +15,4 @@ const program = new Command()
.addCommand(runCommand)
.addCommand(logCommand)
.addCommand(installCommand)
.addCommand(publishCommand)
.addCommand(unpublishCommand)
.addCommand(tagCommand)
.addCommand(statusCommand)
program.parse()
2 changes: 1 addition & 1 deletion apps/perstack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@perstack/api-client": "workspace:*",
"@perstack/api-client": "^0.0.51",
"@perstack/core": "workspace:*",
"@perstack/react": "workspace:*",
"@perstack/runtime": "workspace:*",
Expand Down
132 changes: 117 additions & 15 deletions apps/perstack/src/install.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { readFile, writeFile } from "node:fs/promises"
import path from "node:path"
import { createApiClient, type RegistryExpert } from "@perstack/api-client"
import { createApiClient } from "@perstack/api-client"
import {
defaultPerstackApiBaseUrl,
type Expert,
Expand Down Expand Up @@ -36,23 +36,115 @@ async function findConfigPathRecursively(cwd: string): Promise<string> {
}
}

function toRuntimeExpert(key: string, expert: RegistryExpert): Expert {
type PublishedExpertData = {
name: string
version: string
description?: string
instruction: string
skills?: Record<
string,
| {
type: "mcpStdioSkill"
name: string
description: string
rule?: string
pick?: string[]
omit?: string[]
command: "npx" | "uvx"
packageName: string
requiredEnv?: string[]
}
| {
type: "mcpSseSkill"
name: string
description: string
rule?: string
pick?: string[]
omit?: string[]
endpoint: string
}
| {
type: "interactiveSkill"
name: string
description: string
rule?: string
tools: Record<string, { description: string; inputJsonSchema: string }>
}
>
delegates?: string[]
tags?: string[]
}

function toRuntimeExpert(key: string, expert: PublishedExpertData): Expert {
const skills: Record<string, Skill> = Object.fromEntries(
Object.entries(expert.skills).map(([name, skill]) => {
Object.entries(expert.skills ?? {}).map(([name, skill]) => {
switch (skill.type) {
case "mcpStdioSkill":
return [name, { ...skill, name }]
return [
name,
{
type: skill.type,
name,
description: skill.description,
rule: skill.rule,
pick: skill.pick ?? [],
omit: skill.omit ?? [],
command: skill.command,
packageName: skill.packageName,
requiredEnv: skill.requiredEnv ?? [],
lazyInit: false,
},
]
case "mcpSseSkill":
return [name, { ...skill, name }]
return [
name,
{
type: skill.type,
name,
description: skill.description,
rule: skill.rule,
pick: skill.pick ?? [],
omit: skill.omit ?? [],
endpoint: skill.endpoint,
lazyInit: false,
},
]
case "interactiveSkill":
return [name, { ...skill, name }]
return [
name,
{
type: skill.type,
name,
description: skill.description,
rule: skill.rule,
tools: Object.fromEntries(
Object.entries(skill.tools).map(([toolName, tool]) => [
toolName,
{
name: toolName,
description: tool.description,
inputSchema: JSON.parse(tool.inputJsonSchema),
},
]),
),
},
]
default: {
throw new Error(`Unknown skill type: ${(skill as { type: string }).type}`)
}
}
}),
)
return { ...expert, key, skills }
return {
key,
name: expert.name,
version: expert.version,
description: expert.description ?? "",
instruction: expert.instruction,
skills,
delegates: expert.delegates ?? [],
tags: expert.tags ?? [],
}
}

function configExpertToExpert(
Expand All @@ -79,10 +171,6 @@ async function resolveAllExperts(
env: Record<string, string>,
): Promise<Record<string, Expert>> {
const experts: Record<string, Expert> = {}
const client = createApiClient({
baseUrl: config.perstackApiBaseUrl ?? defaultPerstackApiBaseUrl,
apiKey: env.PERSTACK_API_KEY,
})
for (const [key, configExpert] of Object.entries(config.experts ?? {})) {
experts[key] = configExpertToExpert(key, configExpert)
}
Expand All @@ -94,18 +182,32 @@ async function resolveAllExperts(
}
}
}
if (toResolve.size === 0) {
return experts
}
const apiKey = env.PERSTACK_API_KEY
if (!apiKey) {
throw new Error("PERSTACK_API_KEY is required to resolve remote delegates")
}
const client = createApiClient({
baseUrl: config.perstackApiBaseUrl ?? defaultPerstackApiBaseUrl,
apiKey,
})
while (toResolve.size > 0) {
const delegateKey = toResolve.values().next().value
if (!delegateKey) break
toResolve.delete(delegateKey)
if (experts[delegateKey]) continue
const result = await client.registry.experts.get(delegateKey)
const result = await client.experts.get(delegateKey)
if (!result.ok) {
throw new Error(`Failed to resolve delegate "${delegateKey}": ${result.error.message}`)
}
const registryExpert = result.data
experts[delegateKey] = toRuntimeExpert(delegateKey, registryExpert)
for (const nestedDelegate of registryExpert.delegates) {
const publishedExpert = result.data.data.definition.experts[delegateKey]
if (!publishedExpert) {
throw new Error(`Expert "${delegateKey}" not found in API response`)
}
experts[delegateKey] = toRuntimeExpert(delegateKey, publishedExpert)
for (const nestedDelegate of publishedExpert.delegates ?? []) {
if (!experts[nestedDelegate]) {
toResolve.add(nestedDelegate)
}
Expand Down
144 changes: 0 additions & 144 deletions apps/perstack/src/publish.ts

This file was deleted.

Loading