@lincyaw/dsh 0.0.0-stage → 0.1.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.
Files changed (55) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +22 -2
  3. package/build/client.mjs +66 -0
  4. package/build/dist/client.js +90 -0
  5. package/build/dist/client.json +70 -0
  6. package/build/ensure.mjs +30 -0
  7. package/cordis.patch.yml +17 -0
  8. package/package.json +95 -3
  9. package/profile.patch.yml +25 -0
  10. package/rows/context.mjs +7 -0
  11. package/rows/load.mjs +8 -0
  12. package/rows/policy.mjs +7 -0
  13. package/rows/routes.mjs +7 -0
  14. package/rows/service.mjs +7 -0
  15. package/rows/skills.mjs +7 -0
  16. package/rows/tools.mjs +7 -0
  17. package/rows/turns.mjs +7 -0
  18. package/rows/workbench.mjs +9 -0
  19. package/rows/workspaces.mjs +7 -0
  20. package/skills/presmium-author/SKILL.md +95 -0
  21. package/skills/presmium-author/references/assistance.md +27 -0
  22. package/skills/presmium-author/references/code.md +52 -0
  23. package/skills/presmium-author/references/directives.md +592 -0
  24. package/skills/presmium-author/references/publishing.md +60 -0
  25. package/skills/presmium-author/references/writing.md +72 -0
  26. package/skills/presmium-present/SKILL.md +69 -0
  27. package/skills/presmium-present/references/classroom.md +46 -0
  28. package/skills/presmium-present/references/delivery.md +63 -0
  29. package/skills/presmium-present/references/lecture.md +58 -0
  30. package/src/client/Frame.tsx +314 -0
  31. package/src/client/brand.tsx +90 -0
  32. package/src/client/chrome.tsx +79 -0
  33. package/src/client/contract.ts +35 -0
  34. package/src/client/dsh.d.ts +129 -0
  35. package/src/client/fallback.tsx +49 -0
  36. package/src/client/frame.css +345 -0
  37. package/src/client/index.tsx +154 -0
  38. package/src/client/layout.ts +217 -0
  39. package/src/client/messages.ts +78 -0
  40. package/src/client/source.tsx +81 -0
  41. package/src/client/styles.css +3 -0
  42. package/src/client/theme.ts +149 -0
  43. package/src/node/context.ts +55 -0
  44. package/src/node/dsh.ts +93 -0
  45. package/src/node/launch.ts +59 -0
  46. package/src/node/policy.ts +59 -0
  47. package/src/node/routes.ts +46 -0
  48. package/src/node/service.ts +280 -0
  49. package/src/node/skills.ts +46 -0
  50. package/src/node/tools.ts +43 -0
  51. package/src/node/turns/index.ts +165 -0
  52. package/src/node/watchdog.mjs +36 -0
  53. package/src/node/workbench.ts +43 -0
  54. package/src/node/workspaces.ts +10 -0
  55. package/web.patch.yml +190 -0
