@polylogicai/polycode 1.1.2 → 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 +19 -3
- package/lib/agentic.mjs +96 -12
- package/lib/hooks.mjs +7 -1
- package/package.json +1 -1
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
|
|
111
|
+
- macOS, Linux, or Windows
|
|
96
112
|
- A Groq API key
|
|
97
113
|
|
|
98
114
|
## Documentation and support
|
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 ?? '').
|
|
292
|
+
const pattern = String(args.pattern ?? '').trim();
|
|
293
|
+
if (!pattern) return '(no pattern)';
|
|
243
294
|
try {
|
|
244
|
-
const
|
|
245
|
-
|
|
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 ?? '').
|
|
252
|
-
|
|
311
|
+
const pattern = String(args.pattern ?? '').trim();
|
|
312
|
+
if (!pattern) return '(no pattern)';
|
|
313
|
+
let regex;
|
|
253
314
|
try {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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(
|
|
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.
|
|
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",
|