@lincyaw/dsh 0.1.0 → 0.2.0-rc.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/README.md +8 -4
- package/build/client.mjs +7 -1
- package/build/dist/client.js +1364 -64
- package/build/dist/client.json +225 -21
- package/build/ensure.mjs +11 -2
- package/cordis.patch.yml +9 -0
- package/entries/events.mjs +7 -0
- package/entries/load.mjs +11 -0
- package/entries/telemetry.mjs +7 -0
- package/entries/turns.mjs +7 -0
- package/package.json +32 -19
- package/src/client/RootLayout.tsx +332 -0
- package/src/client/agent-requests.ts +64 -0
- package/src/client/assistant-setup.tsx +185 -0
- package/src/client/author-settings.ts +57 -0
- package/src/client/brand.tsx +21 -14
- package/src/client/contract.ts +48 -1
- package/src/client/document-context.ts +39 -0
- package/src/client/dsh.d.ts +27 -1
- package/src/client/fallback.tsx +23 -19
- package/src/client/frame.css +280 -120
- package/src/client/index.tsx +72 -69
- package/src/client/launch-target.ts +89 -0
- package/src/client/layout.ts +90 -77
- package/src/client/mascot.tsx +37 -0
- package/src/client/messages.ts +142 -49
- package/src/client/{chrome.tsx → navigation-shell.tsx} +20 -25
- package/src/client/observable.ts +16 -0
- package/src/client/paths.ts +17 -0
- package/src/client/root-reload.ts +163 -0
- package/src/client/runtime-diagnostics.tsx +228 -0
- package/src/client/settings-opener.ts +26 -0
- package/src/client/{source.tsx → source-links.tsx} +16 -26
- package/src/client/speech-settings.tsx +7 -0
- package/src/client/styles.css +42 -0
- package/src/client/theme.ts +4 -2
- package/src/client/workbench-keys.ts +36 -0
- package/src/client/workspace-events.ts +114 -0
- package/src/node/bindings.ts +345 -0
- package/src/node/context.ts +131 -20
- package/src/node/{dsh.ts → dsh-types.ts} +33 -2
- package/src/node/fake-collector.ts +46 -0
- package/src/node/fake-model.ts +80 -0
- package/src/node/launch.ts +127 -10
- package/src/node/policy.ts +20 -23
- package/src/node/routes.ts +21 -11
- package/src/node/server-routes.ts +3 -0
- package/src/node/service.ts +58 -207
- package/src/node/skills.ts +9 -31
- package/src/node/telemetry.ts +126 -0
- package/src/node/tools.ts +34 -11
- package/src/node/{turns/index.ts → turns.ts} +48 -37
- package/src/node/workbench.ts +51 -6
- package/src/node/workspace-events.ts +167 -0
- package/src/node/workspaces.ts +2 -2
- package/src/workspace-events.ts +31 -0
- package/web.patch.yml +27 -56
- package/rows/load.mjs +0 -8
- package/rows/turns.mjs +0 -7
- package/skills/presmium-author/SKILL.md +0 -95
- package/skills/presmium-author/references/assistance.md +0 -27
- package/skills/presmium-author/references/code.md +0 -52
- package/skills/presmium-author/references/directives.md +0 -592
- package/skills/presmium-author/references/publishing.md +0 -60
- package/skills/presmium-author/references/writing.md +0 -72
- package/skills/presmium-present/SKILL.md +0 -69
- package/skills/presmium-present/references/classroom.md +0 -46
- package/skills/presmium-present/references/delivery.md +0 -63
- package/skills/presmium-present/references/lecture.md +0 -58
- package/src/client/Frame.tsx +0 -314
- /package/{rows → entries}/context.mjs +0 -0
- /package/{rows → entries}/policy.mjs +0 -0
- /package/{rows → entries}/routes.mjs +0 -0
- /package/{rows → entries}/service.mjs +0 -0
- /package/{rows → entries}/skills.mjs +0 -0
- /package/{rows → entries}/tools.mjs +0 -0
- /package/{rows → entries}/workbench.mjs +0 -0
- /package/{rows → entries}/workspaces.mjs +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createServer } from 'node:http'
|
|
2
|
+
import type { IncomingHttpHeaders } from 'node:http'
|
|
3
|
+
import type { AddressInfo } from 'node:net'
|
|
4
|
+
|
|
5
|
+
export type Step = { text: string } | { tool: string, input: Record<string, unknown> }
|
|
6
|
+
|
|
7
|
+
export interface Received {
|
|
8
|
+
path: string
|
|
9
|
+
headers: IncomingHttpHeaders
|
|
10
|
+
body: Record<string, any>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FakeModel {
|
|
14
|
+
url: string
|
|
15
|
+
received: Received[]
|
|
16
|
+
hold(): void
|
|
17
|
+
release(): void
|
|
18
|
+
close(): Promise<void>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const event = (name: string, data: unknown) => `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`
|
|
22
|
+
|
|
23
|
+
function stream(step: Step, n: number): string {
|
|
24
|
+
const start = { type: 'message_start', message: { id: `msg_${n}`, type: 'message', role: 'assistant', model: 'fake-model', content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 10, output_tokens: 0 } } }
|
|
25
|
+
const block = 'text' in step
|
|
26
|
+
? [event('content_block_start', { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }), event('content_block_delta', { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: step.text } })]
|
|
27
|
+
: [event('content_block_start', { type: 'content_block_start', index: 0, content_block: { type: 'tool_use', id: `toolu_${n}`, name: step.tool, input: {} } }), event('content_block_delta', { type: 'content_block_delta', index: 0, delta: { type: 'input_json_delta', partial_json: JSON.stringify(step.input) } })]
|
|
28
|
+
return [
|
|
29
|
+
event('message_start', start),
|
|
30
|
+
...block,
|
|
31
|
+
event('content_block_stop', { type: 'content_block_stop', index: 0 }),
|
|
32
|
+
event('message_delta', { type: 'message_delta', delta: { stop_reason: 'text' in step ? 'end_turn' : 'tool_use', stop_sequence: null }, usage: { output_tokens: 5 } }),
|
|
33
|
+
event('message_stop', { type: 'message_stop' }),
|
|
34
|
+
].join('')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function fakeModel(script: Step[]): Promise<FakeModel> {
|
|
38
|
+
const received: Received[] = []
|
|
39
|
+
let held: Promise<void> | undefined
|
|
40
|
+
let release = () => {}
|
|
41
|
+
const server = createServer((req, res) => {
|
|
42
|
+
let body = ''
|
|
43
|
+
req.setEncoding('utf8').on('data', (chunk: string) => body += chunk)
|
|
44
|
+
req.on('end', async () => {
|
|
45
|
+
let parsed: Record<string, any> = {}
|
|
46
|
+
try {
|
|
47
|
+
parsed = JSON.parse(body) as Record<string, any>
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
const record: Received = { path: req.url ?? '', headers: req.headers, body: parsed }
|
|
51
|
+
received.push(record)
|
|
52
|
+
if (req.method !== 'POST' || !(req.url ?? '').endsWith('/v1/messages')) {
|
|
53
|
+
res.writeHead(404).end()
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
await held
|
|
57
|
+
const turn = (r: Received) => r.path.endsWith('/v1/messages') && Array.isArray(r.body.tools) && r.body.tools.length > 0
|
|
58
|
+
const n = received.filter(r => r.path.endsWith('/v1/messages')).length
|
|
59
|
+
const step = turn(record) ? script[received.filter(turn).indexOf(record)] ?? { text: '完毕。' } : { text: 'Presmium' }
|
|
60
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream' }).end(stream(step, n))
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
const port = await new Promise<number>(done => server.listen(0, '127.0.0.1', () => done((server.address() as AddressInfo).port)))
|
|
64
|
+
return {
|
|
65
|
+
url: `http://127.0.0.1:${port}`,
|
|
66
|
+
received,
|
|
67
|
+
hold() {
|
|
68
|
+
held = new Promise(done => release = done)
|
|
69
|
+
},
|
|
70
|
+
release() {
|
|
71
|
+
release()
|
|
72
|
+
held = undefined
|
|
73
|
+
},
|
|
74
|
+
close: () => new Promise<void>((done) => {
|
|
75
|
+
release()
|
|
76
|
+
server.close(() => done())
|
|
77
|
+
server.closeAllConnections()
|
|
78
|
+
}),
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/node/launch.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import type { ChildProcess } from 'node:child_process'
|
|
1
3
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
|
2
4
|
import { createRequire } from 'node:module'
|
|
3
5
|
import { dirname, join } from 'node:path'
|
|
4
6
|
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { machineState, NODE_RESOURCE_ARGS, nodeResourceArgs, resourceEnvironment } from '@lincyaw/kernel/node'
|
|
8
|
+
export { bracketIPv6, systemLocale } from '@lincyaw/kernel/node'
|
|
5
9
|
|
|
6
10
|
export type Carrier = 'web' | 'headless' | 'acp'
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
const PACKAGE_DIR = fileURLToPath(new URL('../..', import.meta.url))
|
|
13
|
+
const PACKAGE_NAME = '@lincyaw/dsh'
|
|
14
|
+
const WATCHDOG = join(PACKAGE_DIR, 'src', 'node', 'watchdog.mjs')
|
|
15
|
+
const READY_MS = 180_000
|
|
16
|
+
const STOP_MS = 15_000
|
|
10
17
|
|
|
11
18
|
const CARRIERS: Record<Carrier, string> = {
|
|
12
19
|
web: '@deepseek-ai/dsh-web-app',
|
|
@@ -22,13 +29,10 @@ export function launchArgs(name: string, carrier: Carrier): string[] {
|
|
|
22
29
|
return ['--profile', name, ...launchPatches(carrier).flatMap(file => ['--patch', file])]
|
|
23
30
|
}
|
|
24
31
|
|
|
25
|
-
export const WATCHDOG = join(PACKAGE_DIR, 'src', 'node', 'watchdog.mjs')
|
|
26
|
-
|
|
27
32
|
export const WEB_READY = /^dsh web: (https?:\/\/\S+)$/m
|
|
28
|
-
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
}
|
|
33
|
+
/** The two lines `presmium serve` prints when it is ready: its address and the workspace root. */
|
|
34
|
+
export const SERVE_READY = /^serve {2}(http\S+)$/m
|
|
35
|
+
export const WORKSPACE_READY = /^workspace {2}(.+)$/m
|
|
32
36
|
|
|
33
37
|
export function dshBin(from = PACKAGE_DIR): string {
|
|
34
38
|
const manifest = createRequire(join(from, 'package.json')).resolve('@deepseek-ai/dsh/package.json')
|
|
@@ -36,8 +40,121 @@ export function dshBin(from = PACKAGE_DIR): string {
|
|
|
36
40
|
return join(dirname(manifest), typeof bin === 'string' ? bin : bin.dsh!)
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
export function launchEnv(dshHome: string): Record<string, string> {
|
|
40
|
-
return { DSH_HOME: dshHome, DSH_AGENTS_HOME: join(dshHome, 'agents'), DSH_TELEMETRY_MODE: 'DISABLED' }
|
|
43
|
+
export function launchEnv(dshHome: string, env: NodeJS.ProcessEnv = process.env): Record<string, string> {
|
|
44
|
+
return resourceEnvironment({ DSH_HOME: dshHome, DSH_AGENTS_HOME: join(dshHome, 'agents'), DSH_TELEMETRY_MODE: 'DISABLED', PRESMIUM_MAX_THREADS: env.PRESMIUM_MAX_THREADS }, 'interactive')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The variables a launcher sets for the processes it starts; they are never inherited from its own environment. */
|
|
48
|
+
export function isLauncherVariable(name: string): boolean {
|
|
49
|
+
return /^(?:DSH_|PRESMIUM_MODEL_|PRESMIUM_SERVICE_)/.test(name) || ['PRESMIUM_WORKSPACE', 'NODE_ENV', 'PORT'].includes(name)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function inheritedEnv(env: NodeJS.ProcessEnv, drop: (name: string) => boolean): Record<string, string> {
|
|
53
|
+
return Object.fromEntries(Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined && !drop(entry[0])))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The environment of the workspace server: the launcher's, without the variables of DeepSeek Harness, of its model and of the launcher, with the service token. */
|
|
57
|
+
export function workspaceServerEnv(env: NodeJS.ProcessEnv, serviceToken: string): Record<string, string> {
|
|
58
|
+
return { ...inheritedEnv(env, name => isLauncherVariable(name) || name.startsWith('DEEPSEEK_')), PRESMIUM_SERVICE_TOKEN: serviceToken }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface LaunchedProcess {
|
|
62
|
+
child: ChildProcess
|
|
63
|
+
/** The first match of `pattern` in the output, once it is printed; rejects when the process ends first or takes over 3 minutes. */
|
|
64
|
+
ready(pattern: RegExp): Promise<RegExpExecArray>
|
|
65
|
+
/** Stops keeping the output for `ready`, once the process is ready. */
|
|
66
|
+
stopBuffering(): void
|
|
67
|
+
exited: Promise<void>
|
|
68
|
+
/** SIGTERM, then SIGKILL to the whole process group after 15 s. */
|
|
69
|
+
stop(): Promise<void>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Starts `node <args>` under the watchdog, in a process group of its own, with the interactive thread budget at normal
|
|
74
|
+
* priority: the author waits for the workspace service and DeepSeek Harness. The watchdog ends the group when the
|
|
75
|
+
* launcher's pipe to it closes, so the group ends with the launcher, after `kill -9` too.
|
|
76
|
+
*/
|
|
77
|
+
export function launchNode(name: string, args: string[], options: { cwd: string, env: Record<string, string>, onOutput?(text: string): void }): LaunchedProcess {
|
|
78
|
+
const machine = machineState()
|
|
79
|
+
const node = nodeResourceArgs('interactive', options.env, machine)
|
|
80
|
+
const child = spawn(process.execPath, [...NODE_RESOURCE_ARGS, WATCHDOG, process.execPath, ...node, ...args], { cwd: options.cwd, env: resourceEnvironment(options.env, 'interactive', machine), detached: true, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
81
|
+
child.stdin!.on('error', () => {})
|
|
82
|
+
let output: string | undefined = ''
|
|
83
|
+
const waiting = new Set<() => void>()
|
|
84
|
+
const read = (chunk: Buffer) => {
|
|
85
|
+
const text = chunk.toString()
|
|
86
|
+
options.onOutput?.(text)
|
|
87
|
+
if (output === undefined) return
|
|
88
|
+
output = (output + text).slice(-20_000)
|
|
89
|
+
for (const check of waiting) check()
|
|
90
|
+
}
|
|
91
|
+
child.stdout!.on('data', read)
|
|
92
|
+
child.stderr!.on('data', read)
|
|
93
|
+
let ended = false
|
|
94
|
+
const exited = new Promise<void>(done => child.once('exit', () => {
|
|
95
|
+
ended = true
|
|
96
|
+
done()
|
|
97
|
+
}))
|
|
98
|
+
return {
|
|
99
|
+
child,
|
|
100
|
+
exited,
|
|
101
|
+
ready(pattern) {
|
|
102
|
+
return new Promise((resolveReady, reject) => {
|
|
103
|
+
const tail = () => (output ?? '').trimEnd().split('\n').slice(-12).map(line => ` ${line}`).join('\n')
|
|
104
|
+
const timer = setTimeout(() => finish(new Error(`${name} did not start within ${READY_MS / 1000} s:\n${tail()}\n`)), READY_MS)
|
|
105
|
+
const check = () => {
|
|
106
|
+
const match = pattern.exec(output ?? '')
|
|
107
|
+
if (match) finish(match)
|
|
108
|
+
}
|
|
109
|
+
const finish = (result: RegExpExecArray | Error) => {
|
|
110
|
+
clearTimeout(timer)
|
|
111
|
+
waiting.delete(check)
|
|
112
|
+
if (result instanceof Error) reject(result)
|
|
113
|
+
else resolveReady(result)
|
|
114
|
+
}
|
|
115
|
+
waiting.add(check)
|
|
116
|
+
void exited.then(() => finish(new Error(`${name} ended before it was ready:\n${tail()}\n`)))
|
|
117
|
+
check()
|
|
118
|
+
})
|
|
119
|
+
},
|
|
120
|
+
stopBuffering() {
|
|
121
|
+
output = undefined
|
|
122
|
+
},
|
|
123
|
+
async stop() {
|
|
124
|
+
if (ended) return
|
|
125
|
+
child.kill('SIGTERM')
|
|
126
|
+
const timer = setTimeout(() => {
|
|
127
|
+
try {
|
|
128
|
+
process.kill(-child.pid!, 'SIGKILL')
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
}, STOP_MS)
|
|
132
|
+
await exited
|
|
133
|
+
clearTimeout(timer)
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface LaunchedWorkspaceServer {
|
|
139
|
+
process: LaunchedProcess
|
|
140
|
+
/** The server's address and the workspace root, once `presmium serve` printed them. */
|
|
141
|
+
ready: Promise<{ url: string, root: string }>
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Starts `presmium serve <dir> --port <port, default 0: a free one> [args]` with `launchNode`, in `workspaceServerEnv`. */
|
|
145
|
+
export function launchWorkspaceServer(options: { presmium: string, dir: string, serviceToken: string, port?: number, args?: string[], env?: NodeJS.ProcessEnv, onOutput?(text: string): void }): LaunchedWorkspaceServer {
|
|
146
|
+
const server = launchNode('presmium serve', [options.presmium, 'serve', options.dir, '--port', String(options.port ?? 0), ...options.args ?? []], {
|
|
147
|
+
cwd: options.dir,
|
|
148
|
+
env: workspaceServerEnv(options.env ?? process.env, options.serviceToken),
|
|
149
|
+
...options.onOutput ? { onOutput: options.onOutput } : {},
|
|
150
|
+
})
|
|
151
|
+
const ready = (async () => {
|
|
152
|
+
const url = (await server.ready(SERVE_READY))[1]!
|
|
153
|
+
const root = (await server.ready(WORKSPACE_READY))[1]!.trim()
|
|
154
|
+
server.stopBuffering()
|
|
155
|
+
return { url, root }
|
|
156
|
+
})()
|
|
157
|
+
return { process: server, ready }
|
|
41
158
|
}
|
|
42
159
|
|
|
43
160
|
export function writeProfile(dshHome: string, name: string, carrier: Carrier, bundle = PACKAGE_DIR): string {
|
package/src/node/policy.ts
CHANGED
|
@@ -1,59 +1,56 @@
|
|
|
1
1
|
import { homedir } from 'node:os'
|
|
2
|
-
import {
|
|
2
|
+
import { join, resolve } from 'node:path'
|
|
3
|
+
import { isInside } from '@lincyaw/kernel/node'
|
|
3
4
|
import { describeOperation } from '@lincyaw/mcp/client'
|
|
4
|
-
import type { DshContext, DshPreDecision, DshToolExecution } from './dsh.ts'
|
|
5
|
+
import type { DshContext, DshPreDecision, DshToolExecution } from './dsh-types.ts'
|
|
5
6
|
import { PREFIX } from './tools.ts'
|
|
6
7
|
|
|
7
8
|
export const name = 'presmium-policy'
|
|
8
9
|
export const inject = ['presmium', 'tools']
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
+
const FILE_WRITE_TOOLS = new Set(['write', 'edit', 'str_replace_editor'])
|
|
11
12
|
|
|
12
13
|
export function operatorRoot(env: NodeJS.ProcessEnv = process.env): string {
|
|
13
14
|
return resolve(env.PRESMIUM_OPERATOR_DIR || join(env.XDG_STATE_HOME || join(homedir(), '.local', 'state'), 'presmium', 'operator'))
|
|
14
15
|
}
|
|
15
16
|
|
|
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
17
|
function fileOf(exec: Readonly<DshToolExecution>): string | undefined {
|
|
22
18
|
const args = exec.arguments as { file_path?: unknown, path?: unknown } | undefined
|
|
23
19
|
const path = args?.file_path ?? args?.path
|
|
24
20
|
return typeof path === 'string' && path !== '' ? path : undefined
|
|
25
21
|
}
|
|
26
22
|
|
|
27
|
-
const
|
|
23
|
+
const displayText = (text: string, lang?: string) => ({ en: text, ...lang ? { [lang]: text } : {} })
|
|
28
24
|
|
|
29
25
|
export function apply(ctx: DshContext): void {
|
|
30
|
-
const
|
|
26
|
+
const operatorDir = operatorRoot()
|
|
31
27
|
|
|
32
28
|
ctx.on('tools/pre-execute', async (exec: DshToolExecution, next: () => Promise<DshPreDecision>): Promise<DshPreDecision> => {
|
|
33
|
-
const
|
|
29
|
+
const { bindings } = ctx.presmium
|
|
30
|
+
const binding = bindings.bindingOf(exec.agent)
|
|
34
31
|
if (exec.name.startsWith(PREFIX)) {
|
|
35
|
-
const tool =
|
|
36
|
-
if (!
|
|
32
|
+
const tool = binding?.tools.find(own => `${PREFIX}${own.name}` === exec.name)
|
|
33
|
+
if (!binding || !tool) return { kind: 'deny', reason: `Presmium: ${exec.name} is no tool of this session's document` }
|
|
37
34
|
if (tool.kind === 'read') return next()
|
|
38
35
|
if (tool.approval === 'allow') return { kind: 'allow' }
|
|
39
|
-
const lang =
|
|
36
|
+
const lang = binding.document.lang
|
|
40
37
|
const title = describeOperation(tool.name, (exec.arguments ?? {}) as Record<string, unknown>, lang)
|
|
41
|
-
return { kind: 'ask', reason: `presmium:${tool.kind} ${tool.name}`, displayReason:
|
|
38
|
+
return { kind: 'ask', reason: `presmium:${tool.kind} ${tool.name}`, displayReason: displayText(`Presmium · ${title}`, lang) }
|
|
42
39
|
}
|
|
43
|
-
const file =
|
|
44
|
-
if (file &&
|
|
45
|
-
if (
|
|
46
|
-
return { kind: 'ask', reason: `presmium:write ${file}`, displayReason:
|
|
40
|
+
const file = FILE_WRITE_TOOLS.has(exec.name) ? fileOf(exec) : undefined
|
|
41
|
+
if (file && isInside(resolve(exec.agent ? bindings.workingDirectoryOf(exec.agent) : process.cwd(), file), operatorDir)) return next()
|
|
42
|
+
if (binding && file && !isInside(resolve(bindings.workingDirectoryOf(exec.agent!), file), bindings.workingDirectoryOf(exec.agent!))) {
|
|
43
|
+
return { kind: 'ask', reason: `presmium:write ${file}`, displayReason: displayText(`Presmium · ${file}`, binding.document.lang) }
|
|
47
44
|
}
|
|
48
45
|
return next()
|
|
49
46
|
})
|
|
50
47
|
|
|
51
48
|
ctx.tools.guard((exec) => {
|
|
52
|
-
const file =
|
|
53
|
-
const dir = exec.agent ? ctx.presmium.
|
|
54
|
-
if (file &&
|
|
49
|
+
const file = FILE_WRITE_TOOLS.has(exec.name) ? fileOf(exec) : undefined
|
|
50
|
+
const dir = exec.agent ? ctx.presmium.bindings.workingDirectoryOf(exec.agent) : process.cwd()
|
|
51
|
+
if (file && isInside(resolve(dir, file), operatorDir)) return `Presmium: the operator directory (${operatorDir}) is not the agent's to write`
|
|
55
52
|
const command = exec.name === 'bash' ? (exec.arguments as { command?: unknown } | undefined)?.command : undefined
|
|
56
|
-
if (typeof command === 'string' && command.includes(
|
|
53
|
+
if (typeof command === 'string' && command.includes(operatorDir)) return `Presmium: the operator directory (${operatorDir}) is not the agent's to touch`
|
|
57
54
|
return undefined
|
|
58
55
|
})
|
|
59
56
|
}
|
package/src/node/routes.ts
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
import type { DshContext } from './dsh.ts'
|
|
1
|
+
import type { DshContext } from './dsh-types.ts'
|
|
2
|
+
import { SERVER_ROUTES, WORKSPACE_ROUTES } from './server-routes.ts'
|
|
2
3
|
|
|
3
4
|
export const name = 'presmium-routes'
|
|
4
5
|
export const inject = ['presmium', 'connection']
|
|
5
6
|
|
|
6
|
-
const WORKSPACE = '/__presmium/presmium.workspace/'
|
|
7
|
-
const SERVER = '/__presmium/presmium.server/'
|
|
8
|
-
|
|
9
7
|
export const ROUTES: Record<string, string> = Object.fromEntries([
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
'
|
|
13
|
-
|
|
8
|
+
['speech-settings', `${WORKSPACE_ROUTES}speech-settings`],
|
|
9
|
+
...['project/capabilities', 'project/plan', 'project/apply', 'documents', 'document', 'document/capabilities', 'document/outline', 'document/part', 'document/page/plan', 'document/page/apply', 'document/action/plan', 'document/action/impact', 'document/action/apply', 'document/asset', 'document/events', 'document/change', 'document/image', 'document/audio', 'document/recording/adopt', 'document/media-clip', 'files', 'source', 'source/create', 'source/transaction', 'changes', 'changes/commit', 'changes/discard', 'revisions', 'revisions/revert', 'history/list', 'history/file', 'history/undo', 'history/redo',
|
|
10
|
+
'document/media-clip/validate', 'document/transcription/capabilities', 'document/transcription', 'document/transcription/result', 'document/transcription/cancel',
|
|
11
|
+
'document/transcription/adoption/plan', 'document/transcription/adoption/apply',
|
|
12
|
+
'ir', 'ir/events', 'preview', 'build', 'check', 'jobs', 'jobs/cancel', 'jobs/events', 'voice-catalog', 'voice-capabilities',
|
|
13
|
+
'review', 'review/open', 'review/step', 'review/accept', 'review/reject', 'review/restore'].map(path => [path, `${WORKSPACE_ROUTES}${path}`]),
|
|
14
|
+
['focus', `${SERVER_ROUTES}focus`],
|
|
15
|
+
['control', `${SERVER_ROUTES}control`],
|
|
16
|
+
['diagnostics', `${SERVER_ROUTES}diagnostics`],
|
|
17
|
+
['diagnostics/events', `${SERVER_ROUTES}diagnostics/events`],
|
|
18
|
+
['diagnostics/authoring', `${SERVER_ROUTES}diagnostics/authoring`],
|
|
19
|
+
...['voiceover', 'voiceover/jobs', 'voiceover/job', 'voiceover/cancel', 'voice-preview', 'voice-preview/media', 'production-plan', 'production-media', 'production-asset'].map(path => [path, `/__presmium/presmium.lecture/${path}`]),
|
|
20
|
+
// Training a cloned voice and its status: the speech plugin's owner routes.
|
|
21
|
+
['voice-clone', '/__presmium/presmium.speech/clone'],
|
|
22
|
+
['voice-clone/status', '/__presmium/presmium.speech/clone/status'],
|
|
14
23
|
])
|
|
15
24
|
|
|
16
|
-
const PASSED = ['accept', 'content-type', 'presmium-document', 'last-event-id']
|
|
17
|
-
const RETURNED = ['content-type', 'cache-control', 'content-disposition', 'x-accel-buffering']
|
|
25
|
+
const PASSED = ['accept', 'content-type', 'presmium-document', 'last-event-id', 'if-none-match']
|
|
26
|
+
const RETURNED = ['content-type', 'cache-control', 'content-disposition', 'content-security-policy', 'x-content-type-options', 'cross-origin-resource-policy', 'x-accel-buffering', 'etag', 'retry-after']
|
|
18
27
|
|
|
19
28
|
export function apply(ctx: DshContext): void {
|
|
20
29
|
for (const [path, target] of Object.entries(ROUTES)) {
|
|
@@ -28,12 +37,13 @@ export function apply(ctx: DshContext): void {
|
|
|
28
37
|
const value = request.headers.get(name)
|
|
29
38
|
if (value !== null) headers[name] = value
|
|
30
39
|
}
|
|
31
|
-
const upstream = await ctx.presmium.request(`${target}${new URL(request.url).search}`, {
|
|
40
|
+
const upstream = await ctx.presmium.server.request(`${target}${new URL(request.url).search}`, {
|
|
32
41
|
method: request.method,
|
|
33
42
|
headers,
|
|
34
43
|
...request.method === 'POST' ? { raw: await request.arrayBuffer() } : {},
|
|
35
44
|
signal: request.signal,
|
|
36
45
|
})
|
|
46
|
+
if ((path === 'documents' || path === 'project/apply') && request.method === 'POST' && upstream.ok) ctx.presmium.server.invalidateDocuments()
|
|
37
47
|
const answered = new Headers()
|
|
38
48
|
for (const name of RETURNED) {
|
|
39
49
|
const value = upstream.headers.get(name)
|