@@ -0,0 +1,93 @@
1
+ import type { PresmiumService } from './service.ts'
2
+
3
+ export interface DshLogger {
4
+ info(format: string, ...args: unknown[]): void
5
+ warn(format: string, ...args: unknown[]): void
6
+ debug(format: string, ...args: unknown[]): void
7
+ }
8
+
9
+ export interface DshSession {
10
+ readonly id: string
11
+ readonly header?: { cwd?: string }
12
+ }
13
+
14
+ export interface DshAgent {
15
+ readonly session: DshSession
16
+ readonly ctx: DshContext
17
+ whenIdle(): Promise<void>
18
+ }
19
+
20
+ export interface DshToolExecution {
21
+ readonly callId: string
22
+ readonly name: string
23
+ readonly arguments: unknown
24
+ readonly agent?: DshAgent
25
+ readonly signal: AbortSignal
26
+ }
27
+
28
+ export type DshPreDecision =
29
+ | { kind: 'allow' }
30
+ | { kind: 'deny', reason: string }
31
+ | { kind: 'ask', reason?: string, displayReason?: { en: string, [locale: string]: string } }
32
+
33
+ export interface DshToolDefinition {
34
+ name: string
35
+ description: string
36
+ parameters: Record<string, unknown>
37
+ output: { schema: Record<string, unknown>, render(args: unknown, value: unknown): { type: 'text', text: string }[] }
38
+ execute(args: unknown, exec: DshToolExecution): Promise<unknown>
39
+ }
40
+
41
+ export interface DshTools {
42
+ register(definition: DshToolDefinition): () => void
43
+ guard(guard: (exec: Readonly<DshToolExecution>) => string | undefined): () => void
44
+ }
45
+
46
+ export interface DshPromptPart {
47
+ name: string
48
+ order: number
49
+ text: string | (() => string)
50
+ interpolate?: boolean
51
+ required?: boolean
52
+ }
53
+
54
+ export interface DshSystemPrompt {
55
+ section(section: DshPromptPart): () => void
56
+ context(context: DshPromptPart): () => void
57
+ }
58
+
59
+ export interface DshSkill {
60
+ name: string
61
+ description: string
62
+ content: string
63
+ path: string
64
+ source: 'bundled'
65
+ resourceBase: { kind: 'directory', path: string }
66
+ }
67
+
68
+ export interface DshFetchRoute {
69
+ path: string
70
+ methods: ('GET' | 'HEAD' | 'POST')[]
71
+ requestBody: 'buffered' | 'streaming'
72
+ fetch(request: Request): Promise<Response>
73
+ }
74
+
75
+ export interface DshWorkspaces {
76
+ create(path: string, title?: string): Promise<{ id: string, path: string, title: string, setTitle(title: string): Promise<void> }>
77
+ list(): { id: string, path: string, title: string, setTitle(title: string): Promise<void> }[]
78
+ delete(id: string): Promise<boolean>
79
+ }
80
+
81
+ export interface DshContext {
82
+ get(name: string): unknown
83
+ provide(name: string, value: unknown): void
84
+ on(event: string, listener: (...args: any[]) => unknown): () => void
85
+ effect(effect: () => () => void): void
86
+ logger?: (name: string) => DshLogger
87
+ tools: DshTools
88
+ systemPrompt: DshSystemPrompt
89
+ skills: { register(skill: DshSkill): () => void }
90
+ connection: { fetch: { register(route: DshFetchRoute): () => Promise<void> } }
91
+ workspaceRegistry: DshWorkspaces
92
+ presmium: PresmiumService
93
+ }
@@ -0,0 +1,59 @@
1
+ import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
2
+ import { createRequire } from 'node:module'
3
+ import { dirname, join } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ export type Carrier = 'web' | 'headless' | 'acp'
7
+
8
+ export const PACKAGE_DIR = fileURLToPath(new URL('../..', import.meta.url))
9
+ export const PACKAGE_NAME = '@lincyaw/dsh'
10
+
11
+ const CARRIERS: Record<Carrier, string> = {
12
+ web: '@deepseek-ai/dsh-web-app',
13
+ headless: '@deepseek-ai/dsh-headless',
14
+ acp: '@deepseek-ai/dsh-acp-app',
15
+ }
16
+
17
+ export function launchPatches(carrier: Carrier): string[] {
18
+ return [...carrier === 'web' ? [join(PACKAGE_DIR, 'web.patch.yml')] : [], join(PACKAGE_DIR, 'profile.patch.yml')]
19
+ }
20
+
21
+ export function launchArgs(name: string, carrier: Carrier): string[] {
22
+ return ['--profile', name, ...launchPatches(carrier).flatMap(file => ['--patch', file])]
23
+ }
24
+
25
+ export const WATCHDOG = join(PACKAGE_DIR, 'src', 'node', 'watchdog.mjs')
26
+
27
+ export const WEB_READY = /^dsh web: (https?:\/\/\S+)$/m
28
+
29
+ export function dshVersion(): string {
30
+ return (JSON.parse(readFileSync(join(PACKAGE_DIR, 'package.json'), 'utf8')) as { engines: { dsh: string } }).engines.dsh
31
+ }
32
+
33
+ export function dshBin(from = PACKAGE_DIR): string {
34
+ const manifest = createRequire(join(from, 'package.json')).resolve('@deepseek-ai/dsh/package.json')
35
+ const { bin } = JSON.parse(readFileSync(manifest, 'utf8')) as { bin: string | Record<string, string> }
36
+ return join(dirname(manifest), typeof bin === 'string' ? bin : bin.dsh!)
37
+ }
38
+
39
+ export function launchEnv(dshHome: string): Record<string, string> {
40
+ return { DSH_HOME: dshHome, DSH_AGENTS_HOME: join(dshHome, 'agents'), DSH_TELEMETRY_MODE: 'DISABLED' }
41
+ }
42
+
43
+ export function writeProfile(dshHome: string, name: string, carrier: Carrier, bundle = PACKAGE_DIR): string {
44
+ const dir = join(dshHome, 'profiles', name)
45
+ mkdirSync(join(dir, 'node_modules', '@lincyaw'), { recursive: true })
46
+ mkdirSync(join(dshHome, 'agents'), { recursive: true })
47
+ writeFileSync(join(dir, 'package.json'), `${JSON.stringify({
48
+ name: `dsh-profile-${name}`,
49
+ private: true,
50
+ dependencies: {},
51
+ dsh: { profile: { bundles: ['@deepseek-ai/dsh-base', CARRIERS[carrier], PACKAGE_NAME] } },
52
+ }, null, 2)}\n`)
53
+ writeFileSync(join(dir, 'cordis.yml'), '# The profile root: an empty entry list; the tree is composed from the bundles and the patches.\n[]\n')
54
+ if (!existsSync(join(dir, 'cordis.patch.yml'))) writeFileSync(join(dir, 'cordis.patch.yml'), '# Your patch layer for this profile (Presmium\'s settings are passed with --patch after it).\n[]\n')
55
+ const link = join(dir, 'node_modules', PACKAGE_NAME)
56
+ rmSync(link, { force: true, recursive: true })
57
+ symlinkSync(realpathSync(bundle), link, 'junction')
58
+ return dir
59
+ }
@@ -0,0 +1,59 @@
1
+ import { homedir } from 'node:os'
2
+ import { isAbsolute, join, relative, resolve, sep } from 'node:path'
3
+ import { describeOperation } from '@lincyaw/mcp/client'
4
+ import type { DshContext, DshPreDecision, DshToolExecution } from './dsh.ts'
5
+ import { PREFIX } from './tools.ts'
6
+
7
+ export const name = 'presmium-policy'
8
+ export const inject = ['presmium', 'tools']
9
+
10
+ const FILE_TOOLS = new Set(['write', 'edit', 'str_replace_editor'])
11
+
12
+ export function operatorRoot(env: NodeJS.ProcessEnv = process.env): string {
13
+ return resolve(env.PRESMIUM_OPERATOR_DIR || join(env.XDG_STATE_HOME || join(homedir(), '.local', 'state'), 'presmium', 'operator'))
14
+ }
15
+
16
+ function inside(path: string, dir: string): boolean {
17
+ const rel = relative(dir, path)
18
+ return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))
19
+ }
20
+
21
+ function fileOf(exec: Readonly<DshToolExecution>): string | undefined {
22
+ const args = exec.arguments as { file_path?: unknown, path?: unknown } | undefined
23
+ const path = args?.file_path ?? args?.path
24
+ return typeof path === 'string' && path !== '' ? path : undefined
25
+ }
26
+
27
+ const shown = (text: string, lang?: string) => ({ en: text, ...lang ? { [lang]: text } : {} })
28
+
29
+ export function apply(ctx: DshContext): void {
30
+ const operators = operatorRoot()
31
+
32
+ ctx.on('tools/pre-execute', async (exec: DshToolExecution, next: () => Promise<DshPreDecision>): Promise<DshPreDecision> => {
33
+ const session = ctx.presmium.session(exec.agent)
34
+ if (exec.name.startsWith(PREFIX)) {
35
+ const tool = session?.tools.find(own => `${PREFIX}${own.name}` === exec.name)
36
+ if (!session || !tool) return { kind: 'deny', reason: `Presmium: ${exec.name} is no tool of this session's document` }
37
+ if (tool.kind === 'read') return next()
38
+ if (tool.approval === 'allow') return { kind: 'allow' }
39
+ const lang = session.document.lang
40
+ const title = describeOperation(tool.name, (exec.arguments ?? {}) as Record<string, unknown>, lang)
41
+ return { kind: 'ask', reason: `presmium:${tool.kind} ${tool.name}`, displayReason: shown(`Presmium · ${title}`, lang) }
42
+ }
43
+ const file = FILE_TOOLS.has(exec.name) ? fileOf(exec) : undefined
44
+ if (file && inside(resolve(exec.agent ? ctx.presmium.directoryOf(exec.agent) : process.cwd(), file), operators)) return next()
45
+ if (session && file && !inside(resolve(ctx.presmium.directoryOf(exec.agent!), file), ctx.presmium.directoryOf(exec.agent!))) {
46
+ return { kind: 'ask', reason: `presmium:write ${file}`, displayReason: shown(`Presmium · ${file}`, session.document.lang) }
47
+ }
48
+ return next()
49
+ })
50
+
51
+ ctx.tools.guard((exec) => {
52
+ const file = FILE_TOOLS.has(exec.name) ? fileOf(exec) : undefined
53
+ const dir = exec.agent ? ctx.presmium.directoryOf(exec.agent) : process.cwd()
54
+ if (file && inside(resolve(dir, file), operators)) return `Presmium: the operator directory (${operators}) is not the agent's to write`
55
+ const command = exec.name === 'bash' ? (exec.arguments as { command?: unknown } | undefined)?.command : undefined
56
+ if (typeof command === 'string' && command.includes(operators)) return `Presmium: the operator directory (${operators}) is not the agent's to touch`
57
+ return undefined
58
+ })
59
+ }
@@ -0,0 +1,46 @@
1
+ import type { DshContext } from './dsh.ts'
2
+
3
+ export const name = 'presmium-routes'
4
+ export const inject = ['presmium', 'connection']
5
+
6
+ const WORKSPACE = '/__presmium/presmium.workspace/'
7
+ const SERVER = '/__presmium/presmium.server/'
8
+
9
+ export const ROUTES: Record<string, string> = Object.fromEntries([
10
+ ...['documents', 'files', 'source', 'changes', 'changes/commit', 'changes/discard', 'revisions', 'revisions/revert',
11
+ 'ir', 'ir/events', 'preview', 'build', 'check',
12
+ 'review', 'review/open', 'review/step', 'review/accept', 'review/reject', 'review/restore'].map(path => [path, `${WORKSPACE}${path}`]),
13
+ ['focus', `${SERVER}focus`],
14
+ ])
15
+
16
+ const PASSED = ['accept', 'content-type', 'presmium-document', 'last-event-id']
17
+ const RETURNED = ['content-type', 'cache-control', 'content-disposition', 'x-accel-buffering']
18
+
19
+ export function apply(ctx: DshContext): void {
20
+ for (const [path, target] of Object.entries(ROUTES)) {
21
+ ctx.connection.fetch.register({
22
+ path: `/api/presmium/${path}`,
23
+ methods: ['GET', 'POST'],
24
+ requestBody: 'buffered',
25
+ async fetch(request) {
26
+ const headers: Record<string, string> = {}
27
+ for (const name of PASSED) {
28
+ const value = request.headers.get(name)
29
+ if (value !== null) headers[name] = value
30
+ }
31
+ const upstream = await ctx.presmium.request(`${target}${new URL(request.url).search}`, {
32
+ method: request.method,
33
+ headers,
34
+ ...request.method === 'POST' ? { raw: await request.arrayBuffer() } : {},
35
+ signal: request.signal,
36
+ })
37
+ const answered = new Headers()
38
+ for (const name of RETURNED) {
39
+ const value = upstream.headers.get(name)
40
+ if (value !== null) answered.set(name, value)
41
+ }
42
+ return new Response(upstream.body, { status: upstream.status, headers: answered })
43
+ },
44
+ })
45
+ }
46
+ }
@@ -0,0 +1,280 @@
1
+ import { spawn } from 'node:child_process'
2
+ import type { ChildProcess } from 'node:child_process'
3
+ import { randomBytes } from 'node:crypto'
4
+ import { realpathSync } from 'node:fs'
5
+ import { createRequire } from 'node:module'
6
+ import { dirname, join, relative, resolve, sep } from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { connectPresmiumMcp } from '@lincyaw/mcp/client'
9
+ import type { ListedTool, PresmiumMcpClient } from '@lincyaw/mcp/client'
10
+ import type { DshAgent, DshContext, DshLogger } from './dsh.ts'
11
+
12
+ export const name = 'presmium-service'
13
+
14
+ export interface Config {
15
+ url?: string
16
+ token?: string
17
+ root?: string
18
+ }
19
+
20
+ export interface Endpoint {
21
+ url: string
22
+ token: string
23
+ root: string
24
+ }
25
+
26
+ export interface WorkspaceDocument {
27
+ dir: string
28
+ title: string
29
+ lang?: string
30
+ }
31
+
32
+ export interface PresmiumSession {
33
+ id: string
34
+ document: WorkspaceDocument
35
+ dir: string
36
+ client: PresmiumMcpClient
37
+ tools: ListedTool[]
38
+ }
39
+
40
+ const START_MS = 180_000
41
+ const DOCUMENTS_MS = 2000
42
+ const WATCHDOG = fileURLToPath(new URL('./watchdog.mjs', import.meta.url))
43
+
44
+ function presmiumBin(): string {
45
+ const entry = createRequire(import.meta.url).resolve('@lincyaw/cli')
46
+ return join(dirname(dirname(entry)), 'bin', 'presmium.mjs')
47
+ }
48
+
49
+ function real(path: string): string {
50
+ try {
51
+ return realpathSync(path)
52
+ } catch {
53
+ return resolve(path)
54
+ }
55
+ }
56
+
57
+ export function documentAt(root: string, dir: string, documents: WorkspaceDocument[]): WorkspaceDocument | undefined {
58
+ const rel = relative(real(root), real(dir)).split(sep).join('/')
59
+ if (rel.startsWith('..')) return undefined
60
+ return documents.find(doc => doc.dir === rel)
61
+ }
62
+
63
+ function serviceEnv(token: string): NodeJS.ProcessEnv {
64
+ const env: NodeJS.ProcessEnv = {}
65
+ for (const [key, value] of Object.entries(process.env)) {
66
+ if (key.startsWith('DSH_') || key.startsWith('DEEPSEEK_') || key === 'NODE_ENV') continue
67
+ env[key] = value
68
+ }
69
+ return { ...env, PRESMIUM_SERVICE_TOKEN: token }
70
+ }
71
+
72
+ export class PresmiumService {
73
+ readonly ready: Promise<Endpoint>
74
+ readonly #ctx: DshContext
75
+ readonly #log: DshLogger
76
+ #child: ChildProcess | undefined
77
+ #documents: { at: number, list: Promise<WorkspaceDocument[]> } | undefined
78
+ readonly #opening = new Map<string, Promise<PresmiumSession | undefined>>()
79
+ readonly #open = new Map<string, PresmiumSession>()
80
+ readonly #refreshing = new Map<string, Promise<PresmiumSession | undefined>>()
81
+ readonly #selectionVersions = new Map<string, number>()
82
+ readonly #selectionListeners = new Map<string, { before(): Promise<void>, after(): Promise<void> }>()
83
+ readonly #selected = new Map<string, string | null>()
84
+ #closed = false
85
+
86
+ constructor(ctx: DshContext, config: Config, log: DshLogger) {
87
+ this.#ctx = ctx
88
+ this.#log = log
89
+ const root = resolve(config.root || process.cwd())
90
+ this.ready = config.url
91
+ ? Promise.resolve({ url: config.url.replace(/\/+$/, ''), token: config.token ?? '', root })
92
+ : this.#start(root)
93
+ this.ready.catch((error: unknown) => log.warn(`presmium-service: ${error instanceof Error ? error.message : String(error)}`))
94
+ }
95
+
96
+ #start(root: string): Promise<Endpoint> {
97
+ const token = randomBytes(24).toString('base64url')
98
+ const child = spawn(process.execPath, [WATCHDOG, process.execPath, presmiumBin(), 'serve', root, '--port', '0'], {
99
+ cwd: root,
100
+ env: serviceEnv(token),
101
+ stdio: ['pipe', 'pipe', 'pipe'],
102
+ detached: true,
103
+ })
104
+ this.#child = child
105
+ child.stdin!.on('error', () => {})
106
+ let output = ''
107
+ return new Promise<Endpoint>((resolveStart, reject) => {
108
+ const timer = setTimeout(() => reject(new Error(`presmium serve did not start within ${START_MS / 1000} s:\n${output.slice(-2000)}`)), START_MS)
109
+ const read = (chunk: Buffer) => {
110
+ output = (output + chunk.toString()).slice(-20_000)
111
+ const url = /^serve {2}(http\S+)$/m.exec(output)?.[1]
112
+ const workspace = /^workspace {2}(.+)$/m.exec(output)?.[1]
113
+ if (url && workspace) {
114
+ clearTimeout(timer)
115
+ resolveStart({ url, token, root: workspace.trim() })
116
+ }
117
+ }
118
+ child.stdout!.on('data', read)
119
+ child.stderr!.on('data', (chunk: Buffer) => {
120
+ output = (output + chunk.toString()).slice(-20_000)
121
+ for (const line of chunk.toString().split('\n').filter(Boolean)) this.#log.debug(`presmium serve: ${line}`)
122
+ })
123
+ child.once('exit', (code, signal) => {
124
+ clearTimeout(timer)
125
+ if (!this.#closed) this.#log.warn(`presmium serve exited (${signal ?? code})`)
126
+ reject(new Error(`presmium serve exited (${signal ?? code}):\n${output.slice(-2000)}`))
127
+ })
128
+ })
129
+ }
130
+
131
+ async request(path: string, init: { method?: string, body?: unknown, raw?: ArrayBuffer, headers?: Record<string, string>, signal?: AbortSignal } = {}): Promise<Response> {
132
+ const { url, token } = await this.ready
133
+ const json = init.body !== undefined
134
+ return fetch(`${url}${path}`, {
135
+ method: init.method ?? (json || init.raw ? 'POST' : 'GET'),
136
+ headers: { ...json ? { 'Content-Type': 'application/json' } : {}, ...init.headers, ...token ? { Authorization: `Bearer ${token}` } : {} },
137
+ ...json ? { body: JSON.stringify(init.body) } : init.raw ? { body: init.raw } : {},
138
+ ...init.signal ? { signal: init.signal } : {},
139
+ })
140
+ }
141
+
142
+ async #json<T>(path: string, init?: Parameters<PresmiumService['request']>[1]): Promise<T> {
143
+ const response = await this.request(path, init)
144
+ const text = await response.text()
145
+ if (!response.ok) throw new Error(`${path} answered ${response.status}: ${text}`)
146
+ return JSON.parse(text) as T
147
+ }
148
+
149
+ documents(): Promise<WorkspaceDocument[]> {
150
+ if (!this.#documents || Date.now() - this.#documents.at > DOCUMENTS_MS) {
151
+ const list = this.#json<{ documents: WorkspaceDocument[] }>('/__presmium/presmium.workspace/documents').then(answer => answer.documents)
152
+ this.#documents = { at: Date.now(), list }
153
+ list.catch(() => this.#documents = undefined)
154
+ }
155
+ return this.#documents.list
156
+ }
157
+
158
+ directoryOf(agent: DshAgent): string {
159
+ const dirs = this.#ctx.get('workingDirectory') as { get(session: unknown): string } | undefined
160
+ try {
161
+ return dirs?.get(agent.session) ?? agent.session.header?.cwd ?? process.cwd()
162
+ } catch {
163
+ return agent.session.header?.cwd ?? process.cwd()
164
+ }
165
+ }
166
+
167
+ attach(agent: DshAgent): Promise<PresmiumSession | undefined> {
168
+ const id = agent.session.id
169
+ let known = this.#opening.get(id)
170
+ if (!known) {
171
+ known = this.#attach(agent).catch((error: unknown) => {
172
+ this.#log.warn(`presmium-service: the session ${id} has no Presmium tools: ${error instanceof Error ? error.message : String(error)}`)
173
+ this.#opening.delete(id)
174
+ return undefined
175
+ })
176
+ this.#opening.set(id, known)
177
+ }
178
+ return known
179
+ }
180
+
181
+ async #attach(agent: DshAgent): Promise<PresmiumSession | undefined> {
182
+ const { root } = await this.ready
183
+ const id = agent.session.id
184
+ const documents = await this.documents()
185
+ const selected = this.#selected.get(id)
186
+ const document = selected === undefined ? documentAt(root, this.directoryOf(agent), documents) : documents.find(doc => doc.dir === selected)
187
+ if (!document) return undefined
188
+ const grant = await this.#json<{ url: string, token: string }>('/__presmium/presmium.server/tokens', { body: { document: document.dir, session: id } })
189
+ const client = await connectPresmiumMcp(grant.url, grant.token)
190
+ const session: PresmiumSession = { id, document, dir: resolve(root, document.dir), client, tools: await client.tools() }
191
+ this.#open.set(id, session)
192
+ return session
193
+ }
194
+
195
+ async selectDocument(id: string, document: string | null): Promise<void> {
196
+ if (document !== null && !(await this.documents()).some(doc => doc.dir === document)) throw new Error('Unknown Presmium document')
197
+ const version = (this.#selectionVersions.get(id) ?? 0) + 1
198
+ this.#selectionVersions.set(id, version)
199
+ const listener = this.#selectionListeners.get(id)
200
+ await listener?.before()
201
+ if (this.#selectionVersions.get(id) !== version) return
202
+ this.#selected.set(id, document)
203
+ await listener?.after()
204
+ }
205
+
206
+ watchSelection(id: string, listener: { before(): Promise<void>, after(): Promise<void> }): () => void {
207
+ this.#selectionListeners.set(id, listener)
208
+ return () => { if (this.#selectionListeners.get(id) === listener) this.#selectionListeners.delete(id) }
209
+ }
210
+
211
+ refresh(agent: DshAgent): Promise<PresmiumSession | undefined> {
212
+ const id = agent.session.id
213
+ let pending = this.#refreshing.get(id)
214
+ if (!pending) {
215
+ pending = this.#refresh(agent).finally(() => this.#refreshing.delete(id))
216
+ this.#refreshing.set(id, pending)
217
+ }
218
+ return pending
219
+ }
220
+
221
+ async #refresh(agent: DshAgent): Promise<PresmiumSession | undefined> {
222
+ const id = agent.session.id
223
+ const selected = this.#selected.get(id)
224
+ const current = await this.attach(agent)
225
+ if (selected === undefined || current?.document.dir === selected || (!current && selected === null)) return current
226
+ await this.detach(id, false)
227
+ return this.attach(agent)
228
+ }
229
+
230
+ session(agent: DshAgent | undefined): PresmiumSession | undefined {
231
+ return agent ? this.#open.get(agent.session.id) : undefined
232
+ }
233
+
234
+ async detach(id: string, forget = true): Promise<void> {
235
+ if (forget) {
236
+ this.#selectionVersions.delete(id)
237
+ this.#selected.delete(id)
238
+ this.#selectionListeners.delete(id)
239
+ }
240
+ const session = this.#open.get(id)
241
+ this.#open.delete(id)
242
+ this.#opening.delete(id)
243
+ if (!session) return
244
+ await session.client.close().catch(() => undefined)
245
+ await this.request('/__presmium/presmium.server/tokens', { method: 'DELETE', body: { session: id } }).then(r => r.body?.cancel(), () => undefined)
246
+ }
247
+
248
+ async close(): Promise<void> {
249
+ this.#closed = true
250
+ for (const session of this.#open.values()) await session.client.close().catch(() => undefined)
251
+ this.#open.clear()
252
+ this.#opening.clear()
253
+ this.#selected.clear()
254
+ this.#selectionVersions.clear()
255
+ this.#selectionListeners.clear()
256
+ if (this.#child) {
257
+ this.#child.kill('SIGTERM')
258
+ return
259
+ }
260
+ await this.request('/__presmium/presmium.server/tokens', { method: 'DELETE', body: {} }).then(r => r.body?.cancel(), () => undefined)
261
+ }
262
+
263
+ stop(): void {
264
+ this.#closed = true
265
+ if (this.#child?.exitCode === null) this.#child.kill('SIGTERM')
266
+ }
267
+ }
268
+
269
+ export function apply(ctx: DshContext, config: Config = {}): void {
270
+ const log = ctx.logger?.('presmium') ?? console
271
+ const service = new PresmiumService(ctx, config, log)
272
+ ctx.provide('presmium', service)
273
+ ctx.on('session/disposed', (session: { id: string }) => void service.detach(session.id))
274
+ const stop = () => service.stop()
275
+ process.once('exit', stop)
276
+ ctx.effect(() => () => {
277
+ process.off('exit', stop)
278
+ void service.close()
279
+ })
280
+ }
@@ -0,0 +1,46 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import type { DshContext } from './dsh.ts'
5
+
6
+ export const name = 'presmium-skills'
7
+ export const inject = ['skills']
8
+
9
+ const PACKAGE = fileURLToPath(new URL('../..', import.meta.url))
10
+
11
+ export function bundledSkills(): string[] {
12
+ const manifest = JSON.parse(readFileSync(join(PACKAGE, 'package.json'), 'utf8')) as { presmium?: { skills?: string[] } }
13
+ return manifest.presmium?.skills ?? []
14
+ }
15
+
16
+ export function skillDir(skill: string): string | undefined {
17
+ return [join(PACKAGE, '..', '..', '.agents', 'skills', skill), join(PACKAGE, 'skills', skill)].find(dir => existsSync(join(dir, 'SKILL.md')))
18
+ }
19
+
20
+ function field(frontmatter: string, key: string): string | undefined {
21
+ const raw = new RegExp(`^${key}:\\s*(.*)$`, 'm').exec(frontmatter)?.[1]?.trim()
22
+ if (!raw) return undefined
23
+ if (raw.startsWith('"')) {
24
+ try {
25
+ return JSON.parse(raw) as string
26
+ } catch {
27
+ return raw.slice(1, -1)
28
+ }
29
+ }
30
+ return raw.replace(/^'|'$/g, '')
31
+ }
32
+
33
+ export function apply(ctx: DshContext): void {
34
+ for (const skill of bundledSkills()) {
35
+ const dir = skillDir(skill)
36
+ if (!dir) {
37
+ ctx.logger?.('presmium').warn(`presmium-skills: the skill ${skill} is not in this package`)
38
+ continue
39
+ }
40
+ const path = join(dir, 'SKILL.md')
41
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(readFileSync(path, 'utf8'))
42
+ const description = match && field(match[1]!, 'description')
43
+ if (!match || !description) continue
44
+ ctx.skills.register({ name: field(match[1]!, 'name') ?? skill, description, content: match[2]!, path, source: 'bundled', resourceBase: { kind: 'directory', path: dir } })
45
+ }
46
+ }
@@ -0,0 +1,43 @@
1
+ import type { ListedTool } from '@lincyaw/mcp/client'
2
+ import type { DshAgent, DshContext, DshToolDefinition } from './dsh.ts'
3
+ import type { PresmiumSession } from './service.ts'
4
+
5
+ export const name = 'presmium-tools'
6
+ export const inject = ['presmium', 'tools']
7
+
8
+ export const PREFIX = 'presmium__'
9
+
10
+ function definition(session: PresmiumSession, tool: ListedTool): DshToolDefinition {
11
+ return {
12
+ name: `${PREFIX}${tool.name}`,
13
+ description: tool.description,
14
+ parameters: tool.inputSchema,
15
+ output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: String(value) }] },
16
+ async execute(args, exec) {
17
+ const result = await session.client.call(tool.name, (args ?? {}) as Record<string, unknown>, exec.signal)
18
+ if (result.isError) throw new Error(result.text)
19
+ return result.text
20
+ },
21
+ }
22
+ }
23
+
24
+ export function apply(ctx: DshContext): void {
25
+ const selections = new WeakMap<DshAgent, () => void>()
26
+ const registered = new WeakMap<DshAgent, { session?: PresmiumSession, dispose: (() => void)[] }>()
27
+ const sync = async (agent: DshAgent) => {
28
+ const session = await ctx.presmium.refresh(agent)
29
+ const previous = registered.get(agent)
30
+ if (previous && previous.session === session) return
31
+ for (const dispose of previous?.dispose ?? []) dispose()
32
+ registered.set(agent, { session, dispose: session?.tools.map(tool => agent.ctx.tools.register(definition(session, tool))) ?? [] })
33
+ }
34
+ ctx.on('agent/created', async ({ agent }: { agent: DshAgent }) => {
35
+ await sync(agent)
36
+ selections.set(agent, ctx.presmium.watchSelection(agent.session.id, { before: () => agent.whenIdle(), after: () => sync(agent) }))
37
+ })
38
+ ctx.on('agent/disposed', ({ agent }: { agent: DshAgent }) => { selections.get(agent)?.(); selections.delete(agent) })
39
+ ctx.on('agent/pre-step', async ({ agent }: { agent: DshAgent }, next: () => Promise<unknown>) => {
40
+ await sync(agent)
41
+ return next()
42
+ })
43
+ }