@polylogicai/polycode 1.1.5 → 1.1.6

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/bin/polycode.mjs CHANGED
@@ -26,7 +26,10 @@ import { fileURLToPath } from 'node:url';
26
26
  const __filename = fileURLToPath(import.meta.url);
27
27
  const __dirname = dirname(__filename);
28
28
  const PACKAGE_JSON = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
29
- import * as readline from 'node:readline/promises';
29
+ // node:readline is intentionally NOT imported. The REPL owns stdin via the
30
+ // paste-aware reader in lib/paste-aware-prompt.mjs; slash commands that need
31
+ // masked input call readMaskedInput in lib/key-store.mjs. Any readline
32
+ // import here would reintroduce the v1.1.5 double-echo bug.
30
33
  import { stdin, stdout, exit, argv, env, cwd as getCwd } from 'node:process';
31
34
  import 'dotenv/config';
32
35
 
@@ -260,7 +263,12 @@ async function runOneShot(message, opts) {
260
263
  }
261
264
 
262
265
  async function runRepl(opts) {
263
- const rl = readline.createInterface({ input: stdin, output: stdout });
266
+ // Single stdin owner. Do NOT create a readline interface here. The
267
+ // paste-aware reader (lib/paste-aware-prompt.mjs) owns stdin for the
268
+ // entire REPL lifetime, and slash commands that need line input fall
269
+ // through to the raw-mode readMaskedInput helper in lib/key-store.mjs.
270
+ // Running readline alongside a raw-mode consumer causes every keystroke
271
+ // to echo twice, which is the v1.1.5 double-echo bug this release fixes.
264
272
  stdout.write(BANNER + '\n');
265
273
  if (opts.hostedMode) {
266
274
  stdout.write(`${C.dim}hosted tier: 60 turns/hour via polylogicai.com. Set GROQ_API_KEY for unlimited use.${C.reset}\n`);
@@ -273,10 +281,6 @@ async function runRepl(opts) {
273
281
  }
274
282
  stdout.write(`${C.dim}type /help for commands. ctrl+c or /exit to leave.${C.reset}\n\n`);
275
283
 
276
- // Enable bracketed paste so multi-line pastes collapse to [Pasted #N] in
277
- // the display. The readline instance above is used only as an input owner
278
- // for slash commands via rl.question; the main prompt uses the custom
279
- // paste-aware reader.
280
284
  enableBracketedPaste();
281
285
 
282
286
  try {
@@ -288,7 +292,6 @@ async function runRepl(opts) {
288
292
  if (err && err.message === 'cancelled') break;
289
293
  throw err;
290
294
  }
291
- const displayed = userInput.displayed;
292
295
  const content = userInput.content;
293
296
  if (!content.trim()) continue;
294
297
 
@@ -298,16 +301,13 @@ async function runRepl(opts) {
298
301
  state: opts.state,
299
302
  stdout,
300
303
  loop: opts.loop,
301
- rl,
304
+ rl: null,
302
305
  });
303
306
  if (result.exit) break;
304
307
  stdout.write('\n');
305
308
  continue;
306
309
  }
307
310
 
308
- // Telemetry-light logging of paste events so later /replay shows a
309
- // human-readable trail. The full paste content still goes to the
310
- // agent.
311
311
  if (userInput.pastes && userInput.pastes.length > 0) {
312
312
  for (const p of userInput.pastes) {
313
313
  opts.canon.append('paste', { ordinal: p.ordinal, lines: p.lines, bytes: p.bytes });
@@ -319,7 +319,6 @@ async function runRepl(opts) {
319
319
  }
320
320
  } finally {
321
321
  disableBracketedPaste();
322
- rl.close();
323
322
  }
324
323
  }
325
324
 
package/lib/key-store.mjs CHANGED
@@ -187,23 +187,14 @@ export async function readMaskedInput(prompt) {
187
187
  // Returns { ok: true, provider, path } on success, { ok: false, reason } on
188
188
  // failure. The caller renders the result to the user.
189
189
  //
190
- // If `rl` is passed (a readline.Interface from readline/promises), we use
191
- // rl.question for the prompt instead of attaching a raw stdin listener. That
192
- // path matters for the slash-command case inside the REPL where readline
193
- // already owns stdin; using rl.question avoids a race where readline eats
194
- // the key line before our masked reader can see it.
195
- export async function runInteractiveKeyFlow({ providerHint, stdout, rl } = {}) {
190
+ // readMaskedInput handles both the TTY case (raw-mode masked echo) and the
191
+ // non-TTY case (plain line read from piped stdin). The polycode REPL owns
192
+ // stdin via readPasteAwareLine in the main loop; each slash-command call
193
+ // acquires stdin via this helper, finishes its work, and cleans up so the
194
+ // main loop can re-acquire.
195
+ export async function runInteractiveKeyFlow({ providerHint, stdout } = {}) {
196
196
  const out = stdout || process.stdout;
197
- let rawKey;
198
- if (rl && typeof rl.question === 'function') {
199
- // Inside the REPL: read through the active readline instance.
200
- // Note: this path does not mask echo in the terminal. That is a
201
- // deliberate trade-off for reliable input under the REPL. For a
202
- // fully-masked prompt, use `polycode login` from the shell instead.
203
- rawKey = (await rl.question('Paste your API key (or press Enter to cancel): ')).trim();
204
- } else {
205
- rawKey = (await readMaskedInput('Paste your API key (or press Enter to cancel): ')).trim();
206
- }
197
+ const rawKey = (await readMaskedInput('Paste your API key (or press Enter to cancel): ')).trim();
207
198
  if (!rawKey) {
208
199
  return { ok: false, reason: 'cancelled' };
209
200
  }
@@ -20,7 +20,10 @@ ${C.bold}polycode commands${C.reset}
20
20
  /exit, /quit Leave polycode
21
21
  `;
22
22
 
23
- export async function dispatchSlash(line, { canon, state, stdout, loop, rl }) {
23
+ export async function dispatchSlash(line, { canon, state, stdout, loop }) {
24
+ // Single stdin owner principle: no readline instance is passed in. Slash
25
+ // commands that need masked input fall through to the raw-mode helper
26
+ // in lib/key-store.mjs.
24
27
  const [cmd, ...rest] = line.slice(1).split(/\s+/);
25
28
  const args = rest.join(' ').trim();
26
29
 
@@ -37,7 +40,7 @@ export async function dispatchSlash(line, { canon, state, stdout, loop, rl }) {
37
40
  return { continue: true };
38
41
  }
39
42
  stdout.write(`${C.dim}Your key will be saved locally to ~/.polycode/secrets.env (chmod 600) and never sent to the model.${C.reset}\n`);
40
- const result = await runInteractiveKeyFlow({ providerHint, stdout, rl });
43
+ const result = await runInteractiveKeyFlow({ providerHint, stdout });
41
44
  if (!result.ok) {
42
45
  if (result.reason === 'cancelled') {
43
46
  stdout.write(`${C.dim}cancelled${C.reset}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polylogicai/polycode",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "description": "An agentic coding CLI. Runs on your machine with your keys. Every turn is appended to a SHA-256 chained session log, so your history is auditable, replayable, and portable.",
5
5
  "type": "module",
6
6
  "main": "bin/polycode.mjs",