@polylogicai/polycode 1.1.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/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrew Salvo / Polylogic AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
22
+
23
+ Polycode is inspired by Claude Code's public architecture. No Claude Code
24
+ code or system prompts are copied. Not affiliated with Anthropic.
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # polycode
2
+
3
+ An agentic coding CLI. Runs on your machine with your keys. Every turn is appended to a SHA-256 chained session log on disk, so your history is auditable, replayable, and portable across machines.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @polylogicai/polycode
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```bash
14
+ export GROQ_API_KEY=gsk_...
15
+ polycode
16
+ ```
17
+
18
+ That opens an interactive session. For one-shot mode, pass your prompt as an argument:
19
+
20
+ ```bash
21
+ polycode "read README.md and summarize it in one sentence"
22
+ ```
23
+
24
+ A free Groq API key is available at `console.groq.com`.
25
+
26
+ ## Configuration
27
+
28
+ polycode reads configuration from environment variables and optionally from a `~/.polycode/secrets.env` file (chmod 600 recommended).
29
+
30
+ | Variable | Purpose | Required |
31
+ |---|---|---|
32
+ | `GROQ_API_KEY` | Primary inference key for tool-use and reasoning | yes |
33
+ | `ANTHROPIC_API_KEY` | Optional high-quality tier for long sessions | no |
34
+ | `POLYCODE_MODEL` | Override the default model | no |
35
+ | `POLYCODE_CWD` | Override the working directory | no |
36
+
37
+ ## Usage
38
+
39
+ polycode runs a standard agentic loop: you ask, it thinks, it uses tools, it returns a result. Available tools are:
40
+
41
+ - `bash` run a shell command in the working directory
42
+ - `read_file` read a file relative to the working directory
43
+ - `write_file` write a file to the working directory
44
+ - `edit_file` replace a substring in a file
45
+ - `glob` list files matching a pattern
46
+ - `grep` search for a regex in files
47
+
48
+ All tool calls are sandboxed to the working directory. polycode refuses any path that escapes it.
49
+
50
+ ## Session history
51
+
52
+ Every turn polycode takes is appended to a SHA-256 chained JSONL file at `~/.polycode/history/YYYY-MM-DD.jsonl`. You own this file. You can:
53
+
54
+ - Read it to see exactly what the agent did
55
+ - Back it up by copying it
56
+ - Hand it to a teammate, who can replay the session on their machine
57
+ - Run `polycode --verify` to confirm the chain is intact
58
+
59
+ ## Slash commands
60
+
61
+ Inside the REPL:
62
+
63
+ ```
64
+ /help show all commands
65
+ /clear clear the terminal
66
+ /history show the session history file path and row count
67
+ /verify verify session history integrity
68
+ /exit leave polycode
69
+ ```
70
+
71
+ ## Command-line flags
72
+
73
+ ```
74
+ polycode interactive REPL
75
+ polycode "<prompt>" one-shot mode
76
+ polycode --version print version
77
+ polycode --help print help
78
+ polycode --history print session history status
79
+ polycode --verify verify session history integrity
80
+ ```
81
+
82
+ ## Safety rules
83
+
84
+ polycode enforces a small set of non-negotiable safety rules out of the box:
85
+
86
+ - Shell commands referencing system paths (`/etc/passwd`, `~/.ssh`, private key files) are refused
87
+ - Code patterns that evaluate untrusted input are refused
88
+ - Tool output containing recognized secret patterns (AWS, GitHub, Stripe, Anthropic, Groq, PEM blocks) is redacted before it reaches the model or the terminal
89
+
90
+ Rules live in `~/.polycode/rules.yaml`. You can add your own.
91
+
92
+ ## Requirements
93
+
94
+ - Node.js 20 or newer
95
+ - macOS or Linux
96
+ - A Groq API key
97
+
98
+ ## Documentation and support
99
+
100
+ - Install and usage: `polylogicai.com/polycode`
101
+ - Issues and questions: `ajs10845@psu.edu`
102
+
103
+ ## License
104
+
105
+ MIT. See `LICENSE`. Built by Polylogic AI.
106
+
107
+ polycode is inspired by Claude Code's public architecture at `docs.claude.com`. polycode is not affiliated with Anthropic. No Claude Code code or system prompts are copied.
@@ -0,0 +1,317 @@
1
+ #!/usr/bin/env node
2
+ // bin/polycode.mjs
3
+ // polycode CLI entry point. Runs on your machine with your keys. Writes an
4
+ // append-only session log on disk.
5
+ //
6
+ // polycode is inspired by Claude Code's public architecture at docs.claude.com.
7
+ // polycode is not affiliated with Anthropic. No Claude Code code or system
8
+ // prompts are copied.
9
+
10
+ import { AgenticLoop } from '../lib/agentic.mjs';
11
+ import { createCanon } from '../lib/canon.mjs';
12
+ import { createRenderer, C } from '../lib/repl-ui.mjs';
13
+ import { dispatchSlash } from '../lib/slash-commands.mjs';
14
+ import { computeAgencyReceipt, formatReceipt } from '../lib/agency-receipt.mjs';
15
+ import { fireHook } from '../lib/hooks.mjs';
16
+ import { compilePacket } from '../lib/compiler.mjs';
17
+ import { loadAnthropicKeys, reportKeyStatus } from '../lib/inference-router.mjs';
18
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
19
+ import { homedir } from 'node:os';
20
+ import { join } from 'node:path';
21
+ import { randomUUID } from 'node:crypto';
22
+ import * as readline from 'node:readline/promises';
23
+ import { stdin, stdout, exit, argv, env, cwd as getCwd } from 'node:process';
24
+ import 'dotenv/config';
25
+
26
+ function loadAmbientEnv() {
27
+ const candidates = [
28
+ join(homedir(), '.polycode/secrets.env'),
29
+ join(homedir(), 'orchestrator/.env'),
30
+ join(homedir(), '.polycode/.env'),
31
+ ];
32
+ for (const p of candidates) {
33
+ if (!existsSync(p)) continue;
34
+ try {
35
+ const content = readFileSync(p, 'utf8');
36
+ for (const line of content.split('\n')) {
37
+ if (line.startsWith('#')) continue;
38
+ const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
39
+ if (match && !env[match[1]]) {
40
+ env[match[1]] = match[2].replace(/^['"]|['"]$/g, '');
41
+ }
42
+ }
43
+ } catch {
44
+ // ignore
45
+ }
46
+ }
47
+ }
48
+
49
+ loadAmbientEnv();
50
+
51
+ function loadRules() {
52
+ const candidates = [
53
+ join(homedir(), '.polycode', 'rules.yaml'),
54
+ join(homedir(), 'polycode', 'rules', 'default.yaml'),
55
+ ];
56
+ for (const p of candidates) {
57
+ if (!existsSync(p)) continue;
58
+ try {
59
+ const content = readFileSync(p, 'utf8');
60
+ // Minimal YAML parser for the flat subset we use: top-level scalars, top-level
61
+ // lists, and one level of nested map or list under a top-level key.
62
+ const rules = {};
63
+ let currentTopKey = null;
64
+ let currentTopIsList = false;
65
+ let currentNestedList = null;
66
+ for (const rawLine of content.split('\n')) {
67
+ const line = rawLine.replace(/#.*$/, '').replace(/\s+$/, '');
68
+ if (!line.trim()) continue;
69
+ const topMatch = line.match(/^([A-Za-z_][A-Za-z0-9_]*):\s*(.*)$/);
70
+ if (topMatch && !line.startsWith(' ')) {
71
+ currentTopKey = topMatch[1];
72
+ currentNestedList = null;
73
+ const inlineVal = topMatch[2].trim();
74
+ if (inlineVal === '') {
75
+ // May be a map or a list; decided by the next indented line.
76
+ rules[currentTopKey] = undefined;
77
+ currentTopIsList = false;
78
+ } else if (/^\[.*\]$/.test(inlineVal)) {
79
+ rules[currentTopKey] = inlineVal.slice(1, -1).split(',').map((x) => x.trim().replace(/^['"]|['"]$/g, ''));
80
+ currentTopIsList = false;
81
+ } else if (/^-?\d+(\.\d+)?$/.test(inlineVal)) {
82
+ rules[currentTopKey] = Number(inlineVal);
83
+ currentTopIsList = false;
84
+ } else if (inlineVal === 'true' || inlineVal === 'false') {
85
+ rules[currentTopKey] = inlineVal === 'true';
86
+ currentTopIsList = false;
87
+ } else {
88
+ rules[currentTopKey] = inlineVal.replace(/^['"]|['"]$/g, '');
89
+ currentTopIsList = false;
90
+ }
91
+ continue;
92
+ }
93
+ const topListMatch = line.match(/^\s{2}-\s*(.+)$/);
94
+ if (topListMatch && currentTopKey) {
95
+ if (!Array.isArray(rules[currentTopKey])) {
96
+ rules[currentTopKey] = [];
97
+ currentTopIsList = true;
98
+ }
99
+ rules[currentTopKey].push(topListMatch[1].trim().replace(/^['"]|['"]$/g, ''));
100
+ continue;
101
+ }
102
+ const subMatch = line.match(/^\s{2}([A-Za-z_][A-Za-z0-9_]*):\s*(.*)$/);
103
+ if (subMatch && currentTopKey && !currentTopIsList) {
104
+ if (typeof rules[currentTopKey] !== 'object' || Array.isArray(rules[currentTopKey])) {
105
+ rules[currentTopKey] = {};
106
+ }
107
+ const subKey = subMatch[1];
108
+ const subVal = subMatch[2].trim();
109
+ if (subVal === '') {
110
+ rules[currentTopKey][subKey] = [];
111
+ currentNestedList = rules[currentTopKey][subKey];
112
+ } else if (/^-?\d+(\.\d+)?$/.test(subVal)) {
113
+ rules[currentTopKey][subKey] = Number(subVal);
114
+ currentNestedList = null;
115
+ } else if (subVal === 'true' || subVal === 'false') {
116
+ rules[currentTopKey][subKey] = subVal === 'true';
117
+ currentNestedList = null;
118
+ } else if (/^\[.*\]$/.test(subVal)) {
119
+ rules[currentTopKey][subKey] = subVal.slice(1, -1).split(',').map((x) => x.trim().replace(/^['"]|['"]$/g, ''));
120
+ currentNestedList = null;
121
+ } else {
122
+ rules[currentTopKey][subKey] = subVal.replace(/^['"]|['"]$/g, '');
123
+ currentNestedList = null;
124
+ }
125
+ continue;
126
+ }
127
+ const nestedListMatch = line.match(/^\s{4}-\s*(.+)$/);
128
+ if (nestedListMatch && currentNestedList) {
129
+ currentNestedList.push(nestedListMatch[1].trim().replace(/^['"]|['"]$/g, ''));
130
+ }
131
+ }
132
+ return rules;
133
+ } catch {
134
+ // next candidate
135
+ }
136
+ }
137
+ return {};
138
+ }
139
+
140
+ const VERSION = '1.1.0';
141
+ const DOCS_URL = 'https://polylogicai.com/polycode';
142
+
143
+ const BANNER = `${C.bold}${C.amber}polycode v${VERSION}${C.reset}
144
+ ${C.dim}An agentic coding CLI. Your keys, your machine, your session log.${C.reset}
145
+ ${C.dim}${DOCS_URL}${C.reset}
146
+ `;
147
+
148
+ const HELP = `${BANNER}
149
+
150
+ ${C.bold}Usage${C.reset}
151
+ polycode Interactive session
152
+ polycode "fix the failing test" One-shot mode
153
+ polycode --version Print version
154
+ polycode --help Print this message
155
+ polycode --history Print session history status
156
+ polycode --verify Verify session history integrity
157
+
158
+ ${C.bold}Environment${C.reset}
159
+ GROQ_API_KEY Required (free tier at console.groq.com)
160
+ ANTHROPIC_API_KEY Optional high-quality tier for long sessions
161
+ POLYCODE_MODEL Override the default model
162
+ POLYCODE_CWD Override the working directory
163
+
164
+ ${C.bold}Documentation${C.reset}
165
+ ${DOCS_URL}
166
+ `;
167
+
168
+ function resolveConfigDir() {
169
+ const dir = join(homedir(), '.polycode');
170
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
171
+ if (!existsSync(join(dir, 'canon'))) mkdirSync(join(dir, 'canon'), { recursive: true });
172
+ if (!existsSync(join(dir, 'hooks'))) mkdirSync(join(dir, 'hooks'), { recursive: true });
173
+ return dir;
174
+ }
175
+
176
+ async function runOneShot(message, { loop, canon, cwd, renderer, state, sessionId, hookDir }) {
177
+ await fireHook('UserPromptSubmit', {
178
+ session_id: sessionId, cwd, hook_event_name: 'UserPromptSubmit', prompt: message,
179
+ }, hookDir);
180
+
181
+ const result = await loop.runTurn({
182
+ canon,
183
+ userMessage: message,
184
+ cwd,
185
+ onEvent: (ev) => renderer.onEvent(ev),
186
+ });
187
+
188
+ state.lastTurnTokens = result.promptTokensUsed;
189
+ state.lastCompiler = result.compilerProvider;
190
+
191
+ const receipt = computeAgencyReceipt({
192
+ primitivesList: result.primitivesList,
193
+ wallClockMs: result.durationMs,
194
+ iterations: result.iterations,
195
+ });
196
+ stdout.write(`${C.dim}${formatReceipt(receipt)} . ${canon.size()} rows in log${C.reset}\n`);
197
+
198
+ await fireHook('Stop', { session_id: sessionId, cwd, hook_event_name: 'Stop' }, hookDir);
199
+ return result;
200
+ }
201
+
202
+ async function runRepl(opts) {
203
+ const rl = readline.createInterface({ input: stdin, output: stdout });
204
+ stdout.write(BANNER + '\n');
205
+ stdout.write(`${C.dim}session log: ${opts.canon.size()} rows . ${opts.canon.lastHash().slice(0, 12)}...${C.reset}\n`);
206
+ stdout.write(`${C.dim}type /help for commands. ctrl+c or /exit to leave.${C.reset}\n\n`);
207
+
208
+ try {
209
+ while (true) {
210
+ const line = await rl.question(`${C.bold}${C.amber}> ${C.reset}`);
211
+ if (!line.trim()) continue;
212
+
213
+ if (line.startsWith('/')) {
214
+ const result = await dispatchSlash(line, { canon: opts.canon, state: opts.state, stdout });
215
+ if (result.exit) break;
216
+ stdout.write('\n');
217
+ continue;
218
+ }
219
+
220
+ await runOneShot(line, opts);
221
+ stdout.write('\n');
222
+ }
223
+ } finally {
224
+ rl.close();
225
+ }
226
+ }
227
+
228
+ async function main() {
229
+ const args = argv.slice(2);
230
+
231
+ if (args.includes('--help') || args.includes('-h')) { stdout.write(HELP); exit(0); }
232
+ if (args.includes('--version') || args.includes('-v')) {
233
+ stdout.write(`polycode ${VERSION}\n`);
234
+ exit(0);
235
+ }
236
+
237
+ const configDir = resolveConfigDir();
238
+
239
+ const apiKey = env.GROQ_API_KEY;
240
+ if (!apiKey) {
241
+ stdout.write(`${C.red}polycode error${C.reset}: GROQ_API_KEY is not set.\n`);
242
+ exit(1);
243
+ }
244
+
245
+ const rules = loadRules();
246
+ const model = env.POLYCODE_MODEL || 'moonshotai/kimi-k2-instruct';
247
+ const cwd = env.POLYCODE_CWD || getCwd();
248
+ const hookDir = env.POLYCODE_HOOK_DIR || join(configDir, 'hooks');
249
+ const canonFile = env.POLYCODE_CANON_FILE || join(configDir, 'canon', `${new Date().toISOString().slice(0, 10)}.jsonl`);
250
+
251
+ const canon = createCanon(canonFile);
252
+
253
+ if (args.includes('--history') || args.includes('--log')) {
254
+ stdout.write(`session log: ${canonFile}\nrows: ${canon.size()}\nlast_hash: ${canon.lastHash()}\n`);
255
+ exit(0);
256
+ }
257
+
258
+ if (args.includes('--verify')) {
259
+ const r = canon.verify();
260
+ if (r.valid) {
261
+ stdout.write(`${C.amber}session log verified${C.reset}: ${r.rows} rows, last ${r.last_hash}\n`);
262
+ exit(0);
263
+ } else {
264
+ stdout.write(`${C.red}session log broken${C.reset}: ${r.reason}\n`);
265
+ exit(1);
266
+ }
267
+ }
268
+
269
+ const packetFlagIndex = args.indexOf('--packet');
270
+ if (packetFlagIndex >= 0) {
271
+ const question = args.slice(packetFlagIndex + 1).join(' ').replace(/^["']|["']$/g, '') || 'default';
272
+ const pkt = await compilePacket(canon, question, cwd);
273
+ stdout.write(`--- polycode compile-packet preview ---\n`);
274
+ stdout.write(`compiler: ${pkt.compilerProvider}\n`);
275
+ stdout.write(`estimated_tokens: ${pkt.estimatedTokens}\n`);
276
+ stdout.write(`selected_rows: ${JSON.stringify(pkt.selectedRows)}\n`);
277
+ stdout.write(`fallback: ${pkt.fallback}\n`);
278
+ stdout.write(`---\n`);
279
+ stdout.write(pkt.prompt + '\n');
280
+ stdout.write(`---\n`);
281
+ exit(0);
282
+ }
283
+
284
+ const sessionId = randomUUID();
285
+ canon.append('session_start', { session_id: sessionId, cwd, model });
286
+
287
+ await fireHook('SessionStart', {
288
+ session_id: sessionId,
289
+ cwd,
290
+ hook_event_name: 'SessionStart',
291
+ matcher: 'startup',
292
+ canon_path: canonFile,
293
+ }, hookDir);
294
+
295
+ const loop = new AgenticLoop({ apiKey, model, rules });
296
+ const renderer = createRenderer(stdout);
297
+ const state = { lastTurnTokens: 0, lastCompiler: null };
298
+ const opts = { loop, canon, cwd, renderer, state, sessionId, hookDir };
299
+
300
+ const positional = args.filter((a) => !a.startsWith('--') && args[args.indexOf(a) - 1] !== '--packet');
301
+ if (positional.length > 0) {
302
+ await runOneShot(positional.join(' '), opts);
303
+ } else {
304
+ await runRepl(opts);
305
+ }
306
+
307
+ canon.append('session_end', { session_id: sessionId });
308
+ await fireHook('SessionEnd', { session_id: sessionId, cwd, hook_event_name: 'SessionEnd' }, hookDir);
309
+
310
+ exit(0);
311
+ }
312
+
313
+ main().catch((err) => {
314
+ stdout.write(`${C.red}polycode error${C.reset}: ${err.message}\n`);
315
+ if (env.POLYCODE_DEBUG) stdout.write(err.stack + '\n');
316
+ exit(1);
317
+ });
@@ -0,0 +1,45 @@
1
+ // lib/agency-receipt.mjs
2
+ // Per-turn receipt computation. Aggregates verification results across all
3
+ // commitments in the turn into a small set of numeric factors and a product.
4
+ // Reported at the end of each REPL turn in plain-text form.
5
+
6
+ export function computeAgencyReceipt({ primitivesList, wallClockMs, iterations }) {
7
+ const weights = { PASS: 1, PENDING: 0.5, FAIL: 0 };
8
+
9
+ let witnessSum = 0;
10
+ let witnessCount = 0;
11
+ let fidelityScalarSum = 0;
12
+ let fidelityScalarCount = 0;
13
+
14
+ for (const p of primitivesList) {
15
+ if (!p) continue;
16
+ for (const [name, prim] of Object.entries(p)) {
17
+ if (name === 'g_fidelity') {
18
+ if (typeof prim.scalar === 'number') {
19
+ fidelityScalarSum += prim.scalar;
20
+ fidelityScalarCount++;
21
+ }
22
+ continue;
23
+ }
24
+ const w = weights[prim?.verdict];
25
+ if (w != null) {
26
+ witnessSum += w;
27
+ witnessCount++;
28
+ }
29
+ }
30
+ }
31
+
32
+ const V = witnessCount > 0 ? witnessSum / witnessCount : 0;
33
+ const G = fidelityScalarCount > 0 ? fidelityScalarSum / fidelityScalarCount : 0.5;
34
+ const I = V * G;
35
+ const seconds = wallClockMs / 1000;
36
+ const E = iterations > 0 ? (seconds * 10) / iterations : seconds;
37
+ const A = I * E;
38
+
39
+ return { V, G, I, E, A, seconds, iterations };
40
+ }
41
+
42
+ export function formatReceipt(r) {
43
+ // Unambiguous-decimal format. Parsers can split on space-delimited keys.
44
+ return `Agency: V=${r.V.toFixed(2)} G=${r.G.toFixed(2)} I=${r.I.toFixed(2)} E=${r.E.toFixed(2)} A=${r.A.toFixed(2)} . ${r.seconds.toFixed(2)}s . ${r.iterations} iter`;
45
+ }