@polylogicai/polycode 1.1.1 → 1.1.3

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 CHANGED
@@ -4,21 +4,37 @@ An agentic coding CLI. Runs on your machine with your keys. Every turn is append
4
4
 
5
5
  ## Install
6
6
 
7
+ The fastest way to try polycode is with `npx`. It works on macOS, Linux, and Windows from any terminal:
8
+
9
+ ```bash
10
+ npx @polylogicai/polycode@latest
11
+ ```
12
+
13
+ For repeated use, install it globally:
14
+
7
15
  ```bash
8
16
  npm install -g @polylogicai/polycode
17
+ polycode
9
18
  ```
10
19
 
11
20
  ## Quick start
12
21
 
22
+ Set your Groq API key, then run polycode:
23
+
13
24
  ```bash
25
+ # macOS and Linux
14
26
  export GROQ_API_KEY=gsk_...
15
- polycode
27
+ npx @polylogicai/polycode@latest
28
+
29
+ # Windows PowerShell
30
+ $env:GROQ_API_KEY = "gsk_..."
31
+ npx @polylogicai/polycode@latest
16
32
  ```
17
33
 
18
34
  That opens an interactive session. For one-shot mode, pass your prompt as an argument:
19
35
 
20
36
  ```bash
21
- polycode "read README.md and summarize it in one sentence"
37
+ npx @polylogicai/polycode@latest "read README.md and summarize it in one sentence"
22
38
  ```
23
39
 
24
40
  A free Groq API key is available at `console.groq.com`.
@@ -92,7 +108,7 @@ Rules live in `~/.polycode/rules.yaml`. You can add your own.
92
108
  ## Requirements
93
109
 
94
110
  - Node.js 20 or newer
95
- - macOS or Linux
111
+ - macOS, Linux, or Windows
96
112
  - A Groq API key
97
113
 
98
114
  ## Documentation and support
package/bin/polycode.mjs CHANGED
@@ -19,6 +19,11 @@ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'no
19
19
  import { homedir } from 'node:os';
20
20
  import { join, dirname, resolve } from 'node:path';
21
21
  import { randomUUID, createHash } from 'node:crypto';
22
+ import { fileURLToPath } from 'node:url';
23
+
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = dirname(__filename);
26
+ const PACKAGE_JSON = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
22
27
  import * as readline from 'node:readline/promises';
23
28
  import { stdin, stdout, exit, argv, env, cwd as getCwd } from 'node:process';
24
29
  import 'dotenv/config';
@@ -137,7 +142,7 @@ function loadRules() {
137
142
  return {};
138
143
  }
139
144
 
140
- const VERSION = '1.1.0';
145
+ const VERSION = PACKAGE_JSON.version;
141
146
  const DOCS_URL = 'https://polylogicai.com/polycode';
142
147
 
