rewritable 0.6.0 → 0.7.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 +3 -3
- package/bin/rwa.mjs +9 -8
- package/package.json +1 -1
- package/seeds/rewritable.html +29 -4
- package/src/agent-loop.mjs +5 -3
- package/src/backend.mjs +24 -6
- package/src/commands.mjs +1 -1
- package/src/create.mjs +2 -1
package/README.md
CHANGED
|
@@ -125,10 +125,10 @@ The agent loop retries up to 3 times when the model emits plain text instead of
|
|
|
125
125
|
|
|
126
126
|
| Flag | Effect |
|
|
127
127
|
|---|---|
|
|
128
|
-
| `--backend <name>` | `openrouter` (default), `ollama`, `lmstudio`. Falls back to `$RWA_BACKEND`. `bridge` is browser-only by design. |
|
|
128
|
+
| `--backend <name>` | `openrouter` (default), `ollama`, `lmstudio`, `atomic`. Falls back to `$RWA_BACKEND`. `bridge` is browser-only by design. |
|
|
129
129
|
| `--model <id>` | model id passed to the backend. Falls back to `$RWA_MODEL`, then `google/gemini-3.5-flash`. |
|
|
130
|
-
| `--base-url <url>` | OpenAI-compatible base URL override. Defaults: `https://openrouter.ai/api/v1`, `http://localhost:11434/v1` (or `$RWA_OLLAMA_URL`), `http://localhost:1234/v1` (or `$RWA_LMSTUDIO_URL`). |
|
|
131
|
-
| `--api-key <key>` | openrouter only; falls back to `$RWA_OPENROUTER_KEY`. ollama / lmstudio run locally without auth. |
|
|
130
|
+
| `--base-url <url>` | OpenAI-compatible base URL override. Defaults: `https://openrouter.ai/api/v1`, `http://localhost:11434/v1` (or `$RWA_OLLAMA_URL`), `http://localhost:1234/v1` (or `$RWA_LMSTUDIO_URL`), `http://127.0.0.1:1337/v1` (or `$RWA_ATOMIC_URL`). |
|
|
131
|
+
| `--api-key <key>` | openrouter only; falls back to `$RWA_OPENROUTER_KEY`. ollama / lmstudio / atomic run locally without auth. |
|
|
132
132
|
|
|
133
133
|
#### Other edit flags
|
|
134
134
|
|
package/bin/rwa.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { newCmd, importCmd, version, KNOWN_KINDS, openWithPrefill, SEED_CANDIDATES } from '../src/commands.mjs';
|
|
3
|
-
import { resolveApiKey } from '../src/backend.mjs';
|
|
3
|
+
import { resolveApiKey, backendMaxTokens } from '../src/backend.mjs';
|
|
4
4
|
import { parseCreateArgs, createCmd } from '../src/create.mjs';
|
|
5
5
|
import { relative } from 'node:path';
|
|
6
6
|
|
|
@@ -101,7 +101,7 @@ Flags:
|
|
|
101
101
|
--open, -o open the resulting file in the default app. First-paint
|
|
102
102
|
sessionStorage is pre-populated from env / ./.env:
|
|
103
103
|
OPENROUTER_API_KEY → ?key=… (lifted into rwa_apikey)
|
|
104
|
-
RWA_BACKEND → ?backend= (openrouter|ollama|lmstudio|bridge)
|
|
104
|
+
RWA_BACKEND → ?backend= (openrouter|ollama|lmstudio|atomic|bridge)
|
|
105
105
|
RWA_MODEL → ?model=… (model name string)
|
|
106
106
|
The bootstrap lifts each into sessionStorage and scrubs the
|
|
107
107
|
URL bar on first paint, so the values don't sit in history.
|
|
@@ -137,8 +137,8 @@ Flags:
|
|
|
137
137
|
the raw body; on failure, the \`{code, subcode, details}\`
|
|
138
138
|
object goes to stderr.
|
|
139
139
|
--backend <n> (edit instruction path / skin --l1) backend name. One of:
|
|
140
|
-
openrouter (default), ollama, lmstudio. Falls back
|
|
141
|
-
\$RWA_BACKEND if unset.
|
|
140
|
+
openrouter (default), ollama, lmstudio, atomic. Falls back
|
|
141
|
+
to \$RWA_BACKEND if unset.
|
|
142
142
|
--model <id> (edit instruction path / skin --l1) model id passed to the
|
|
143
143
|
backend. Falls back to \$RWA_MODEL, then a
|
|
144
144
|
sensible default for the backend.
|
|
@@ -248,6 +248,7 @@ function envBaseUrl(name) {
|
|
|
248
248
|
case 'openrouter': return 'https://openrouter.ai/api/v1';
|
|
249
249
|
case 'ollama': return process.env.RWA_OLLAMA_URL || 'http://localhost:11434/v1';
|
|
250
250
|
case 'lmstudio': return process.env.RWA_LMSTUDIO_URL || 'http://localhost:1234/v1';
|
|
251
|
+
case 'atomic': return process.env.RWA_ATOMIC_URL || 'http://127.0.0.1:1337/v1';
|
|
251
252
|
default: return undefined;
|
|
252
253
|
}
|
|
253
254
|
}
|
|
@@ -377,7 +378,7 @@ function detectProductKind(fileText) {
|
|
|
377
378
|
|
|
378
379
|
// Reject unknown backends fast. `bridge` is browser-only by design
|
|
379
380
|
// (single-shot via web_cli_bridge); the CLI has no equivalent.
|
|
380
|
-
if (!['openrouter', 'ollama', 'lmstudio'].includes(backendName)) {
|
|
381
|
+
if (!['openrouter', 'ollama', 'lmstudio', 'atomic'].includes(backendName)) {
|
|
381
382
|
emitEdit({ code: 'usage_error', subcode: 'unknown_backend', details: { backend: backendName } }, jsonMode);
|
|
382
383
|
process.exitCode = 1;
|
|
383
384
|
return;
|
|
@@ -457,7 +458,7 @@ function detectProductKind(fileText) {
|
|
|
457
458
|
currentDoc: promptDoc,
|
|
458
459
|
instruction,
|
|
459
460
|
frozenZoneNames,
|
|
460
|
-
backend: { baseUrl, model: modelId, apiKey },
|
|
461
|
+
backend: { baseUrl, model: modelId, apiKey, maxTokens: backendMaxTokens(backendName) },
|
|
461
462
|
onRetry: r => {
|
|
462
463
|
if (jsonMode) {
|
|
463
464
|
process.stderr.write(JSON.stringify({ phase: 'retry', attempt: r.attempt, reason: r.reason }) + '\n');
|
|
@@ -845,7 +846,7 @@ function detectProductKind(fileText) {
|
|
|
845
846
|
const baseUrl = baseUrlFlag.value || envBaseUrl(backendName);
|
|
846
847
|
const apiKey = resolveApiKey(backendName, apiKeyFlag.value);
|
|
847
848
|
|
|
848
|
-
if (!['openrouter', 'ollama', 'lmstudio'].includes(backendName)) {
|
|
849
|
+
if (!['openrouter', 'ollama', 'lmstudio', 'atomic'].includes(backendName)) {
|
|
849
850
|
emitSkin({ code: 'usage_error', subcode: 'unknown_backend', details: { backend: backendName } });
|
|
850
851
|
process.exitCode = 1; return;
|
|
851
852
|
}
|
|
@@ -882,7 +883,7 @@ function detectProductKind(fileText) {
|
|
|
882
883
|
result = await skinCmdL1(filePath, action, {
|
|
883
884
|
systemPrompt,
|
|
884
885
|
toolSchemas: TOOL_SCHEMAS,
|
|
885
|
-
backend: { baseUrl, model: modelId, apiKey },
|
|
886
|
+
backend: { baseUrl, model: modelId, apiKey, maxTokens: backendMaxTokens(backendName) },
|
|
886
887
|
onRetry: r => {
|
|
887
888
|
if (jsonMode) process.stderr.write(JSON.stringify({ phase: 'retry', attempt: r.attempt, reason: r.reason }) + '\n');
|
|
888
889
|
else process.stderr.write(`rwa skin: attempt ${r.attempt}/3 retrying — ${r.reason}\n`);
|
package/package.json
CHANGED
package/seeds/rewritable.html
CHANGED
|
@@ -380,6 +380,13 @@ const RWA = {
|
|
|
380
380
|
// not block these requests even when the container is served over HTTPS.
|
|
381
381
|
DEFAULT_OLLAMA_URL:'http://localhost:11434/v1',
|
|
382
382
|
DEFAULT_LMSTUDIO_URL:'http://localhost:1234/v1',
|
|
383
|
+
// atomic.chat: a local OpenAI-compatible inference server (MLX-backed on
|
|
384
|
+
// Apple Silicon) with the same /v1/chat/completions + /v1/models shape and
|
|
385
|
+
// real multi-turn tool_calls. CORS note: it allows http(s) page origins out
|
|
386
|
+
// of the box but NOT the file:// null origin — serve the container from a
|
|
387
|
+
// local origin (or a hosted projection) to use it.
|
|
388
|
+
DEFAULT_ATOMIC_URL:'http://127.0.0.1:1337/v1',
|
|
389
|
+
K_BASE_URL_ATOMIC:'rwa_base_url_atomic',
|
|
383
390
|
// Optional alternative agent backend: a localhost CLI bridge
|
|
384
391
|
// (https://github.com/martintreiber/web_cli_bridge style) that runs
|
|
385
392
|
// arbitrary shell commands. When the user picks "bridge" in settings, ⌘K
|
|
@@ -411,7 +418,7 @@ try {
|
|
|
411
418
|
qs.delete('key');
|
|
412
419
|
}
|
|
413
420
|
const b = qs.get('backend');
|
|
414
|
-
if (b && ['openrouter','ollama','lmstudio','bridge','bridge-session'].includes(b)) {
|
|
421
|
+
if (b && ['openrouter','ollama','lmstudio','atomic','bridge','bridge-session'].includes(b)) {
|
|
415
422
|
sessionStorage.setItem(RWA.K_BACKEND, b);
|
|
416
423
|
qs.delete('backend');
|
|
417
424
|
}
|
|
@@ -1117,7 +1124,7 @@ function buildUI() {
|
|
|
1117
1124
|
<button class="rwa-st-btn pri" id="rwa-st-commit">⌘S</button>
|
|
1118
1125
|
</div>
|
|
1119
1126
|
<div id="rwa-set-panel">
|
|
1120
|
-
<div class="rwa-set-row"><label>Backend</label><select id="rwa-backend"><option value="openrouter">OpenRouter (API key)</option><option value="ollama">Ollama (localhost)</option><option value="lmstudio">LM Studio (localhost)</option><option value="bridge">Bridge (claude -p, localhost)</option><option value="bridge-session">Bridge session (claude, persistent)</option></select></div>
|
|
1127
|
+
<div class="rwa-set-row"><label>Backend</label><select id="rwa-backend"><option value="openrouter">OpenRouter (API key)</option><option value="ollama">Ollama (localhost)</option><option value="lmstudio">LM Studio (localhost)</option><option value="atomic">atomic.chat (localhost)</option><option value="bridge">Bridge (claude -p, localhost)</option><option value="bridge-session">Bridge session (claude, persistent)</option></select></div>
|
|
1121
1128
|
<div class="rwa-set-row" id="rwa-set-row-key"><label>OpenRouter Key</label><input type="password" id="rwa-key" placeholder="sk-or-..." autocomplete="off"></div>
|
|
1122
1129
|
<div class="rwa-set-row" id="rwa-set-row-bridge-token" style="display:none;"><label>Bridge Token</label><input type="password" id="rwa-bridge-token" placeholder="WEB_CLI_BRIDGE_TOKEN" autocomplete="off"></div>
|
|
1123
1130
|
<div class="rwa-set-row" id="rwa-set-row-bridge-cwd" style="display:none;"><label>Session Dir</label><input type="text" id="rwa-bridge-cwd" placeholder="/path/on/bridge/host" autocomplete="off" spellcheck="false"></div>
|
|
@@ -1198,6 +1205,12 @@ function buildUI() {
|
|
|
1198
1205
|
defaultUrl: RWA.DEFAULT_LMSTUDIO_URL,
|
|
1199
1206
|
storageKey: RWA.K_BASE_URL_LMSTUDIO,
|
|
1200
1207
|
},
|
|
1208
|
+
atomic: {
|
|
1209
|
+
showKey: false, showBaseUrl: true, showModel: true, showHint: true,
|
|
1210
|
+
hintHTML: 'atomic.chat serves an OpenAI-compatible API on <code>127.0.0.1:1337</code>, no key needed — use <code>Test</code> to list its models. CORS allows http(s) page origins out of the box but <strong>not <code>file://</code> pages</strong> (the null origin): open this container from a local web server or a hosted projection to use it.',
|
|
1211
|
+
defaultUrl: RWA.DEFAULT_ATOMIC_URL,
|
|
1212
|
+
storageKey: RWA.K_BASE_URL_ATOMIC,
|
|
1213
|
+
},
|
|
1201
1214
|
bridge: {
|
|
1202
1215
|
showKey: false, showBaseUrl: false, showModel: false, showHint: false,
|
|
1203
1216
|
hintHTML: '',
|
|
@@ -4321,6 +4334,18 @@ function resolveBackendConfig() {
|
|
|
4321
4334
|
apiKey: null, extraHeaders: {}, requiresKey: false,
|
|
4322
4335
|
};
|
|
4323
4336
|
}
|
|
4337
|
+
if (backend === 'atomic') {
|
|
4338
|
+
return {
|
|
4339
|
+
backend, kind:'openai_compat',
|
|
4340
|
+
baseUrl: (sessionStorage.getItem(RWA.K_BASE_URL_ATOMIC) || '').trim() || RWA.DEFAULT_ATOMIC_URL,
|
|
4341
|
+
apiKey: null, extraHeaders: {}, requiresKey: false,
|
|
4342
|
+
// atomic.chat REJECTS (400) requests whose prompt + max generation
|
|
4343
|
+
// exceed its MAX_KV_SIZE (16384 default) rather than clamping like
|
|
4344
|
+
// ollama/lmstudio — half the window for generation, half for the
|
|
4345
|
+
// prompt + document. Callers read cfg.maxTokens || 32000.
|
|
4346
|
+
maxTokens: 8192,
|
|
4347
|
+
};
|
|
4348
|
+
}
|
|
4324
4349
|
if (backend === 'bridge') {
|
|
4325
4350
|
return { backend, kind:'bridge' };
|
|
4326
4351
|
}
|
|
@@ -4379,7 +4404,7 @@ async function callAgentSingleShot(prompt) {
|
|
|
4379
4404
|
const model = sessionStorage.getItem(RWA.K_MODEL) || RWA.MODEL;
|
|
4380
4405
|
const data = await openAiCompatChat(cfg, {
|
|
4381
4406
|
model,
|
|
4382
|
-
max_tokens: 32000,
|
|
4407
|
+
max_tokens: cfg.maxTokens || 32000,
|
|
4383
4408
|
messages: [{ role:'user', content: prompt }],
|
|
4384
4409
|
});
|
|
4385
4410
|
const msg = data.choices?.[0]?.message;
|
|
@@ -6578,7 +6603,7 @@ async function modify(instr, lensMeta = null, opts = null) {
|
|
|
6578
6603
|
try {
|
|
6579
6604
|
data = await openAiCompatChat(cfg, {
|
|
6580
6605
|
model,
|
|
6581
|
-
max_tokens: 32000,
|
|
6606
|
+
max_tokens: cfg.maxTokens || 32000,
|
|
6582
6607
|
messages,
|
|
6583
6608
|
tools: TOOL_SCHEMAS,
|
|
6584
6609
|
tool_choice: 'auto',
|
package/src/agent-loop.mjs
CHANGED
|
@@ -128,12 +128,14 @@ export async function runAgentLoop({
|
|
|
128
128
|
throw new AgentError('no_envelope_after_retries', { retries: RETRY_BUDGET });
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
async function callBackend({ baseUrl, model, apiKey }, body) {
|
|
131
|
+
async function callBackend({ baseUrl, model, apiKey, maxTokens }, body) {
|
|
132
132
|
const headers = { 'Content-Type': 'application/json' };
|
|
133
133
|
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
|
134
134
|
const url = baseUrl.replace(/\/+$/, '') + '/chat/completions';
|
|
135
135
|
// Seed parity (seeds/rewritable.html openAiCompatChat caller in modify()):
|
|
136
|
-
// every request carries max_tokens
|
|
136
|
+
// every request carries the backend's max_tokens (32000 historically; 8192
|
|
137
|
+
// for atomic, whose server REJECTS prompt+generation past MAX_KV_SIZE rather
|
|
138
|
+
// than clamping — see backendMaxTokens) and tool_choice: 'auto'. The
|
|
137
139
|
// tool_choice default forces the model to call one of the provided tools
|
|
138
140
|
// rather than emitting plain text (which would trip our no_tool_call retry).
|
|
139
141
|
const res = await fetch(url, {
|
|
@@ -141,7 +143,7 @@ async function callBackend({ baseUrl, model, apiKey }, body) {
|
|
|
141
143
|
headers,
|
|
142
144
|
body: JSON.stringify({
|
|
143
145
|
model,
|
|
144
|
-
max_tokens: 32000,
|
|
146
|
+
max_tokens: maxTokens || 32000,
|
|
145
147
|
tool_choice: 'auto',
|
|
146
148
|
...body,
|
|
147
149
|
}),
|
package/src/backend.mjs
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
// precedence is unit-testable (the bin entrypoint runs on import and can't be
|
|
3
3
|
// imported cleanly).
|
|
4
4
|
//
|
|
5
|
-
// Only the openrouter backend needs a key — ollama and
|
|
6
|
-
// without auth. The key resolves in order: an explicit --api-key flag, then the
|
|
5
|
+
// Only the openrouter backend needs a key — ollama, lmstudio, and atomic run
|
|
6
|
+
// locally without auth. The key resolves in order: an explicit --api-key flag, then the
|
|
7
7
|
// project-specific RWA_OPENROUTER_KEY (env conventions match the docker-compose
|
|
8
8
|
// deploy in service/), then the CONVENTIONAL OPENROUTER_API_KEY that agents and
|
|
9
9
|
// users normally have exported. Empty strings count as absent.
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Resolve the API key for a backend.
|
|
13
|
-
* @param {string} backendName — 'openrouter' | 'ollama' | 'lmstudio'
|
|
13
|
+
* @param {string} backendName — 'openrouter' | 'ollama' | 'lmstudio' | 'atomic'
|
|
14
14
|
* @param {string|undefined} flagValue — the --api-key flag value, if any
|
|
15
15
|
* @param {Record<string,string|undefined>} [env] — environment (injectable for tests)
|
|
16
16
|
* @returns {string|undefined} the key, or undefined when none applies
|
|
@@ -26,10 +26,10 @@ export function resolveApiKey(backendName, flagValue, env = process.env) {
|
|
|
26
26
|
/**
|
|
27
27
|
* Default OpenAI-compatible base URL for a backend — mirrors the inline
|
|
28
28
|
* `envBaseUrl` in bin/rwa.mjs (and seeds/rewritable.html resolveBackendConfig).
|
|
29
|
-
* ollama and
|
|
30
|
-
* openrouter is fixed (the URL has never drifted in the seed). Shared by `rwa edit`
|
|
29
|
+
* ollama, lmstudio, and atomic honor RWA_*_URL overrides (remote host / non-standard
|
|
30
|
+
* port); openrouter is fixed (the URL has never drifted in the seed). Shared by `rwa edit`
|
|
31
31
|
* and `rwa create` so the default never diverges between the two.
|
|
32
|
-
* @param {string} name — 'openrouter' | 'ollama' | 'lmstudio'
|
|
32
|
+
* @param {string} name — 'openrouter' | 'ollama' | 'lmstudio' | 'atomic'
|
|
33
33
|
* @param {Record<string,string|undefined>} [env] — environment (injectable for tests)
|
|
34
34
|
* @returns {string|undefined}
|
|
35
35
|
*/
|
|
@@ -38,6 +38,24 @@ export function envBaseUrl(name, env = process.env) {
|
|
|
38
38
|
case 'openrouter': return 'https://openrouter.ai/api/v1';
|
|
39
39
|
case 'ollama': return env.RWA_OLLAMA_URL || 'http://localhost:11434/v1';
|
|
40
40
|
case 'lmstudio': return env.RWA_LMSTUDIO_URL || 'http://localhost:1234/v1';
|
|
41
|
+
case 'atomic': return env.RWA_ATOMIC_URL || 'http://127.0.0.1:1337/v1';
|
|
41
42
|
default: return undefined;
|
|
42
43
|
}
|
|
43
44
|
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Per-backend max_tokens for the agent loop. The historical 32000 stands for
|
|
48
|
+
* hosted/clamping backends, but atomic.chat REJECTS (400) any request whose
|
|
49
|
+
* prompt + max generation exceeds its MAX_KV_SIZE (16384 by default) rather
|
|
50
|
+
* than clamping — so it gets 8192, leaving the other half of the window for
|
|
51
|
+
* the system prompt + document. RWA_MAX_TOKENS overrides for unusual servers.
|
|
52
|
+
* Mirrors the seed's resolveBackendConfig() maxTokens.
|
|
53
|
+
* @param {string} name — backend name
|
|
54
|
+
* @param {Record<string,string|undefined>} [env]
|
|
55
|
+
* @returns {number}
|
|
56
|
+
*/
|
|
57
|
+
export function backendMaxTokens(name, env = process.env) {
|
|
58
|
+
const override = Number(env.RWA_MAX_TOKENS);
|
|
59
|
+
if (Number.isFinite(override) && override > 0) return override;
|
|
60
|
+
return name === 'atomic' ? 8192 : 32000;
|
|
61
|
+
}
|
package/src/commands.mjs
CHANGED
|
@@ -85,7 +85,7 @@ async function readEnvKey(name) {
|
|
|
85
85
|
// than throwing — pre-fill is best-effort; an unknown value just means the
|
|
86
86
|
// user sees the default backend (openrouter) on first paint.
|
|
87
87
|
function validBackend(v) {
|
|
88
|
-
return ['openrouter', 'ollama', 'lmstudio', 'bridge'].includes(v) ? v : null;
|
|
88
|
+
return ['openrouter', 'ollama', 'lmstudio', 'atomic', 'bridge'].includes(v) ? v : null;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// Collect URL-param pre-fills from env / ./.env. Returns an object whose keys
|
package/src/create.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import { runAgentLoop } from './agent-loop.mjs';
|
|
|
20
20
|
import { applyPlan, CliError } from './edit.mjs';
|
|
21
21
|
import { assertSelfContained } from './self-contained.mjs';
|
|
22
22
|
import { findFrozenZones } from './apply-edits.mjs';
|
|
23
|
-
import { resolveApiKey, envBaseUrl } from './backend.mjs';
|
|
23
|
+
import { resolveApiKey, envBaseUrl, backendMaxTokens } from './backend.mjs';
|
|
24
24
|
import { atomicWrite } from './atomic-write.mjs';
|
|
25
25
|
|
|
26
26
|
// Hard cap on --data baked into the snapshot. The dataset lands inside INLINE_DOC
|
|
@@ -219,6 +219,7 @@ export async function createCmd(parsed, { seedCandidates, cwd = process.cwd(), s
|
|
|
219
219
|
baseUrl: parsed.backend.baseUrl || envBaseUrl(backendName),
|
|
220
220
|
model: parsed.backend.model || process.env.RWA_MODEL || 'google/gemini-3.5-flash',
|
|
221
221
|
apiKey: resolveApiKey(backendName, parsed.backend.apiKey),
|
|
222
|
+
maxTokens: backendMaxTokens(backendName),
|
|
222
223
|
};
|
|
223
224
|
|
|
224
225
|
// Per-kind system prompt + the create-only self-containment directive; the brief
|