@moxxy/cli 0.1.5 → 0.2.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/dist/bin.js +1559 -932
- package/dist/bin.js.map +1 -1
- package/dist/read-handler.js +1 -2
- package/dist/read-handler.js.map +1 -1
- package/dist/skills/self-update.md +76 -0
- package/dist/skills/vault-setup.md +1 -1
- package/package.json +1 -1
package/dist/read-handler.js
CHANGED
|
@@ -8,7 +8,6 @@ function resolvePath(cwd, target) {
|
|
|
8
8
|
if (path.isAbsolute(target)) return path.normalize(target);
|
|
9
9
|
return path.resolve(cwd, target);
|
|
10
10
|
}
|
|
11
|
-
var resolveSafe = resolvePath;
|
|
12
11
|
function clampString(s, max) {
|
|
13
12
|
if (s.length <= max) return s;
|
|
14
13
|
return s.slice(0, max) + `
|
|
@@ -18,7 +17,7 @@ function clampString(s, max) {
|
|
|
18
17
|
// ../tools-builtin/src/read-handler.ts
|
|
19
18
|
async function readHandler(input, ctx) {
|
|
20
19
|
const { file_path, offset = 0, limit = 2e3 } = input;
|
|
21
|
-
const resolved =
|
|
20
|
+
const resolved = resolvePath(ctx.cwd, file_path);
|
|
22
21
|
const text = ctx.fs ? await ctx.fs.readFile(resolved, { encoding: "utf8" }) : (await promises.readFile(resolved)).toString("utf8");
|
|
23
22
|
const lines = text.split("\n");
|
|
24
23
|
const sliced = lines.slice(offset, offset + limit);
|
package/dist/read-handler.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../tools-builtin/src/util.ts","../../tools-builtin/src/read-handler.ts"],"names":["fs"],"mappings":";;;;;;AAeO,SAAS,WAAA,CAAY,KAAa,MAAA,EAAwB;AAC/D,EAAA,IAAS,IAAA,CAAA,UAAA,CAAW,MAAM,CAAA,EAAG,OAAY,eAAU,MAAM,CAAA;AACzD,EAAA,OAAY,IAAA,CAAA,OAAA,CAAQ,KAAK,MAAM,CAAA;AACjC;
|
|
1
|
+
{"version":3,"sources":["../../tools-builtin/src/util.ts","../../tools-builtin/src/read-handler.ts"],"names":["fs"],"mappings":";;;;;;AAeO,SAAS,WAAA,CAAY,KAAa,MAAA,EAAwB;AAC/D,EAAA,IAAS,IAAA,CAAA,UAAA,CAAW,MAAM,CAAA,EAAG,OAAY,eAAU,MAAM,CAAA;AACzD,EAAA,OAAY,IAAA,CAAA,OAAA,CAAQ,KAAK,MAAM,CAAA;AACjC;AAgCO,SAAS,WAAA,CAAY,GAAW,GAAA,EAAqB;AAC1D,EAAA,IAAI,CAAA,CAAE,MAAA,IAAU,GAAA,EAAK,OAAO,CAAA;AAC5B,EAAA,OAAO,CAAA,CAAE,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA,GAAI;AAAA,eAAA,EAAoB,CAAA,CAAE,SAAS,GAAG,CAAA,OAAA,CAAA;AAC7D;;;AC5BA,eAAsB,WAAA,CAAY,OAAkB,GAAA,EAAmC;AACrF,EAAA,MAAM,EAAE,SAAA,EAAW,MAAA,GAAS,CAAA,EAAG,KAAA,GAAQ,KAAK,GAAI,KAAA;AAChD,EAAA,MAAM,QAAA,GAAW,WAAA,CAAY,GAAA,CAAI,GAAA,EAAK,SAAS,CAAA;AAO/C,EAAA,MAAM,OAAO,GAAA,CAAI,EAAA,GACb,MAAM,GAAA,CAAI,EAAA,CAAG,SAAS,QAAA,EAAU,EAAE,UAAU,MAAA,EAAQ,KACnD,MAAMA,QAAA,CAAG,SAAS,QAAQ,CAAA,EAAG,SAAS,MAAM,CAAA;AACjD,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAC7B,EAAA,MAAM,MAAA,GAAS,KAAA,CAAM,KAAA,CAAM,MAAA,EAAQ,SAAS,KAAK,CAAA;AACjD,EAAA,MAAM,QAAA,GAAW,OACd,GAAA,CAAI,CAAC,MAAM,CAAA,KAAM,CAAA,EAAG,OAAO,MAAA,GAAS,CAAA,GAAI,CAAC,CAAA,CAAE,QAAA,CAAS,GAAG,GAAG,CAAC,IAAK,IAAI,CAAA,CAAE,CAAA,CACtE,IAAA,CAAK,IAAI,CAAA;AACZ,EAAA,OAAO,WAAA,CAAY,UAAU,GAAO,CAAA;AACtC","file":"read-handler.js","sourcesContent":["import * as path from 'node:path';\nimport { MoxxyError } from '@moxxy/sdk';\n\n/**\n * Normalize `target` against `cwd`. Returns an absolute path. **Does not\n * sandbox** — absolute targets and `../` traversal are allowed by design,\n * because the agent often needs to touch paths outside the cwd (`~/.config`,\n * `/etc/...`). Safety against unintended access lives at the permission\n * layer (`PermissionEngine` + the resolver), which prompts the user before\n * any tool runs. Tools that genuinely need to confine to cwd should use\n * `resolveWithinCwd` instead.\n *\n * Renamed from `resolveSafe` to make the contract honest — the old name\n * implied a sandbox it never performed.\n */\nexport function resolvePath(cwd: string, target: string): string {\n if (path.isAbsolute(target)) return path.normalize(target);\n return path.resolve(cwd, target);\n}\n\n/**\n * Like `resolvePath` but throws if the result escapes `cwd`. Use for tools\n * that should be strictly confined (rare).\n */\nexport function resolveWithinCwd(cwd: string, target: string): string {\n const resolved = resolvePath(cwd, target);\n const cwdAbs = path.resolve(cwd);\n const rel = path.relative(cwdAbs, resolved);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new MoxxyError({\n code: 'TOOL_ERROR',\n message: `Path escapes cwd: ${target} (resolved to ${resolved}, outside ${cwdAbs})`,\n });\n }\n return resolved;\n}\n\n/**\n * @deprecated Use `resolvePath` (no behavior change — only honest name).\n * Kept as a thin alias so external callers still compile.\n */\nexport const resolveSafe = resolvePath;\n\n/**\n * Directory names skipped during recursive traversal by the Glob and Grep\n * tools — build outputs and VCS metadata that are never useful search targets.\n * Kept in one place so the two walkers stay in sync.\n */\nexport const IGNORED_DIR_NAMES = new Set(['node_modules', '.git', 'dist', '.turbo']);\n\nexport function clampString(s: string, max: number): string {\n if (s.length <= max) return s;\n return s.slice(0, max) + `\\n... [truncated ${s.length - max} chars]`;\n}\n\n/**\n * Convert a glob pattern (`**`, `*`, `?`) to an anchored RegExp. Shared by\n * the Glob and Grep tools; not exposed externally.\n */\nexport function globToRegExp(pattern: string): RegExp {\n const escaped = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\?/g, '[^/]')\n .replace(/\\*\\*\\//g, '(?:.*/)?')\n .replace(/\\*\\*/g, '.*')\n .replace(/\\*/g, '[^/]*');\n return new RegExp('^' + escaped + '$');\n}\n","import { promises as fs } from 'node:fs';\nimport type { BrokeredFs } from '@moxxy/sdk';\nimport { clampString, resolvePath } from './util.js';\n\n/**\n * Pure handler module for the Read tool. Lives in its own file so the\n * worker_threads isolator (`@moxxy/isolator-worker`) can re-import it\n * on the worker side via the `handlerModule` reference declared in\n * `read.ts`.\n *\n * Closures can't cross thread boundaries; module exports can.\n */\nexport interface ReadInput {\n readonly file_path: string;\n readonly offset?: number;\n readonly limit?: number;\n}\n\nexport interface ReadCtxLike {\n readonly cwd: string;\n /** Capability-mediated fs. Present when invoked under an isolator that\n * brokers (`@moxxy/isolator-worker`); absent under `none` / `inproc`. */\n readonly fs?: BrokeredFs;\n}\n\nexport async function readHandler(input: ReadInput, ctx: ReadCtxLike): Promise<string> {\n const { file_path, offset = 0, limit = 2000 } = input;\n const resolved = resolvePath(ctx.cwd, file_path);\n // Use the brokered fs when the isolator provides one. The broker\n // re-validates the path against the tool's declared `caps.fs.read`\n // on the parent side, so reads outside the cap are denied at the\n // boundary regardless of what's in the input. Without a broker\n // (inproc / none), fall back to direct `node:fs` — input-level\n // cap-check already screened the file_path.\n const text = ctx.fs\n ? await ctx.fs.readFile(resolved, { encoding: 'utf8' })\n : (await fs.readFile(resolved)).toString('utf8');\n const lines = text.split('\\n');\n const sliced = lines.slice(offset, offset + limit);\n const numbered = sliced\n .map((line, i) => `${String(offset + i + 1).padStart(6, ' ')}\\t${line}`)\n .join('\\n');\n return clampString(numbered, 200_000);\n}\n"]}
|
|
@@ -126,6 +126,82 @@ export default definePlugin({
|
|
|
126
126
|
});
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
+
### API keys / secrets in a plugin
|
|
130
|
+
|
|
131
|
+
If the plugin needs an API key (e.g. to call a third-party API), **read it at call
|
|
132
|
+
time from the vault via `ctx.getSecret(name)`** — never `process.env`, and never ask
|
|
133
|
+
the user to paste the key into the chat. Tell the user to store it once with
|
|
134
|
+
`/vault set NAME <value>` (or the desktop Secrets UI); the plaintext only ever reaches
|
|
135
|
+
your handler, not the model's context.
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
import { definePlugin, defineTool, z } from '@moxxy/sdk';
|
|
139
|
+
|
|
140
|
+
export default definePlugin({
|
|
141
|
+
name: 'elevenlabs',
|
|
142
|
+
version: '0.0.0',
|
|
143
|
+
tools: [
|
|
144
|
+
defineTool({
|
|
145
|
+
name: 'elevenlabs_tts',
|
|
146
|
+
description: 'Synthesize speech from text via ElevenLabs.',
|
|
147
|
+
inputSchema: z.object({ text: z.string() }),
|
|
148
|
+
permission: { action: 'prompt' },
|
|
149
|
+
handler: async ({ text }, ctx) => {
|
|
150
|
+
const key = await ctx.getSecret?.('ELEVENLABS_API_KEY');
|
|
151
|
+
if (!key) throw new Error('Set the key first: /vault set ELEVENLABS_API_KEY <value>');
|
|
152
|
+
const res = await fetch('https://api.elevenlabs.io/v1/text-to-speech/...', {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: { 'xi-api-key': key, 'content-type': 'application/json' },
|
|
155
|
+
body: JSON.stringify({ text }),
|
|
156
|
+
});
|
|
157
|
+
// ... return result
|
|
158
|
+
},
|
|
159
|
+
}),
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Text-to-speech (read-aloud) plugins
|
|
165
|
+
|
|
166
|
+
To give the app a new voice (e.g. ElevenLabs) for the "Read aloud" button,
|
|
167
|
+
register a **synthesizer** — `synthesizers: [SynthesizerDef]` — not a tool. The
|
|
168
|
+
`create({ getSecret })` factory reads the API key from the vault the same way;
|
|
169
|
+
return `{ audio: Uint8Array, mimeType }`. A newly registered synthesizer
|
|
170
|
+
**auto-activates**, so read-aloud uses it immediately. The user switches back
|
|
171
|
+
with `set_voice system`, or between voices with `set_voice <name>`.
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
import { definePlugin } from '@moxxy/sdk';
|
|
175
|
+
|
|
176
|
+
export default definePlugin({
|
|
177
|
+
name: 'elevenlabs-voice',
|
|
178
|
+
version: '0.0.0',
|
|
179
|
+
synthesizers: [
|
|
180
|
+
{
|
|
181
|
+
name: 'elevenlabs',
|
|
182
|
+
displayName: 'ElevenLabs',
|
|
183
|
+
create: ({ getSecret }) => ({
|
|
184
|
+
name: 'elevenlabs',
|
|
185
|
+
async synthesize(text) {
|
|
186
|
+
const key = await getSecret?.('ELEVENLABS_API_KEY');
|
|
187
|
+
if (!key) throw new Error('Set the key: /vault set ELEVENLABS_API_KEY <value>');
|
|
188
|
+
const res = await fetch(
|
|
189
|
+
'https://api.elevenlabs.io/v1/text-to-speech/<voiceId>',
|
|
190
|
+
{
|
|
191
|
+
method: 'POST',
|
|
192
|
+
headers: { 'xi-api-key': key, 'content-type': 'application/json' },
|
|
193
|
+
body: JSON.stringify({ text, model_id: 'eleven_multilingual_v2' }),
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
if (!res.ok) throw new Error(`ElevenLabs ${res.status}`);
|
|
197
|
+
return { audio: new Uint8Array(await res.arrayBuffer()), mimeType: 'audio/mpeg' };
|
|
198
|
+
},
|
|
199
|
+
}),
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
129
205
|
## Don't
|
|
130
206
|
|
|
131
207
|
- **Don't skip the transaction.** Always `self_update_begin` before editing so
|
|
@@ -31,7 +31,7 @@ Naming: use slug-style names like `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `slack_
|
|
|
31
31
|
## Using a stored secret
|
|
32
32
|
|
|
33
33
|
- In `moxxy.config.ts` / `moxxy.config.yaml`: write `${vault:OPENAI_API_KEY}` anywhere a string is expected. The CLI resolves it on session start.
|
|
34
|
-
-
|
|
34
|
+
- **From plugin/tool code that needs the value at call time:** read it with `const key = await ctx.getSecret('OPENAI_API_KEY')` (the `ToolContext` passed to every tool handler). The plaintext goes straight to your handler — it never enters the model's context or `process.env`. This is the way an authored plugin should consume an API key; do **not** read `process.env`, and do not fetch-and-inline the plaintext anywhere the model can see it.
|
|
35
35
|
|
|
36
36
|
## Where things live
|
|
37
37
|
|