143
148
  const BANNER = `${C.bold}${C.amber}polycode v${VERSION}${C.reset}
package/lib/agentic.mjs CHANGED
@@ -13,12 +13,62 @@ import Groq from 'groq-sdk';
13
13
  import { promises as fs } from 'node:fs';
14
14
  import { exec } from 'node:child_process';
15
15
  import { promisify } from 'node:util';
16
- import { resolve, relative, sep, dirname } from 'node:path';
16
+ import { resolve, relative, sep, dirname, join, basename } from 'node:path';
17
17
  import { compilePacket } from './compiler.mjs';
18
18
  import { mintCommitment } from './commitment.mjs';
19
19
  import { ensureActiveIntent } from './intent.mjs';
20
20
  import { scrubSecrets } from './witness/secret-scrubber.mjs';
21
21
 
22
+ // Cross-platform directory walker. Pure Node.js, no shell-out. Skips common
23
+ // noise directories (node_modules, .git, etc.) by default. Yields absolute
24
+ // file paths as an async iterator so callers can bail out early.
25
+ export const DEFAULT_SKIP_DIRS = new Set(['node_modules', '.git', '.svn', '.hg', 'dist', 'build', '.next', '.nuxt']);
26
+
27
+ export async function* walkFiles(root, skipDirs = DEFAULT_SKIP_DIRS) {
28
+ let entries;
29
+ try {
30
+ entries = await fs.readdir(root, { withFileTypes: true });
31
+ } catch {
32
+ return;
33
+ }
34
+ for (const entry of entries) {
35
+ if (skipDirs.has(entry.name) || entry.name.startsWith('.DS_Store')) continue;
36
+ const fullPath = join(root, entry.name);
37
+ if (entry.isDirectory()) {
38
+ yield* walkFiles(fullPath, skipDirs);
39
+ } else if (entry.isFile()) {
40
+ yield fullPath;
41
+ }
42
+ }
43
+ }
44
+
45
+ // Minimal glob-to-regex converter. Supports ** (any depth), * (any chars
46
+ // except /), ? (one char). Anchors the pattern with ^ and $ so callers get
47
+ // full-string matching.
48
+ export function globToRegex(pattern) {
49
+ const escaped = pattern
50
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
51
+ .replace(/\*\*\//g, '\u0001')
52
+ .replace(/\*\*/g, '\u0001')
53
+ .replace(/\*/g, '[^/]*')
54
+ .replace(/\?/g, '[^/]')
55
+ .replace(/\u0001/g, '(?:.*/)?');
56
+ return new RegExp('^' + escaped + '$');
57
+ }
58
+
59
+ // Normalize a filesystem path to forward slashes so glob matching and output
60
+ // formatting behave the same on every platform.
61
+ export function toPosix(p) {
62
+ return sep === '/' ? p : p.split(sep).join('/');
63
+ }
64
+
65
+ // Detect whether a file buffer is likely binary by looking for a NUL byte in
66
+ // the first kilobyte. Used by the grep tool to skip binary files.
67
+ export function looksBinary(content) {
68
+ const head = content.slice(0, 1024);
69
+ return head.indexOf('\u0000') !== -1;
70
+ }
71
+
22
72
  const execAsync = promisify(exec);
23
73
 
24
74
  const DEFAULT_MODEL = 'moonshotai/kimi-k2-instruct';
@@ -95,7 +145,7 @@ const TOOL_SCHEMAS = [
95
145
  type: 'function',
96
146
  function: {
97
147
  name: 'bash',
98
- description: 'Run a shell command in the working directory. 30 second timeout. Output truncated at 3200 bytes.',
148
+ description: 'Run a shell command in the working directory. Uses the system default shell, so assume POSIX tools on macOS and Linux and cmd.exe on Windows. Prefer read_file, write_file, edit_file, glob, and grep for file operations; reserve this tool for real commands like running tests or git. 30 second timeout. Output truncated at 3200 bytes.',
99
149
  parameters: {
100
150
  type: 'object',
101
151
  properties: { command: { type: 'string' } },
@@ -239,23 +289,57 @@ async function runTool(name, args, cwd) {
239
289
  }
240
290
  }
241
291
  case 'glob': {
242
- const pattern = String(args.pattern ?? '').replace(/'/g, "'\\''");
292
+ const pattern = String(args.pattern ?? '').trim();
293
+ if (!pattern) return '(no pattern)';
243
294
  try {
244
- const { stdout } = await execAsync(`find . -type f -name '${pattern}' 2>/dev/null | head -200`, { cwd });
245
- return truncateStr(stdout || '(no matches)');
295
+ const regex = globToRegex(pattern);
296
+ const matches = [];
297
+ for await (const fullPath of walkFiles(cwd)) {
298
+ const rel = toPosix(relative(cwd, fullPath));
299
+ if (regex.test(rel) || regex.test(basename(fullPath))) {
300
+ matches.push('./' + rel);
301
+ if (matches.length >= 200) break;
302
+ }
303
+ }
304
+ if (matches.length === 0) return '(no matches)';
305
+ return truncateStr(matches.join('\n'));
246
306
  } catch (err) {
247
307
  return `error: ${err.message}`;
248
308
  }
249
309
  }
250
310
  case 'grep': {
251
- const pattern = String(args.pattern ?? '').replace(/'/g, "'\\''");
252
- const glob = String(args.glob ?? '').replace(/'/g, "'\\''");
311
+ const pattern = String(args.pattern ?? '').trim();
312
+ if (!pattern) return '(no pattern)';
313
+ let regex;
253
314
  try {
254
- const cmd = glob
255
- ? `grep -rn --include='${glob}' -E '${pattern}' . 2>/dev/null | head -100`
256
- : `grep -rn -E '${pattern}' . 2>/dev/null | head -100`;
257
- const { stdout } = await execAsync(cmd, { cwd });
258
- return truncateStr(stdout || '(no matches)');
315
+ regex = new RegExp(pattern);
316
+ } catch (err) {
317
+ return `error: invalid regex: ${err.message}`;
318
+ }
319
+ const globPattern = String(args.glob ?? '').trim();
320
+ const globRegex = globPattern ? globToRegex(globPattern) : null;
321
+ const matches = [];
322
+ try {
323
+ outer: for await (const fullPath of walkFiles(cwd)) {
324
+ const rel = toPosix(relative(cwd, fullPath));
325
+ if (globRegex && !(globRegex.test(rel) || globRegex.test(basename(fullPath)))) continue;
326
+ let content;
327
+ try {
328
+ content = await fs.readFile(fullPath, 'utf8');
329
+ } catch {
330
+ continue;
331
+ }
332
+ if (looksBinary(content)) continue;
333
+ const lines = content.split('\n');
334
+ for (let i = 0; i < lines.length; i++) {
335
+ if (regex.test(lines[i])) {
336
+ matches.push(`./${rel}:${i + 1}:${lines[i].slice(0, 200)}`);
337
+ if (matches.length >= 100) break outer;
338
+ }
339
+ }
340
+ }
341
+ if (matches.length === 0) return '(no matches)';
342
+ return truncateStr(matches.join('\n'));
259
343
  } catch (err) {
260
344
  return `error: ${err.message}`;
261
345
  }
package/lib/hooks.mjs CHANGED
@@ -72,11 +72,17 @@ export async function fireHook(eventName, payload, hookDir) {
72
72
  const isShell = hookPath.endsWith('.sh') || !hookPath.includes('.');
73
73
  const isNode = hookPath.endsWith('.mjs') || hookPath.endsWith('.js');
74
74
 
75
+ // On Windows we cannot spawn sh for POSIX shell hooks. Skip with a clear
76
+ // reason so the caller knows the hook did not run. Node hooks are portable.
77
+ if (isShell && process.platform === 'win32') {
78
+ return { action: 'allow', reason: 'shell hooks not supported on Windows; use .mjs' };
79
+ }
80
+
75
81
  return new Promise((resolveHook) => {
76
82
  let child;
77
83
  try {
78
84
  child = isNode
79
- ? spawn('node', [hookPath], { stdio: ['pipe', 'pipe', 'pipe'] })
85
+ ? spawn(process.execPath, [hookPath], { stdio: ['pipe', 'pipe', 'pipe'] })
80
86
  : spawn('sh', [hookPath], { stdio: ['pipe', 'pipe', 'pipe'] });
81
87
  } catch (err) {
82
88
  resolveHook({ action: 'allow', reason: `spawn failed: ${err.message}` });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polylogicai/polycode",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
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",