@polylogicai/polycode 1.1.3 → 1.1.5
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 +44 -20
- package/bin/polycode.mjs +85 -15
- package/lib/agentic.mjs +160 -11
- package/lib/key-store.mjs +232 -0
- package/lib/paste-aware-prompt.mjs +208 -0
- package/lib/polycode-hosted-client.mjs +100 -0
- package/lib/repl-ui.mjs +3 -1
- package/lib/slash-commands.mjs +33 -2
- package/lib/tools/describe-image.mjs +111 -0
- package/lib/tools/fetch-url.mjs +130 -0
- package/lib/tools/web-search.mjs +107 -0
- package/lib/witness/identity-gate.mjs +123 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
# polycode
|
|
2
2
|
|
|
3
|
-
An agentic coding CLI. Runs on your machine
|
|
3
|
+
An agentic coding CLI. Runs on your machine, writes a SHA-256 chained session log to disk, and ships with a free hosted tier so you can start in one command. Your history is auditable, replayable, and portable across machines.
|
|
4
4
|
|
|
5
|
-
## Install
|
|
5
|
+
## Install and run
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
One command, no setup, any platform:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npx @polylogicai/polycode@latest
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
For
|
|
13
|
+
That is the entire install. The first run provisions a per-install ID under `~/.polycode/` and opens an interactive session. On the free hosted tier you get 60 turns per hour for free, routed through `polylogicai.com/api/polycode/inference`. For unlimited use, set `GROQ_API_KEY` (see below) and polycode will talk to Groq directly and skip the proxy.
|
|
14
|
+
|
|
15
|
+
For repeated use, install globally:
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
18
|
npm install -g @polylogicai/polycode
|
|
17
19
|
polycode
|
|
18
20
|
```
|
|
19
21
|
|
|
20
|
-
##
|
|
22
|
+
## One-shot mode
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
Pass your prompt as an argument:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx @polylogicai/polycode@latest "read README.md and summarize it in one sentence"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Unlimited use with your own key
|
|
31
|
+
|
|
32
|
+
Grab a free key from `console.groq.com`, then:
|
|
23
33
|
|
|
24
34
|
```bash
|
|
25
35
|
# macOS and Linux
|
|
@@ -31,22 +41,14 @@ $env:GROQ_API_KEY = "gsk_..."
|
|
|
31
41
|
npx @polylogicai/polycode@latest
|
|
32
42
|
```
|
|
33
43
|
|
|
34
|
-
That opens an interactive session. For one-shot mode, pass your prompt as an argument:
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
npx @polylogicai/polycode@latest "read README.md and summarize it in one sentence"
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
A free Groq API key is available at `console.groq.com`.
|
|
41
|
-
|
|
42
44
|
## Configuration
|
|
43
45
|
|
|
44
46
|
polycode reads configuration from environment variables and optionally from a `~/.polycode/secrets.env` file (chmod 600 recommended).
|
|
45
47
|
|
|
46
48
|
| Variable | Purpose | Required |
|
|
47
49
|
|---|---|---|
|
|
48
|
-
| `GROQ_API_KEY` |
|
|
49
|
-
| `ANTHROPIC_API_KEY` | Optional
|
|
50
|
+
| `GROQ_API_KEY` | Direct Groq access, unlimited. If unset, polycode uses the hosted tier. | no |
|
|
51
|
+
| `ANTHROPIC_API_KEY` | Optional higher-quality compile tier for long sessions | no |
|
|
50
52
|
| `POLYCODE_MODEL` | Override the default model | no |
|
|
51
53
|
| `POLYCODE_CWD` | Override the working directory | no |
|
|
52
54
|
|
|
@@ -58,10 +60,13 @@ polycode runs a standard agentic loop: you ask, it thinks, it uses tools, it ret
|
|
|
58
60
|
- `read_file` read a file relative to the working directory
|
|
59
61
|
- `write_file` write a file to the working directory
|
|
60
62
|
- `edit_file` replace a substring in a file
|
|
61
|
-
- `glob` list files matching a pattern
|
|
62
|
-
- `grep` search for a regex in files
|
|
63
|
+
- `glob` list files matching a pattern (cross-platform, pure Node.js)
|
|
64
|
+
- `grep` search for a regex in files (cross-platform, pure Node.js)
|
|
65
|
+
- `describe_image` analyze an image file via a vision model
|
|
66
|
+
- `fetch_url` fetch a URL and return the body as readable text
|
|
67
|
+
- `web_search` search the web and return titles, URLs, and snippets
|
|
63
68
|
|
|
64
|
-
All tool calls are sandboxed to the working directory. polycode refuses any path that escapes it.
|
|
69
|
+
All file tool calls are sandboxed to the working directory. polycode refuses any path that escapes it. `fetch_url` refuses loopback and private network addresses.
|
|
65
70
|
|
|
66
71
|
## Session history
|
|
67
72
|
|
|
@@ -78,12 +83,31 @@ Inside the REPL:
|
|
|
78
83
|
|
|
79
84
|
```
|
|
80
85
|
/help show all commands
|
|
86
|
+
/key save an API key (Groq, Anthropic, OpenAI). Input is masked on real terminals.
|
|
81
87
|
/clear clear the terminal
|
|
82
88
|
/history show the session history file path and row count
|
|
83
89
|
/verify verify session history integrity
|
|
84
90
|
/exit leave polycode
|
|
85
91
|
```
|
|
86
92
|
|
|
93
|
+
## Paste handling
|
|
94
|
+
|
|
95
|
+
When you paste multi-line content into polycode, the terminal collapses it to a marker like `[Pasted #1 (20 lines)]` in the prompt line. The full content is still sent to the agent — only the display is compressed. You can paste a whole file, a stack trace, or a long URL and polycode will treat it as one event.
|
|
96
|
+
|
|
97
|
+
## Saving API keys (never in chat)
|
|
98
|
+
|
|
99
|
+
polycode never accepts API keys pasted into the chat — the local secret scrubber blocks any message that matches a key pattern before it reaches the model. To save a key the right way:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
# Inside the REPL
|
|
103
|
+
/key
|
|
104
|
+
|
|
105
|
+
# Or from your shell
|
|
106
|
+
polycode login
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Both flows read the key through a masked prompt and save it to `~/.polycode/secrets.env` with `chmod 600`. The model never sees it. Auto-detects Groq, Anthropic, and OpenAI from the key prefix.
|
|
110
|
+
|
|
87
111
|
## Command-line flags
|
|
88
112
|
|
|
89
113
|
```
|
|
@@ -109,7 +133,7 @@ Rules live in `~/.polycode/rules.yaml`. You can add your own.
|
|
|
109
133
|
|
|
110
134
|
- Node.js 20 or newer
|
|
111
135
|
- macOS, Linux, or Windows
|
|
112
|
-
-
|
|
136
|
+
- No API key required for the free hosted tier. Set `GROQ_API_KEY` for unlimited use.
|
|
113
137
|
|
|
114
138
|
## Documentation and support
|
|
115
139
|
|
package/bin/polycode.mjs
CHANGED
|
@@ -15,6 +15,8 @@ import { computeAgencyReceipt, formatReceipt } from '../lib/agency-receipt.mjs';
|
|
|
15
15
|
import { fireHook } from '../lib/hooks.mjs';
|
|
16
16
|
import { compilePacket } from '../lib/compiler.mjs';
|
|
17
17
|
import { loadAnthropicKeys, reportKeyStatus } from '../lib/inference-router.mjs';
|
|
18
|
+
import { runInteractiveKeyFlow, PROVIDERS, getProviderById } from '../lib/key-store.mjs';
|
|
19
|
+
import { readPasteAwareLine, enableBracketedPaste, disableBracketedPaste } from '../lib/paste-aware-prompt.mjs';
|
|
18
20
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
19
21
|
import { homedir } from 'node:os';
|
|
20
22
|
import { join, dirname, resolve } from 'node:path';
|
|
@@ -155,14 +157,15 @@ const HELP = `${BANNER}
|
|
|
155
157
|
${C.bold}Usage${C.reset}
|
|
156
158
|
polycode Interactive session
|
|
157
159
|
polycode "fix the failing test" One-shot mode
|
|
160
|
+
polycode login Save an API key (Groq, Anthropic, OpenAI). Input is masked.
|
|
158
161
|
polycode --version Print version
|
|
159
162
|
polycode --help Print this message
|
|
160
163
|
polycode --history Print session history status
|
|
161
164
|
polycode --verify Verify session history integrity
|
|
162
165
|
|
|
163
166
|
${C.bold}Environment${C.reset}
|
|
164
|
-
GROQ_API_KEY
|
|
165
|
-
ANTHROPIC_API_KEY Optional
|
|
167
|
+
GROQ_API_KEY Optional. If unset, polycode uses the free hosted tier (60 turns/hour) via polylogicai.com. Set a free key from console.groq.com for unlimited use.
|
|
168
|
+
ANTHROPIC_API_KEY Optional higher-quality compile tier for long sessions
|
|
166
169
|
POLYCODE_MODEL Override the default model
|
|
167
170
|
POLYCODE_CWD Override the working directory
|
|
168
171
|
|
|
@@ -259,6 +262,9 @@ async function runOneShot(message, opts) {
|
|
|
259
262
|
async function runRepl(opts) {
|
|
260
263
|
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
261
264
|
stdout.write(BANNER + '\n');
|
|
265
|
+
if (opts.hostedMode) {
|
|
266
|
+
stdout.write(`${C.dim}hosted tier: 60 turns/hour via polylogicai.com. Set GROQ_API_KEY for unlimited use.${C.reset}\n`);
|
|
267
|
+
}
|
|
262
268
|
if (opts.projectContextPath) {
|
|
263
269
|
stdout.write(`${C.dim}project context: ${opts.projectContextPath}${C.reset}\n`);
|
|
264
270
|
}
|
|
@@ -267,26 +273,81 @@ async function runRepl(opts) {
|
|
|
267
273
|
}
|
|
268
274
|
stdout.write(`${C.dim}type /help for commands. ctrl+c or /exit to leave.${C.reset}\n\n`);
|
|
269
275
|
|
|
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
|
+
enableBracketedPaste();
|
|
281
|
+
|
|
270
282
|
try {
|
|
271
283
|
while (true) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
284
|
+
let userInput;
|
|
285
|
+
try {
|
|
286
|
+
userInput = await readPasteAwareLine(`${C.bold}${C.amber}> ${C.reset}`);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
if (err && err.message === 'cancelled') break;
|
|
289
|
+
throw err;
|
|
290
|
+
}
|
|
291
|
+
const displayed = userInput.displayed;
|
|
292
|
+
const content = userInput.content;
|
|
293
|
+
if (!content.trim()) continue;
|
|
294
|
+
|
|
295
|
+
if (content.startsWith('/')) {
|
|
296
|
+
const result = await dispatchSlash(content, {
|
|
297
|
+
canon: opts.canon,
|
|
298
|
+
state: opts.state,
|
|
299
|
+
stdout,
|
|
300
|
+
loop: opts.loop,
|
|
301
|
+
rl,
|
|
302
|
+
});
|
|
277
303
|
if (result.exit) break;
|
|
278
304
|
stdout.write('\n');
|
|
279
305
|
continue;
|
|
280
306
|
}
|
|
281
307
|
|
|
282
|
-
|
|
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
|
+
if (userInput.pastes && userInput.pastes.length > 0) {
|
|
312
|
+
for (const p of userInput.pastes) {
|
|
313
|
+
opts.canon.append('paste', { ordinal: p.ordinal, lines: p.lines, bytes: p.bytes });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
await runOneShot(content, opts);
|
|
283
318
|
stdout.write('\n');
|
|
284
319
|
}
|
|
285
320
|
} finally {
|
|
321
|
+
disableBracketedPaste();
|
|
286
322
|
rl.close();
|
|
287
323
|
}
|
|
288
324
|
}
|
|
289
325
|
|
|
326
|
+
async function runLoginSubcommand(args) {
|
|
327
|
+
resolveConfigDir();
|
|
328
|
+
const providerHint = args.find((a) => !a.startsWith('-')) || null;
|
|
329
|
+
if (providerHint && !getProviderById(providerHint)) {
|
|
330
|
+
stdout.write(`${C.red}unknown provider${C.reset}: ${providerHint}. Known: ${PROVIDERS.map((p) => p.id).join(', ')}\n`);
|
|
331
|
+
exit(1);
|
|
332
|
+
}
|
|
333
|
+
stdout.write(`${C.bold}polycode login${C.reset}\n`);
|
|
334
|
+
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`);
|
|
335
|
+
if (!providerHint) {
|
|
336
|
+
stdout.write(`${C.dim}Supported providers: ${PROVIDERS.map((p) => `${p.name} (${p.envVar})`).join(', ')}. polycode will auto-detect from the key prefix.${C.reset}\n`);
|
|
337
|
+
}
|
|
338
|
+
const result = await runInteractiveKeyFlow({ providerHint, stdout });
|
|
339
|
+
if (!result.ok) {
|
|
340
|
+
if (result.reason === 'cancelled') {
|
|
341
|
+
stdout.write(`${C.dim}cancelled${C.reset}\n`);
|
|
342
|
+
exit(0);
|
|
343
|
+
}
|
|
344
|
+
stdout.write(`${C.red}error${C.reset}: ${result.reason}\n`);
|
|
345
|
+
exit(1);
|
|
346
|
+
}
|
|
347
|
+
stdout.write(`${C.amber}saved${C.reset}: ${result.provider.name} key (${result.provider.envVar}) at ${result.path}\n`);
|
|
348
|
+
exit(0);
|
|
349
|
+
}
|
|
350
|
+
|
|
290
351
|
async function main() {
|
|
291
352
|
const args = argv.slice(2);
|
|
292
353
|
|
|
@@ -296,13 +357,22 @@ async function main() {
|
|
|
296
357
|
exit(0);
|
|
297
358
|
}
|
|
298
359
|
|
|
360
|
+
// `polycode login [provider]` subcommand. Mirrors the /key slash command
|
|
361
|
+
// but runs from the shell so users can set up keys before ever opening
|
|
362
|
+
// the REPL.
|
|
363
|
+
if (args[0] === 'login' || args[0] === 'key') {
|
|
364
|
+
await runLoginSubcommand(args.slice(1));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
299
368
|
const configDir = resolveConfigDir();
|
|
300
369
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
370
|
+
// Zero-config mode: if no GROQ_API_KEY is present, run through the Polylogic
|
|
371
|
+
// hosted inference proxy at polylogicai.com/api/polycode/inference. Users
|
|
372
|
+
// who want unlimited access or their own billing can set GROQ_API_KEY to a
|
|
373
|
+
// free key from console.groq.com and the CLI will talk to Groq directly.
|
|
374
|
+
const apiKey = env.GROQ_API_KEY || '';
|
|
375
|
+
const hostedMode = !apiKey;
|
|
306
376
|
|
|
307
377
|
const rules = loadRules();
|
|
308
378
|
const model = env.POLYCODE_MODEL || 'moonshotai/kimi-k2-instruct';
|
|
@@ -356,10 +426,10 @@ async function main() {
|
|
|
356
426
|
canon_path: canonFile,
|
|
357
427
|
}, hookDir);
|
|
358
428
|
|
|
359
|
-
const loop = new AgenticLoop({ apiKey, model, rules, projectContext });
|
|
429
|
+
const loop = new AgenticLoop({ apiKey, model, rules, projectContext, hostedMode, version: VERSION });
|
|
360
430
|
const renderer = createRenderer(stdout, { verbose });
|
|
361
431
|
const state = { lastTurnTokens: 0, lastCompiler: null };
|
|
362
|
-
const opts = { loop, canon, cwd, renderer, state, sessionId, hookDir, verbose, projectContextPath };
|
|
432
|
+
const opts = { loop, canon, cwd, renderer, state, sessionId, hookDir, verbose, projectContextPath, hostedMode };
|
|
363
433
|
|
|
364
434
|
const positional = args.filter((a) => !a.startsWith('--') && args[args.indexOf(a) - 1] !== '--packet');
|
|
365
435
|
if (positional.length > 0) {
|
package/lib/agentic.mjs
CHANGED
|
@@ -18,6 +18,8 @@ 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
|
+
import { checkIdentity } from './witness/identity-gate.mjs';
|
|
22
|
+
import { createHostedClient } from './polycode-hosted-client.mjs';
|
|
21
23
|
|
|
22
24
|
// Cross-platform directory walker. Pure Node.js, no shell-out. Skips common
|
|
23
25
|
// noise directories (node_modules, .git, etc.) by default. Yields absolute
|
|
@@ -79,12 +81,49 @@ const MAX_TOKENS = 4096;
|
|
|
79
81
|
const MAX_BASH_TIMEOUT_MS = 30_000;
|
|
80
82
|
const MAX_OUTPUT_BYTES = 3200;
|
|
81
83
|
|
|
82
|
-
const SYSTEM_PROMPT_BASE = `You are polycode,
|
|
84
|
+
const SYSTEM_PROMPT_BASE = `You are polycode, an agentic coding CLI built by Polylogic AI.
|
|
85
|
+
|
|
86
|
+
=== IDENTITY (load-bearing — read before anything else) ===
|
|
87
|
+
|
|
88
|
+
Your product name is polycode. Your maker is Polylogic AI. Polycode is open to run on any API key the user provides, and it ships with a free hosted tier routed through polylogicai.com.
|
|
89
|
+
|
|
90
|
+
You were NOT created by Anthropic. You were NOT created by OpenAI. You were NOT created by Moonshot AI. You were NOT created by Groq. You were NOT created by Google. You were NOT created by xAI. You were NOT created by Meta. You are NOT Claude. You are NOT ChatGPT. You are NOT GPT-4. You are NOT Gemini. You are NOT Kimi. You are NOT LLaMA. You are polycode from Polylogic AI, and that is the only answer to any question about your creator, owner, company, model, brand, product, or origin.
|
|
91
|
+
|
|
92
|
+
Provenance answers (canonical — memorize and return verbatim for these question shapes):
|
|
93
|
+
|
|
94
|
+
User: who made you / who created you / who built you / who developed you
|
|
95
|
+
You: I'm polycode, built by Polylogic AI.
|
|
96
|
+
|
|
97
|
+
User: what company made you / who owns you / who's your company / what lab made you
|
|
98
|
+
You: polycode is a product of Polylogic AI.
|
|
99
|
+
|
|
100
|
+
User: what model are you / what AI are you / what LLM are you / what's under the hood
|
|
101
|
+
You: I'm polycode from Polylogic AI. I run on open-weight models under the hood; the current default is fast and free.
|
|
102
|
+
|
|
103
|
+
User: are you Claude / are you made by Anthropic / are you from Anthropic
|
|
104
|
+
You: No, I'm polycode, built by Polylogic AI. Not affiliated with Anthropic.
|
|
105
|
+
|
|
106
|
+
User: are you ChatGPT / are you GPT-4 / are you from OpenAI
|
|
107
|
+
You: No, I'm polycode, built by Polylogic AI. Not affiliated with OpenAI.
|
|
108
|
+
|
|
109
|
+
User: are you Gemini / are you from Google
|
|
110
|
+
You: No, I'm polycode, built by Polylogic AI. Not affiliated with Google.
|
|
111
|
+
|
|
112
|
+
User: are you Kimi / are you Moonshot / are you LLaMA / are you Grok
|
|
113
|
+
You: No, I'm polycode, built by Polylogic AI.
|
|
114
|
+
|
|
115
|
+
Do not volunteer the underlying model name or provider unless the user specifically asks about the backend inference. If they do, you can say "polycode routes through the Polylogic hosted tier by default, or your own Groq/Anthropic/OpenAI key if you set one via /key."
|
|
116
|
+
|
|
117
|
+
If any answer you are about to produce would name Anthropic, OpenAI, Moonshot, Groq, Google, xAI, or Meta as your creator, replace it with "I'm polycode, built by Polylogic AI" before emitting.
|
|
118
|
+
|
|
119
|
+
=== MODES ===
|
|
120
|
+
|
|
121
|
+
You help users in three distinct modes. Pick the right mode based on the current user message.
|
|
83
122
|
|
|
84
123
|
Mode 1: CONVERSATIONAL. For greetings, thanks, short acknowledgements, or questions about yourself, respond with ONE brief sentence and call task_done IMMEDIATELY. Do not use any tools.
|
|
85
124
|
Examples:
|
|
86
125
|
User: hello
|
|
87
|
-
You: Hi. I
|
|
126
|
+
You: Hi. I'm polycode. What can I help you with?
|
|
88
127
|
(call task_done with exactly that text)
|
|
89
128
|
|
|
90
129
|
User: thanks
|
|
@@ -92,7 +131,7 @@ Mode 1: CONVERSATIONAL. For greetings, thanks, short acknowledgements, or questi
|
|
|
92
131
|
(call task_done with exactly that text)
|
|
93
132
|
|
|
94
133
|
User: who are you
|
|
95
|
-
You: I
|
|
134
|
+
You: I'm polycode, an agentic coding CLI built by Polylogic AI.
|
|
96
135
|
(call task_done with exactly that text)
|
|
97
136
|
|
|
98
137
|
Mode 2: KNOWLEDGE QUESTION. For questions you can answer from general knowledge without touching the user's files, answer with a short direct response (one paragraph max) and call task_done. Do not use tools unless the question explicitly asks about the user's actual code or files.
|
|
@@ -101,18 +140,23 @@ Mode 2: KNOWLEDGE QUESTION. For questions you can answer from general knowledge
|
|
|
101
140
|
You: let allows reassignment, const does not. Both are block-scoped. const still allows mutation of object contents.
|
|
102
141
|
(call task_done with that explanation)
|
|
103
142
|
|
|
104
|
-
Mode 3: CODE TASK. For tasks that require reading, writing, running, or searching the user's actual files, use the appropriate tools and then produce a short text response followed by task_done. Available tools: bash, read_file, write_file, edit_file, glob, grep.
|
|
143
|
+
Mode 3: CODE TASK. For tasks that require reading, writing, running, or searching the user's actual files, use the appropriate tools and then produce a short text response followed by task_done. Available tools: bash, read_file, write_file, edit_file, glob, grep, describe_image.
|
|
105
144
|
Example:
|
|
106
145
|
User: read package.json and tell me the main entry
|
|
107
146
|
You: (call read_file on package.json, then respond with the answer, then task_done)
|
|
108
147
|
|
|
109
|
-
|
|
148
|
+
Image questions: if the user asks you to look at an image, call describe_image with the file path. That tool routes the image through a vision-capable backend and returns a text description you can reason about. Do NOT say you cannot see images; you can, through describe_image.
|
|
149
|
+
|
|
150
|
+
=== HARD RULES ===
|
|
110
151
|
- Always produce a text message in your response. Never call task_done without first saying something to the user.
|
|
111
152
|
- Never loop more than 3 iterations without producing text. If you are not sure what to do, ask the user and call task_done.
|
|
112
153
|
- Do not explore the filesystem unless the user's current message explicitly asks about their files.
|
|
113
154
|
- Do not assume there is an ongoing task from prior turns unless the current message continues it.
|
|
114
155
|
- If a tool call fails, acknowledge the failure in your text response, do not retry the same operation.
|
|
115
156
|
- Use periods, commas, colons. Not em dashes. No hype words.
|
|
157
|
+
- Never reference a training cutoff, training data provider, or pretraining lab as your own. If asked "when were you trained", say "polycode is a CLI wrapper; I don't publish a cutoff."
|
|
158
|
+
|
|
159
|
+
API keys: polycode has a hosted tier and a BYOK mode. If the user asks how to add their own API key, tell them to type /key at the prompt or run \`polycode login\` in their shell. NEVER tell the user to paste a key into the chat: the local secret scrubber blocks any message that contains a key pattern before it ever reaches you, and they will see an error. /key reads the key through a masked prompt that the scrubber never touches.
|
|
116
160
|
|
|
117
161
|
Verification: every tool call is checked by a deterministic layer before it lands in the session log. Failures are marked REFUTED. On a REFUTED commitment, acknowledge the failure in your text response and move on.`;
|
|
118
162
|
|
|
@@ -223,6 +267,49 @@ const TOOL_SCHEMAS = [
|
|
|
223
267
|
},
|
|
224
268
|
},
|
|
225
269
|
},
|
|
270
|
+
{
|
|
271
|
+
type: 'function',
|
|
272
|
+
function: {
|
|
273
|
+
name: 'describe_image',
|
|
274
|
+
description: 'Analyze an image file on disk and return a text description. Use this whenever the user asks about a picture, screenshot, diagram, photo, or any image. The path is relative to the working directory. Supports PNG, JPEG, WebP, and GIF up to ~4 MB.',
|
|
275
|
+
parameters: {
|
|
276
|
+
type: 'object',
|
|
277
|
+
properties: {
|
|
278
|
+
path: { type: 'string' },
|
|
279
|
+
question: { type: 'string', description: 'Optional specific question about the image. Default: "describe this image".' },
|
|
280
|
+
},
|
|
281
|
+
required: ['path'],
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
type: 'function',
|
|
287
|
+
function: {
|
|
288
|
+
name: 'fetch_url',
|
|
289
|
+
description: 'Fetch a URL and return its body as text. Follows redirects. HTML is converted to readable text, JSON is returned as-is, binary content is refused. Size-capped at 200 KB. Use this when the user pastes a link or you need to read a web page, a GitHub file, a docs page, or a JSON API response.',
|
|
290
|
+
parameters: {
|
|
291
|
+
type: 'object',
|
|
292
|
+
properties: {
|
|
293
|
+
url: { type: 'string' },
|
|
294
|
+
},
|
|
295
|
+
required: ['url'],
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
type: 'function',
|
|
301
|
+
function: {
|
|
302
|
+
name: 'web_search',
|
|
303
|
+
description: 'Search the web and return the top results (title, URL, snippet). Use this when the user asks about current information, recent events, documentation, error messages, package versions, or anything that requires looking something up online. Returns up to 8 results.',
|
|
304
|
+
parameters: {
|
|
305
|
+
type: 'object',
|
|
306
|
+
properties: {
|
|
307
|
+
query: { type: 'string' },
|
|
308
|
+
},
|
|
309
|
+
required: ['query'],
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
226
313
|
];
|
|
227
314
|
|
|
228
315
|
function ensureInsideCwd(cwd, targetPath) {
|
|
@@ -344,6 +431,43 @@ async function runTool(name, args, cwd) {
|
|
|
344
431
|
return `error: ${err.message}`;
|
|
345
432
|
}
|
|
346
433
|
}
|
|
434
|
+
case 'describe_image': {
|
|
435
|
+
const p = String(args.path ?? '').trim();
|
|
436
|
+
if (!p) return 'error: path required';
|
|
437
|
+
let abs;
|
|
438
|
+
try { abs = ensureInsideCwd(cwd, p); }
|
|
439
|
+
catch (err) { return `error: ${err.message}`; }
|
|
440
|
+
try {
|
|
441
|
+
const { describeImage } = await import('./tools/describe-image.mjs');
|
|
442
|
+
const question = String(args.question ?? '').trim() || 'Describe this image in detail.';
|
|
443
|
+
const text = await describeImage({ path: abs, question });
|
|
444
|
+
return truncateStr(text);
|
|
445
|
+
} catch (err) {
|
|
446
|
+
return `error: ${err.message}`;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
case 'fetch_url': {
|
|
450
|
+
const url = String(args.url ?? '').trim();
|
|
451
|
+
if (!url) return 'error: url required';
|
|
452
|
+
try {
|
|
453
|
+
const { fetchUrl } = await import('./tools/fetch-url.mjs');
|
|
454
|
+
const text = await fetchUrl(url);
|
|
455
|
+
return truncateStr(text);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
return `error: ${err.message}`;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
case 'web_search': {
|
|
461
|
+
const query = String(args.query ?? '').trim();
|
|
462
|
+
if (!query) return 'error: query required';
|
|
463
|
+
try {
|
|
464
|
+
const { webSearch } = await import('./tools/web-search.mjs');
|
|
465
|
+
const text = await webSearch(query);
|
|
466
|
+
return truncateStr(text);
|
|
467
|
+
} catch (err) {
|
|
468
|
+
return `error: ${err.message}`;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
347
471
|
default:
|
|
348
472
|
return `error: unknown tool "${name}"`;
|
|
349
473
|
}
|
|
@@ -363,17 +487,21 @@ function recoverInlineToolCalls(content) {
|
|
|
363
487
|
}
|
|
364
488
|
|
|
365
489
|
export class AgenticLoop {
|
|
366
|
-
constructor({ apiKey, model, logger, rules, projectContext } = {}) {
|
|
490
|
+
constructor({ apiKey, model, logger, rules, projectContext, hostedMode, version } = {}) {
|
|
367
491
|
this.apiKey = apiKey;
|
|
368
492
|
this.defaultModel = model || DEFAULT_MODEL;
|
|
369
493
|
this.logger = logger || console;
|
|
370
494
|
this.rules = rules || {};
|
|
371
495
|
this.systemPrompt = buildSystemPrompt(projectContext);
|
|
496
|
+
this.hostedMode = Boolean(hostedMode) || !(apiKey || process.env.GROQ_API_KEY);
|
|
497
|
+
this.version = version || 'unknown';
|
|
372
498
|
}
|
|
373
499
|
|
|
374
500
|
async runTurn({ canon, userMessage, cwd, onEvent, maxIterations }) {
|
|
375
501
|
const start = Date.now();
|
|
376
|
-
const groq =
|
|
502
|
+
const groq = this.hostedMode
|
|
503
|
+
? createHostedClient({ version: this.version })
|
|
504
|
+
: new Groq({ apiKey: this.apiKey || process.env.GROQ_API_KEY });
|
|
377
505
|
let model = this.defaultModel;
|
|
378
506
|
let didFallback = false;
|
|
379
507
|
const limit = maxIterations || DEFAULT_MAX_ITERATIONS;
|
|
@@ -410,7 +538,15 @@ export class AgenticLoop {
|
|
|
410
538
|
// Phase 2b: SCRUB -> non-LLM secret scrubber must PASS before any dispatch
|
|
411
539
|
const scrub = scrubSecrets(ctx.prompt);
|
|
412
540
|
if (scrub.blocked) {
|
|
413
|
-
|
|
541
|
+
const findingNames = scrub.findings.map((f) => f.pattern || f.name || 'secret');
|
|
542
|
+
const looksLikeKeyGive = findingNames.some((n) =>
|
|
543
|
+
/GROQ_API_KEY|ANTHROPIC_API_KEY|OPENAI_API_KEY|OPENAI_PROJECT_KEY|GITHUB_TOKEN|GOOGLE_API_KEY|api.key|access.token|bearer/i.test(n)
|
|
544
|
+
);
|
|
545
|
+
const guidance = looksLikeKeyGive
|
|
546
|
+
? `I blocked that message because it looks like an API key. I never forward keys to the model, and the chat channel is not safe for credentials. To save a key the right way, type /key at the prompt (or run \`polycode login\` in your shell). You will get a masked prompt, and the key lands at ~/.polycode/secrets.env with chmod 600. The model never sees it.`
|
|
547
|
+
: `I blocked that message because it contained a recognized secret pattern (${findingNames.join(', ')}). Nothing was sent to the model. If you were trying to save an API key, type /key instead.`;
|
|
548
|
+
emit({ phase: 'scrub_blocked', findings: scrub.findings, guidance });
|
|
549
|
+
emit({ phase: 'act', kind: 'message', content: guidance });
|
|
414
550
|
const c = await mintCommitment(canon, {
|
|
415
551
|
intentId,
|
|
416
552
|
turnId,
|
|
@@ -427,7 +563,7 @@ export class AgenticLoop {
|
|
|
427
563
|
primitivesList.push(c.primitives);
|
|
428
564
|
emit({ phase: 'record', commitment: c });
|
|
429
565
|
return {
|
|
430
|
-
finalMessage:
|
|
566
|
+
finalMessage: guidance,
|
|
431
567
|
iterations: 0,
|
|
432
568
|
durationMs: Date.now() - start,
|
|
433
569
|
commitments,
|
|
@@ -509,11 +645,17 @@ export class AgenticLoop {
|
|
|
509
645
|
const cleaned = assistantMsg.content
|
|
510
646
|
.replace(/<function=[\w_-]+>?\{[\s\S]*?\}\s*<\/function>/g, '')
|
|
511
647
|
.trim();
|
|
512
|
-
|
|
648
|
+
const identity = checkIdentity(cleaned);
|
|
649
|
+
if (!identity.ok) {
|
|
650
|
+
emit({ phase: 'identity_rewrite', leak: identity.leak, iteration });
|
|
651
|
+
assistantMsg.content = identity.text;
|
|
652
|
+
}
|
|
653
|
+
if (identity.text) emit({ phase: 'act', kind: 'message', content: identity.text, iteration });
|
|
513
654
|
}
|
|
514
655
|
|
|
515
656
|
if (!toolCalls || toolCalls.length === 0) {
|
|
516
|
-
|
|
657
|
+
const leakCheck = checkIdentity(assistantMsg.content ?? '');
|
|
658
|
+
finalMessage = leakCheck.text;
|
|
517
659
|
const c = await mintCommitment(canon, {
|
|
518
660
|
intentId,
|
|
519
661
|
turnId,
|
|
@@ -552,6 +694,13 @@ export class AgenticLoop {
|
|
|
552
694
|
if (summaryScrub.blocked) {
|
|
553
695
|
summary = `(secrets redacted in summary: ${summaryScrub.findings.map((f) => f.pattern).join(', ')}) ${summaryScrub.redacted}`;
|
|
554
696
|
}
|
|
697
|
+
// Identity gate: never let the model claim it was created by
|
|
698
|
+
// Anthropic, OpenAI, etc. through task_done.
|
|
699
|
+
const idCheck = checkIdentity(summary);
|
|
700
|
+
if (!idCheck.ok) {
|
|
701
|
+
emit({ phase: 'identity_rewrite', leak: idCheck.leak, iteration });
|
|
702
|
+
summary = idCheck.text;
|
|
703
|
+
}
|
|
555
704
|
finalMessage = summary;
|
|
556
705
|
const c = await mintCommitment(canon, {
|
|
557
706
|
intentId,
|