@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.
- package/LICENSE +202 -0
- package/README.md +22 -2
- package/build/client.mjs +66 -0
- package/build/dist/client.js +90 -0
- package/build/dist/client.json +70 -0
- package/build/ensure.mjs +30 -0
- package/cordis.patch.yml +17 -0
- package/package.json +95 -3
- package/profile.patch.yml +25 -0
- package/rows/context.mjs +7 -0
- package/rows/load.mjs +8 -0
- package/rows/policy.mjs +7 -0
- package/rows/routes.mjs +7 -0
- package/rows/service.mjs +7 -0
- package/rows/skills.mjs +7 -0
- package/rows/tools.mjs +7 -0
- package/rows/turns.mjs +7 -0
- package/rows/workbench.mjs +9 -0
- package/rows/workspaces.mjs +7 -0
- package/skills/presmium-author/SKILL.md +95 -0
- package/skills/presmium-author/references/assistance.md +27 -0
- package/skills/presmium-author/references/code.md +52 -0
- package/skills/presmium-author/references/directives.md +592 -0
- package/skills/presmium-author/references/publishing.md +60 -0
- package/skills/presmium-author/references/writing.md +72 -0
- package/skills/presmium-present/SKILL.md +69 -0
- package/skills/presmium-present/references/classroom.md +46 -0
- package/skills/presmium-present/references/delivery.md +63 -0
- package/skills/presmium-present/references/lecture.md +58 -0
- package/src/client/Frame.tsx +314 -0
- package/src/client/brand.tsx +90 -0
- package/src/client/chrome.tsx +79 -0
- package/src/client/contract.ts +35 -0
- package/src/client/dsh.d.ts +129 -0
- package/src/client/fallback.tsx +49 -0
- package/src/client/frame.css +345 -0
- package/src/client/index.tsx +154 -0
- package/src/client/layout.ts +217 -0
- package/src/client/messages.ts +78 -0
- package/src/client/source.tsx +81 -0
- package/src/client/styles.css +3 -0
- package/src/client/theme.ts +149 -0
- package/src/node/context.ts +55 -0
- package/src/node/dsh.ts +93 -0
- package/src/node/launch.ts +59 -0
- package/src/node/policy.ts +59 -0
- package/src/node/routes.ts +46 -0
- package/src/node/service.ts +280 -0
- package/src/node/skills.ts +46 -0
- package/src/node/tools.ts +43 -0
- package/src/node/turns/index.ts +165 -0
- package/src/node/watchdog.mjs +36 -0
- package/src/node/workbench.ts +43 -0
- package/src/node/workspaces.ts +10 -0
- package/web.patch.yml +190 -0
package/src/node/dsh.ts
ADDED
|
@@ -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
|
+
}
|