Scoped Context
A scoped context is a namespaced view of the context: it auto-prefixes every RPC id, shared-state key, and streaming channel with your tool's id, and adds a typed, persisted settings store, used from a single tool's code.
Node side
setup(ctx) receives the full DevframeNodeContext; scope it with ctx.scope(id), conventionally your devframe id:
import { defineDevframe, defineRpcFunction } from 'devframe'
export default defineDevframe({
id: 'my-plugin',
name: 'My Plugin',
setup(ctx) {
const my = ctx.scope('my-plugin')
my.rpc.register(defineRpcFunction({
name: 'get-modules', // bare name: stored as `my-plugin:get-modules`
type: 'query',
handler: () => loadModules(),
}))
},
})
declare function loadModules(): { id: string }[]ctx.scope(id) is stable per id, re-exposing unscoped APIs (views, diagnostics, agent, host, cwd, mode), swapping in the auto-namespaced rpc, and keeping the original as my.base.
Browser side
(await connectDevframe()).scope(id) gives the matching view: my.rpc carries call / callEvent / callOptional, register, sharedState, and streaming, plus my.settings.
Auto-namespacing
Bare names are prefixed <namespace>: (call('get-modules') → my-plugin:get-modules); a name with : passes through unchanged to another tool (call('other-plugin:status')).
register accepts only bare names; an already-namespaced one throws DF0034; use ctx.base.rpc.register.
Bare names stay typed: call('get-modules') resolves to your RPC registry entry, sharedState('selection') to the matching DevframeRpcSharedStates key.
Settings
my.settings is a persisted key-value store (alongside my.rpc), with two scopes:
project: per-checkout values, under theworkspacestorage dir.global: per-user values, under theglobalstorage dir.
Both are file-backed and synced to the browser over the shared-state protocol; a set propagates to peers, surviving restarts.
const { settings } = my
await settings.project.set('theme', 'dark')
await settings.project.get('theme') // => 'dark'
await settings.project.all() // => theme is 'dark'
await settings.project.delete('theme')
const off = await settings.global.onChange((value) => {
console.log('global settings changed', value)
})Every method is async; the store resolves on first access.
Typed settings
Augment DevframeSettingsRegistry to type a namespace's settings once; the scope types settings.global and settings.project:
declare module 'devframe' {
interface DevframeSettingsRegistry {
'my-plugin': {
theme: 'light' | 'dark'
recentFiles: string[]
}
}
}const my = ctx.scope('my-plugin')
await my.settings.project.set('theme', 'dark') // ✓ typed
await my.settings.project.set('theme', 'blue') // ✗ not assignableUnaugmented namespaces fall back to an open record.