@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.
@@ -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 = resolveSafe(ctx.cwd, file_path);
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);
@@ -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;AAuBO,IAAM,WAAA,GAAc,WAAA;AAEpB,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;;;ACrBA,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\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, resolveSafe } 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 = resolveSafe(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"]}
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
- - For a tool/integration that needs the value at call time: pass the `${vault:NAME}` reference where supported, or let the integration resolve it do not fetch and inline the plaintext.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moxxy/cli",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "moxxy command-line binary. Subcommand dispatcher consuming the moxxy SDK.",
5
5
  "keywords": [
6
6
  "moxxy",