Three things live here: where the plugin keeps its files, how it renders a
slopo config, and how it finds, installs and invokes the binary.
The adapter is driven by behaviour observed against slopo 0.8.0 rather than by
its documentation:
* --config is a global option and must precede the subcommand
* every path is written absolute, because slopo resolves relative paths
against the process working directory, not against the config file
* unknown config keys are a hard error, so the renderer writes from a closed
allow-list
* the embedding API key is never written at all -- slopo reads
SLOPO_EMBEDDING_API_KEY from the environment ahead of the file, so the
secret stays out of every file this plugin touches. Enforced in pruneConfig
rather than at the call site, so no future caller can leak it by passing one.
explainFailure maps slopo's deliberately terse agent-facing messages onto a
message and a next step, and reads the log tail because stderr alone leaves the
agent with no move: on a real failure stderr said "Embedding failed." while the
log named the exact litellm parameter that was rejected.
installEnv redirects UV_TOOL_DIR, UV_TOOL_BIN_DIR and UV_PYTHON_INSTALL_DIR
into $DSH_HOME. Only two directories in this container are persistent mounts;
~/.local, where uv installs by default, is on the container's writable layer
and does not survive a recreation.
80 lines
3.0 KiB
JavaScript
80 lines
3.0 KiB
JavaScript
/**
|
|
* Where this plugin keeps things on disk.
|
|
*
|
|
* Every path here is deliberately under `$DSH_HOME`, because `$DSH_HOME` is the
|
|
* one directory that survives the container this harness usually runs in. The
|
|
* uv-managed slopo install and the CPython it needs would otherwise live in
|
|
* `~/.local`, which is on the container's writable layer and is lost whenever
|
|
* the container is recreated rather than merely restarted.
|
|
*
|
|
* Relative paths are never used: slopo resolves every relative path against the
|
|
* *process working directory*, not against the config file that named it, so a
|
|
* relative path in a config invoked as `--config /elsewhere/x.yaml` still lands
|
|
* in whatever directory the caller happened to be in.
|
|
*/
|
|
|
|
import { createHash } from 'node:crypto'
|
|
import { homedir } from 'node:os'
|
|
import { basename, join, resolve } from 'node:path'
|
|
|
|
/** Plugin name. Also the `$DSH_HOME/<name>` directory, matching graph-memory's shape. */
|
|
export const PLUGIN_NAME = 'dsh-slopo'
|
|
|
|
/** The dsh home directory: `$DSH_HOME`, else `~/.dsh`. */
|
|
export function resolveDshHome(env = process.env) {
|
|
const configured = typeof env.DSH_HOME === 'string' ? env.DSH_HOME.trim() : ''
|
|
return configured === '' ? join(homedir(), '.dsh') : resolve(configured)
|
|
}
|
|
|
|
/**
|
|
* A stable, filesystem-safe key for one workspace.
|
|
*
|
|
* The basename is for a human reading the directory listing; the hash is what
|
|
* actually identifies it, so two checkouts both named `app` cannot collide.
|
|
*/
|
|
export function workspaceKey(cwd) {
|
|
const absolute = resolve(cwd)
|
|
const name =
|
|
basename(absolute)
|
|
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
|
.replace(/^[-.]+|-+$/g, '') || 'workspace'
|
|
const hash = createHash('sha256').update(absolute).digest('hex').slice(0, 8)
|
|
return `${name}-${hash}`
|
|
}
|
|
|
|
/** Per-workspace state: the slopo config, index database, ignore file and log. */
|
|
export function workspaceStateDir(dshHome, cwd) {
|
|
return join(pluginDataDir(dshHome), 'workspaces', workspaceKey(cwd))
|
|
}
|
|
|
|
/** Root of this plugin's own data, independent of any workspace. */
|
|
export function pluginDataDir(dshHome) {
|
|
return join(dshHome, PLUGIN_NAME)
|
|
}
|
|
|
|
/**
|
|
* The directory `uv tool install` writes the slopo venv and shim into.
|
|
*
|
|
* Redirected here from its `~/.local/share/uv/tools` default for the durability
|
|
* reason in the file header.
|
|
*/
|
|
export function managedToolDir(dshHome) {
|
|
return join(pluginDataDir(dshHome), 'uv-tools')
|
|
}
|
|
|
|
/** The directory uv keeps its downloaded CPython builds in. */
|
|
export function managedPythonDir(dshHome) {
|
|
return join(pluginDataDir(dshHome), 'uv-python')
|
|
}
|
|
|
|
/** The slopo executable inside {@link managedToolDir}. */
|
|
export function managedSlopoPath(dshHome) {
|
|
return join(managedToolDir(dshHome), 'bin', process.platform === 'win32' ? 'slopo.exe' : 'slopo')
|
|
}
|
|
|
|
/** Slopo's own default install location, for machines that already have one. */
|
|
export function userLocalSlopoPath(env = process.env) {
|
|
const home = typeof env.HOME === 'string' && env.HOME !== '' ? env.HOME : homedir()
|
|
return join(home, '.local', 'bin', process.platform === 'win32' ? 'slopo.exe' : 'slopo')
|
|
}
|