diff --git a/packages/opencode/src/cli/cmd/command-groups.ts b/packages/opencode/src/cli/cmd/command-groups.ts index ca1c0dc88e7e..136922b9129c 100644 --- a/packages/opencode/src/cli/cmd/command-groups.ts +++ b/packages/opencode/src/cli/cmd/command-groups.ts @@ -117,6 +117,8 @@ export const COMMAND_CATEGORY_MAP: Record = { workflows: "agents", schedules: "agents", monitor: "agents", + personality: "agents", + recall: "knowledge", // Integrations & Tools integrations: "integrations", diff --git a/packages/opencode/src/cli/cmd/platform-personality.ts b/packages/opencode/src/cli/cmd/platform-personality.ts new file mode 100644 index 000000000000..52f76681f1af --- /dev/null +++ b/packages/opencode/src/cli/cmd/platform-personality.ts @@ -0,0 +1,116 @@ +import { cmd } from "./cmd" +import * as prompts from "@clack/prompts" +import { UI } from "../ui" +import { irisFetch, requireAuth, requireUserId, handleApiError, printDivider, dim, bold, success } from "./iris-api" + +// Personality presets — list available preset library and apply to an agent. +// Backed by config/personalities.php (fl-api). +// +// Usage: +// iris personality # list presets +// iris personality show concise # show full preset traits +// iris personality apply concise + +const PersonalityListCommand = cmd({ + command: "list", + aliases: ["ls"], + describe: "list available personality presets", + builder: (yargs) => yargs.option("json", { type: "boolean", default: false }), + async handler(args) { + UI.empty() + prompts.intro("◈ Personality Presets") + const token = await requireAuth(); if (!token) { prompts.outro("Done"); return } + const res = await irisFetch(`/api/v1/personalities`) + const ok = await handleApiError(res, "List personalities"); if (!ok) { prompts.outro("Done"); return } + const data = (await res.json()) as any + const presets: any[] = data?.data ?? [] + if (args.json) { console.log(JSON.stringify(presets, null, 2)); prompts.outro("Done"); return } + printDivider() + for (const p of presets) { + console.log(` ${bold(p.key.padEnd(12))} ${p.name}`) + console.log(` ${dim("".padEnd(12))} ${dim(p.description)}`) + console.log() + } + printDivider() + prompts.outro(`${presets.length} preset(s) — apply with: iris personality apply `) + }, +}) + +const PersonalityShowCommand = cmd({ + command: "show ", + describe: "show full traits text for a preset", + builder: (yargs) => + yargs + .positional("key", { type: "string", demandOption: true, describe: "preset key (e.g. concise, formal)" }) + .option("json", { type: "boolean", default: false }), + async handler(args) { + UI.empty() + prompts.intro(`◈ Preset: ${bold(args.key as string)}`) + const token = await requireAuth(); if (!token) { prompts.outro("Done"); return } + const res = await irisFetch(`/api/v1/personalities/${encodeURIComponent(String(args.key))}`) + const ok = await handleApiError(res, "Show personality"); if (!ok) { prompts.outro("Done"); return } + const data = (await res.json()) as any + const preset = data?.data + if (!preset) { prompts.outro("Not found"); return } + if (args.json) { console.log(JSON.stringify(preset, null, 2)); prompts.outro("Done"); return } + printDivider() + console.log(` ${bold("Name:")} ${preset.name}`) + console.log(` ${bold("Description:")} ${preset.description}`) + console.log() + console.log(` ${bold("Traits:")}`) + console.log(` ${preset.traits}`) + printDivider() + prompts.outro("Done") + }, +}) + +const PersonalityApplyCommand = cmd({ + command: "apply [key]", + describe: "apply a preset (or raw traits via --traits) to an agent", + builder: (yargs) => + yargs + .positional("agentId", { type: "string", demandOption: true, describe: "target bloq_agent ID" }) + .positional("key", { type: "string", describe: "preset key (omit when using --traits)" }) + .option("traits", { type: "string", describe: "raw personality_traits text (alternative to a preset key)" }) + .option("json", { type: "boolean", default: false }), + async handler(args) { + if (!args.key && !args.traits) { + console.error("personality apply: provide either a preset key or --traits \"...\"") + process.exit(1) + } + UI.empty() + prompts.intro(`◈ Apply Personality → agent ${bold(String(args.agentId))}`) + const token = await requireAuth(); if (!token) { prompts.outro("Done"); return } + const userId = await requireUserId(); if (!userId) { prompts.outro("Done"); return } + + const body: Record = {} + if (args.key) body.preset = String(args.key) + if (args.traits) body.traits = String(args.traits) + + const res = await irisFetch(`/api/v1/users/${userId}/bloqs/agents/${args.agentId}/apply-personality`, { + method: "POST", + body: JSON.stringify(body), + }) + const ok = await handleApiError(res, "Apply personality"); if (!ok) { prompts.outro("Done"); return } + const data = (await res.json()) as any + if (args.json) { console.log(JSON.stringify(data, null, 2)); prompts.outro("Done"); return } + printDivider() + console.log(` ${success("✓")} ${data?.message ?? "Applied"}`) + if (data?.data?.applied_from) console.log(` ${dim("From:")} ${data.data.applied_from}`) + printDivider() + prompts.outro("Done") + }, +}) + +export const PlatformPersonalityCommand = cmd({ + command: "personality ", + aliases: ["personalities"], + describe: "manage agent personality presets — list, show, apply", + builder: (yargs) => + yargs + .command(PersonalityListCommand) + .command(PersonalityShowCommand) + .command(PersonalityApplyCommand) + .demandCommand(1, "specify a subcommand: list | show | apply"), + handler: () => {}, +}) diff --git a/packages/opencode/src/cli/cmd/platform-recall.ts b/packages/opencode/src/cli/cmd/platform-recall.ts new file mode 100644 index 000000000000..570d8c46e245 --- /dev/null +++ b/packages/opencode/src/cli/cmd/platform-recall.ts @@ -0,0 +1,81 @@ +import { cmd } from "./cmd" +import * as prompts from "@clack/prompts" +import { UI } from "../ui" +import { irisFetch, requireAuth, handleApiError, printDivider, dim, bold, IRIS_API } from "./iris-api" + +// Cross-source recall — search past sessions, agent memory, and diary. +// Backed by /api/v6/recall on iris-api. +// +// Usage: +// iris recall "saddlepass deal" +// iris recall "vagaro integration" --days 30 --no-summarize +// iris recall "carrington" --agent 11 --limit 10 + +export const PlatformRecallCommand = cmd({ + command: "recall ", + aliases: ["search-memory"], + describe: "search past sessions, memory, and diary for a query", + builder: (yargs) => + yargs + .positional("query", { describe: "what to search for", type: "string", array: true }) + .option("days", { type: "number", default: 14, describe: "lookback window for diary (1-90)" }) + .option("limit", { type: "number", default: 5, describe: "max hits per source (1-20)" }) + .option("agent", { type: "string", describe: "scope to a specific agent_id" }) + .option("summarize", { type: "boolean", default: true, describe: "LLM summary of hits (gpt-5-nano)" }) + .option("json", { type: "boolean", default: false, describe: "raw JSON output" }), + async handler(args) { + const query = (args.query ?? []).join(" ").trim() + if (!query) { + console.error("recall: query is required — e.g. iris recall \"the saddlepass deal\"") + process.exit(1) + } + const token = await requireAuth(); if (!token) return + + UI.empty() + prompts.intro(`◈ Recall: ${bold(query)}`) + + const qs = new URLSearchParams({ + q: query, + days: String(args.days), + limit: String(args.limit), + summarize: args.summarize ? "1" : "0", + }) + if (args.agent) qs.set("agent_id", args.agent) + + const res = await irisFetch(`/api/v6/recall?${qs.toString()}`, {}, IRIS_API) + const ok = await handleApiError(res, "Recall") + if (!ok) { prompts.outro("Done"); return } + + const data = (await res.json()) as any + + if (args.json) { + console.log(JSON.stringify(data, null, 2)) + prompts.outro("Done") + return + } + + if (data?.summary) { + printDivider() + console.log(` ${bold("Summary")}`) + console.log(` ${data.summary}`) + } + + const renderBucket = (label: string, items: any[]) => { + if (!items?.length) return + printDivider() + console.log(` ${bold(label)} ${dim(`(${items.length})`)}`) + for (const item of items) { + const when = item.created_at ?? item.date ?? "" + const text = (item.content ?? item.summary ?? "").toString().slice(0, 200) + console.log(` ${dim(when)} ${text}`) + } + } + + renderBucket("Memory", data?.hits?.memory ?? []) + renderBucket("Chat", data?.hits?.chat ?? []) + renderBucket("Diary", data?.hits?.diary ?? []) + + printDivider() + prompts.outro(`${data?.count ?? 0} hit(s) for "${query}"`) + }, +}) diff --git a/packages/opencode/src/cli/cmd/platform-sdk-call.ts b/packages/opencode/src/cli/cmd/platform-sdk-call.ts index c1050de4ece5..200b3655f445 100644 --- a/packages/opencode/src/cli/cmd/platform-sdk-call.ts +++ b/packages/opencode/src/cli/cmd/platform-sdk-call.ts @@ -1,7 +1,7 @@ import { cmd } from "./cmd" import * as prompts from "@clack/prompts" import { UI } from "../ui" -import { irisFetch, requireAuth, requireUserId, handleApiError, printDivider, dim, bold } from "./iris-api" +import { irisFetch, requireAuth, requireUserId, handleApiError, printDivider, dim, bold, FL_API, IRIS_API } from "./iris-api" // ============================================================================ // platform-sdk-call — generic SDK proxy @@ -27,6 +27,8 @@ interface RouteDescriptor { path: string // params consumed by URL placeholders are removed; remaining go to body (POST/PUT/PATCH) or query (GET/DELETE) needsUserId?: boolean + // which API to call — defaults to fl-api. iris-api endpoints (e.g. /api/v6/recall) set this to "iris" + base?: "fl" | "iris" } // Endpoints sourced directly from PHP SDK Resources/* (grepped April 2026) @@ -116,6 +118,14 @@ const ROUTES: Record = { "tools.list": { method: "GET", path: "/api/v1/tools" }, "tools.invoke": { method: "POST", path: "/api/v1/tools/invoke" }, + // Personality presets (fl-api) + "personalities.list": { method: "GET", path: "/api/v1/personalities" }, + "personalities.get": { method: "GET", path: "/api/v1/personalities/{key}" }, + "personalities.apply": { method: "POST", path: "/api/v1/users/{userId}/bloqs/agents/{agent}/apply-personality", needsUserId: true }, + + // Cross-source recall (iris-api) + "recall.search": { method: "GET", path: "/api/v6/recall", base: "iris" }, + // Bloq ingestion "bloqs.ingestFolder": { method: "POST", path: "/api/v1/bloqs/{bloqId}/ingest-folder" }, "bloqs.ingestionJobs": { method: "GET", path: "/api/v1/bloqs/{bloqId}/ingestion-jobs" }, @@ -301,10 +311,11 @@ export const PlatformSdkCallCommand = cmd({ body = JSON.stringify(remaining) } + const apiBase = route.base === "iris" ? IRIS_API : FL_API const res = await irisFetch(finalUrl, { method: route.method, ...(body ? { body } : {}), - }) + }, apiBase) const ok = await handleApiError(res, `${route.method} ${endpoint}`) if (!ok) process.exit(1) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index ae4f18d4cb39..1c80e544d1d6 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -374,6 +374,14 @@ export function Autocomplete(props: { } } + const insertSlash = (name: string) => () => { + const newText = "/" + name + " " + const cursor = props.input().logicalCursor + props.input().deleteRange(0, 0, cursor.row, cursor.col) + props.input().insertText(newText) + props.input().cursorOffset = Bun.stringWidth(newText) + } + results.push( { display: "/new", @@ -381,6 +389,31 @@ export function Autocomplete(props: { description: "create a new session", onSelect: () => command.trigger("session.new"), }, + { + display: "/recall", + description: "search past sessions, memory, and diary", + onSelect: insertSlash("recall"), + }, + { + display: "/personality", + description: "view or switch agent personality preset", + onSelect: insertSlash("personality"), + }, + { + display: "/usage", + description: "show token usage and costs", + onSelect: insertSlash("usage"), + }, + { + display: "/insights", + description: "show usage insights over time (e.g. /insights 7)", + onSelect: insertSlash("insights"), + }, + { + display: "/sdk", + description: "invoke any SDK endpoint (e.g. /sdk leads.list search=acme)", + onSelect: insertSlash("sdk"), + }, { display: "/models", description: "list models", diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 96a1cdd3b1cc..048da8befa05 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -102,6 +102,8 @@ import { PlatformToolsCommand } from "./cli/cmd/platform-tools" import { PlatformUsersCommand } from "./cli/cmd/platform-users" import { PlatformPhoneCommand } from "./cli/cmd/platform-phone" import { PlatformVoiceCommand } from "./cli/cmd/platform-voice" +import { PlatformRecallCommand } from "./cli/cmd/platform-recall" +import { PlatformPersonalityCommand } from "./cli/cmd/platform-personality" import { PlatformMailCommand } from "./cli/cmd/platform-mail" import { PlatformImessageCommand } from "./cli/cmd/platform-imessage" import { PlatformCalendarCommand } from "./cli/cmd/platform-calendar" @@ -278,6 +280,8 @@ const cli = yargs(rawArgs) .command(reg(PlatformUsersCommand)) .command(reg(PlatformPhoneCommand)) .command(reg(PlatformVoiceCommand)) + .command(reg(PlatformRecallCommand)) + .command(reg(PlatformPersonalityCommand)) .command(reg(PlatformMailCommand)) .command(reg(PlatformImessageCommand)) .command(reg(PlatformCalendarCommand)) diff --git a/scaffold/AGENTS.md b/scaffold/AGENTS.md index 77b02393aaac..b0435bbe3bf3 100644 --- a/scaffold/AGENTS.md +++ b/scaffold/AGENTS.md @@ -57,6 +57,20 @@ When the user asks something that might match a recipe, **read the recipe file f - **NEVER invent component type names.** Run `iris pages component-registry` first. Invalid types render blank. - **READ CLI output carefully.** Use exact values shown — don't make up IDs, URLs, or status values. +## In-chat slash commands + +When the user's message starts with one of these slash commands, treat it as a structured request and respond using `iris sdk:call` (preferred) or the appropriate `iris` shell command — don't ask follow-up questions if the intent is clear. + +| Command | What it means | How to handle | +|---|---|---| +| `/recall ` | Search past sessions, memory, and diary for the query | Use `iris sdk:call diary.list` and `iris memory show ` to gather context, then summarize matches. If no specific bloq is set, search across the user's recent diary entries. | +| `/personality [name]` | View or switch the active agent's personality | No name → list available agents with their `personality_traits` via `iris sdk:call agents.list userId=me`. With a name → find a matching agent or update the active agent's `personality_traits` field via `iris agents push` after pulling. | +| `/usage` | Show token usage and costs | Run `iris stats` and surface the totals. Show recent session breakdown if available. | +| `/insights [days]` | Usage insights over a time range | Default 7 days. Use `iris stats` plus `iris sdk:call diary.list days=` to show token consumption + agent activity over the window. | +| `/sdk [params]` | Call any IRIS SDK endpoint directly | Run `iris sdk:call ...` via bash. If the user picks `/sdk` without args, show categories from `iris sdk:call --list` so they can choose. | + +These slash messages are user shortcuts — interpret them, do the work, return a concise result. Don't echo the slash back; just answer. + ## Genesis Page Builder — Component Rules When building or editing pages with `iris pages`, follow these rules: