@gaberrb/polypus 0.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/dist/index.js ADDED
@@ -0,0 +1,2786 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { Command } from "commander";
5
+ import pc10 from "picocolors";
6
+
7
+ // src/cli/commands/add-agent.ts
8
+ import pc from "picocolors";
9
+
10
+ // src/core/config/schema.ts
11
+ import { z } from "zod";
12
+ var ProviderKind = z.enum([
13
+ "openrouter",
14
+ "ollama",
15
+ "openai-compatible",
16
+ "anthropic"
17
+ ]);
18
+ var ToolMode = z.enum(["auto", "native", "emulated"]);
19
+ var PermissionMode = z.enum(["plan", "review", "bypass"]);
20
+ var AgentConfig = z.object({
21
+ /** Unique, human-friendly identifier used by add-agent/remove-agent and orchestration. */
22
+ name: z.string().min(1),
23
+ provider: ProviderKind,
24
+ /** Override the provider's default base URL (required for `openai-compatible`). */
25
+ baseUrl: z.string().url().optional(),
26
+ model: z.string().min(1),
27
+ /**
28
+ * API key. Prefer an env reference like "${OPENROUTER_API_KEY}" over an inline secret.
29
+ * Optional because local providers (Ollama) do not need one.
30
+ */
31
+ apiKey: z.string().optional(),
32
+ toolMode: ToolMode.default("auto")
33
+ });
34
+ var Permissions = z.object({
35
+ mode: PermissionMode.default("review"),
36
+ /** Glob patterns of paths the agent may read/write, relative to the workspace root. */
37
+ allow: z.array(z.string()).default(["**/*"]),
38
+ /** Glob patterns that are always denied, even if they match `allow`. */
39
+ deny: z.array(z.string()).default([".git/**", ".polypus/**", "**/.env"]),
40
+ /** Shell commands (matched by prefix) the agent may run without per-call escalation. */
41
+ allowedCommands: z.array(z.string()).default([])
42
+ });
43
+ var Locale = z.enum(["pt-BR", "en"]);
44
+ var PolypusConfig = z.object({
45
+ version: z.literal(1).default(1),
46
+ /** Interface language. Defaults to pt-BR. */
47
+ locale: Locale.default("pt-BR"),
48
+ defaultAgent: z.string().optional(),
49
+ agents: z.array(AgentConfig).default([]),
50
+ permissions: Permissions.default({})
51
+ });
52
+ var DEFAULT_CONFIG = PolypusConfig.parse({});
53
+
54
+ // src/core/config/store.ts
55
+ import { homedir } from "os";
56
+ import { join } from "path";
57
+ import { mkdir, readFile, writeFile } from "fs/promises";
58
+ import { existsSync } from "fs";
59
+
60
+ // src/core/i18n/index.ts
61
+ var LOCALES = ["pt-BR", "en"];
62
+ var DEFAULT_LOCALE = "pt-BR";
63
+ var LOCALE_NAMES = {
64
+ "pt-BR": "Portugu\xEAs (Brasil)",
65
+ en: "English"
66
+ };
67
+ var en = {
68
+ // generic
69
+ "common.default": "default",
70
+ "common.keySet": "key set",
71
+ "common.noKey": "no key",
72
+ // cli descriptions
73
+ "cli.description": "Agentic coding harness that makes any AI API generate and apply code \u2014 OpenRouter, Ollama, and any OpenAI-compatible endpoint.",
74
+ "cli.opt.lang": "interface language: pt-BR | en",
75
+ "cli.cmd.setup": "Interactive setup wizard (configure agents, keys, permissions)",
76
+ "cli.cmd.addAgent": "Register a new agent (API key + model)",
77
+ "cli.cmd.removeAgent": "Remove a configured agent",
78
+ "cli.cmd.listAgents": "List configured agents",
79
+ "cli.cmd.run": "Run a coding task with an agent",
80
+ "cli.cmd.swarm": "Split a task across multiple agents working in parallel git worktrees",
81
+ "cli.cmd.models": "Browse OpenRouter models (price, context, tool support)",
82
+ "cli.opt.search": "filter by id/name substring",
83
+ "cli.opt.toolsOnly": "only models that support tool-calling",
84
+ "cli.opt.free": "only free models",
85
+ "cli.opt.maxPrice": "max prompt price (USD per 1M tokens)",
86
+ "cli.opt.sort": "price | price-desc | context | name",
87
+ "cli.opt.limit": "max rows to show",
88
+ "cli.arg.addAgentName": "unique name for the agent",
89
+ "cli.opt.provider": "openrouter | ollama | openai-compatible | anthropic",
90
+ "cli.opt.model": "model id, e.g. anthropic/claude-3.5-sonnet or llama3.1",
91
+ "cli.opt.apiKey": 'API key or env reference like "${OPENROUTER_API_KEY}"',
92
+ "cli.opt.baseUrl": "override the provider base URL",
93
+ "cli.opt.toolMode": "auto | native | emulated",
94
+ "cli.opt.setDefault": "make this the default agent",
95
+ "cli.arg.removeAgentName": "name of the agent to remove",
96
+ "cli.arg.runTask": "task for the agent; omit to start an interactive session",
97
+ "cli.opt.agent": "which configured agent to use",
98
+ "cli.opt.mode": "plan | review | bypass (overrides config)",
99
+ "cli.opt.maxSteps": "maximum agent steps",
100
+ "cli.arg.swarmTask": "high-level task to split across agents",
101
+ "cli.opt.agents": "comma-separated agent names (default: all configured)",
102
+ "cli.opt.maxSubtasks": "maximum number of parallel subtasks",
103
+ // agents
104
+ "agent.exists": 'An agent named "{name}" already exists. Use remove-agent first to replace it.',
105
+ "agent.needBaseUrl": 'Provider "{provider}" requires --base-url.',
106
+ "agent.needApiKey": 'Provider "{provider}" requires an API key. Pass --api-key "${ENV_VAR}" (recommended) or a literal value.',
107
+ "agent.added": "\u2713 Added agent {name}",
108
+ "agent.removed": "\u2713 Removed agent {name}",
109
+ "agent.notFound": 'No agent named "{name}".',
110
+ "agent.none": "No agents configured. Run `polypus setup` or `polypus add-agent`.",
111
+ "agent.listHeader": "Agents:",
112
+ "agent.permLine": "Permissions: mode={mode}, allow=[{allow}]",
113
+ "agent.noneKnown": 'No agent named "{name}". Known agents: {names}',
114
+ "agent.needAnthropicKey": 'Agent "{name}" (anthropic) requires an API key.',
115
+ "agent.noBaseUrl": 'Agent "{name}" has no base URL configured.',
116
+ "agent.noneConfigured": "No agents configured. Run `polypus setup` or `polypus add-agent` first.",
117
+ "agent.multipleNoDefault": "Multiple agents configured but no default set. Pass --agent <name> or set a default. Agents: {names}",
118
+ // run / loop
119
+ "run.status": "agent={name} provider={provider} model={model} tool-mode={toolMode} permission-mode={mode}",
120
+ "run.done": "\u2713 Done ({steps} steps).",
121
+ "run.stopped": "\u26A0 Stopped after {steps} steps without a finish signal. You can continue with another instruction.",
122
+ "run.confirm": "Allow {summary}?",
123
+ "run.reprompt": "\u21BB no tool call \u2014 reinforcing instructions (attempt {attempt})",
124
+ "run.cancelled": "\u25A0 cancelled",
125
+ // repl
126
+ "repl.welcome": "Polypus interactive session.",
127
+ "repl.welcomeHint": " Type /help for commands, /exit to quit.",
128
+ "repl.modeChanged": "mode \u2192 {mode}",
129
+ "repl.allowAdded": "allow-list += {glob}",
130
+ "repl.allowShow": "mode={mode} allow=[{allow}]",
131
+ "repl.historyCleared": "history cleared",
132
+ "repl.unknown": "Unknown command /{cmd}. Type /help.",
133
+ "repl.agentSwitched": "active agent \u2192 {name}",
134
+ "repl.switchedTo": "active agent is now {name}",
135
+ "repl.noAgentsLeft": "No agents left. Use /add to create one.",
136
+ "repl.needName": "Usage: {usage}",
137
+ "repl.help": [
138
+ "Slash commands:",
139
+ " /agents list configured agents",
140
+ " /agent <name> switch the active agent",
141
+ " /add add a new agent (wizard)",
142
+ " /remove <name> remove an agent",
143
+ " /plan switch to plan mode (read-only)",
144
+ " /review switch to review mode (confirm each action)",
145
+ " /bypass switch to bypass mode (auto-approve)",
146
+ " /allow <glob> add a path glob to the allow-list",
147
+ " /allow show the current allow-list and mode",
148
+ " /reset clear the conversation history",
149
+ " /help show this help",
150
+ " /exit quit",
151
+ "Anything else is sent to the agent as a task."
152
+ ].join("\n"),
153
+ // swarm
154
+ "swarm.noAgents": "No agents configured. Run `polypus setup` or `polypus add-agent` first.",
155
+ "swarm.status": "swarm agents=[{agents}] workspace={workspace}",
156
+ "swarm.bypassNote": "Workers run in bypass mode inside isolated git worktrees; branches are merged at the end.",
157
+ "swarm.decomposed": "Decomposed into {n} subtask(s):",
158
+ "swarm.workerStart": "\u25B6 {id} started by {agent}",
159
+ "swarm.workerDone": "\u2713 {id} done",
160
+ "swarm.workerStopped": "\u2026 {id} stopped",
161
+ "swarm.workerMeta": " ({steps} steps, {changes})",
162
+ "swarm.changesCommitted": "changes committed",
163
+ "swarm.noChanges": "no changes",
164
+ "swarm.merged": " merged {branch}",
165
+ "swarm.mergeConflict": " conflict merging {branch}",
166
+ "swarm.summary": "Summary:",
167
+ "swarm.allMerged": "\u2713 All committed branches merged cleanly.",
168
+ "swarm.conflictsHeader": "\u26A0 {n} branch(es) had merge conflicts (kept for inspection):",
169
+ "swarm.statusDone": "done",
170
+ "swarm.statusIncomplete": "incomplete",
171
+ // wizard
172
+ "wizard.title": " polypus setup ",
173
+ "wizard.intro": [
174
+ "Polypus drives any AI API to read and write code in this kind of project.",
175
+ "You can add several agents (different keys/models) and they can work in parallel.",
176
+ "Tip: reference API keys via environment variables instead of pasting them here."
177
+ ].join("\n"),
178
+ "wizard.welcome": "Welcome",
179
+ "wizard.cancelled": "Setup cancelled.",
180
+ "wizard.language": "Interface language",
181
+ "wizard.addAnother": "Add another agent?",
182
+ "wizard.defaultAgent": "Default agent",
183
+ "wizard.permMode": "Default permission mode",
184
+ "wizard.permReview": "review \u2014 confirm each file write / command (safe default)",
185
+ "wizard.permPlan": "plan \u2014 read-only, propose changes",
186
+ "wizard.permBypass": "bypass \u2014 auto-approve everything (use with care)",
187
+ "wizard.allowPaths": "Editable paths (comma-separated globs)",
188
+ "wizard.saved": "Saved {n} agent(s) to {path}",
189
+ "wizard.next": 'Run `polypus run "your task"` to start, or `polypus run` for an interactive session.',
190
+ "wizard.provider": "Provider",
191
+ "wizard.providerOpenrouter": "OpenRouter (hosted, many models)",
192
+ "wizard.providerOllama": "Ollama (local models)",
193
+ "wizard.providerCompatible": "OpenAI-compatible (custom base URL)",
194
+ "wizard.providerAnthropic": "Anthropic (Claude)",
195
+ "wizard.agentName": "Agent name",
196
+ "wizard.required": "Required",
197
+ "wizard.nameTaken": "An agent with this name already exists",
198
+ "wizard.modelId": "Model id",
199
+ "wizard.ollamaDetecting": "Detecting models on your local Ollama\u2026",
200
+ "wizard.ollamaFound": "Found {n} local Ollama model(s)",
201
+ "wizard.ollamaNone": "Ollama not reachable \u2014 type the model id manually",
202
+ "wizard.ollamaPick": "Model (detected on your Ollama)",
203
+ "wizard.ollamaOther": "Other (type it manually)",
204
+ "wizard.orError": "Could not reach OpenRouter \u2014 type the model id manually",
205
+ "wizard.orSearch": "Search by id/name (optional)",
206
+ "wizard.orFilters": "Filters (space to toggle, enter to confirm)",
207
+ "wizard.orToolsOnly": "Only models with native tools",
208
+ "wizard.orFreeOnly": "Only free models",
209
+ "wizard.orSort": "Sort by",
210
+ "wizard.orSortPrice": "price \u2014 cheapest first",
211
+ "wizard.orSortPriceDesc": "price \u2014 most expensive first",
212
+ "wizard.orSortContext": "context length",
213
+ "wizard.orSortName": "name",
214
+ "wizard.orPick": "Pick a model ({n} matches)",
215
+ "wizard.orRefilter": "\u21BB change filters",
216
+ "wizard.orManual": "\u270E type the model id manually",
217
+ "wizard.orNone": "No models match \u2014 adjust the filters",
218
+ "wizard.baseUrl": "Base URL",
219
+ "wizard.baseUrlRequired": "Required for openai-compatible",
220
+ "wizard.toolMode": "Tool-calling mode",
221
+ "wizard.toolAuto": "auto \u2014 native for hosted, emulated for local (recommended)",
222
+ "wizard.toolNative": "native \u2014 provider function-calling",
223
+ "wizard.toolEmulated": "emulated \u2014 XML tool protocol in the prompt (works without tool support)",
224
+ "wizard.keyNotNeeded": "{provider} usually needs no API key. Add one anyway?",
225
+ "wizard.apiKey": "API key",
226
+ "wizard.keyEnv": "Reference an environment variable (recommended)",
227
+ "wizard.keyInline": "Enter it now (stored in the config file)",
228
+ "wizard.keySkip": "Skip for now",
229
+ "wizard.envName": "Environment variable name",
230
+ "wizard.envInvalid": "Use letters, digits, underscores",
231
+ "wizard.keyPrompt": "API key (stored in plain text in the config file)",
232
+ // models browser
233
+ "models.fetching": "Fetching OpenRouter models\u2026",
234
+ "models.fetchError": "Could not fetch models: {msg}",
235
+ "models.none": "No models match the filters.",
236
+ "models.shown": "Showing {shown} of {total} models",
237
+ "models.legend": "\u{1F6E0} = native tools \xB7 prices = USD per 1M tokens (in/out)",
238
+ "models.colTools": "TOOLS",
239
+ "models.colPrice": "PRICE in/out",
240
+ "models.colCtx": "CONTEXT",
241
+ "models.colModel": "MODEL",
242
+ // live status
243
+ "ui.thinking": "thinking",
244
+ "ui.running": "running {tool}",
245
+ "ui.tokens": "{total} tokens (in {in} / out {out})",
246
+ "ui.tokensShort": "{total} tok",
247
+ // welcome / interactive UI
248
+ "welcome.tagline": "agentic harness \u2014 make any AI write code",
249
+ "welcome.agent": "agent",
250
+ "welcome.model": "model",
251
+ "welcome.mode": "mode",
252
+ "welcome.workspace": "folder",
253
+ "welcome.hints": "Type your task and press Enter \xB7 ESC cancels \xB7 /help \xB7 /exit",
254
+ "welcome.firstRun": "No agents configured yet \u2014 let's set you up.",
255
+ // agent system prompt
256
+ "prompt.language": "Communicate with the user in {language}."
257
+ };
258
+ var ptBR = {
259
+ "common.default": "padr\xE3o",
260
+ "common.keySet": "com chave",
261
+ "common.noKey": "sem chave",
262
+ "cli.description": "Harness ag\xEAntico que faz qualquer API de IA gerar e aplicar c\xF3digo \u2014 OpenRouter, Ollama e qualquer endpoint compat\xEDvel com OpenAI.",
263
+ "cli.opt.lang": "idioma da interface: pt-BR | en",
264
+ "cli.cmd.setup": "Assistente de configura\xE7\xE3o interativo (agentes, chaves, permiss\xF5es)",
265
+ "cli.cmd.addAgent": "Cadastra um novo agente (chave de API + modelo)",
266
+ "cli.cmd.removeAgent": "Remove um agente configurado",
267
+ "cli.cmd.listAgents": "Lista os agentes configurados",
268
+ "cli.cmd.run": "Executa uma tarefa de c\xF3digo com um agente",
269
+ "cli.cmd.swarm": "Divide uma tarefa entre v\xE1rios agentes trabalhando em paralelo em git worktrees",
270
+ "cli.cmd.models": "Explora os modelos do OpenRouter (pre\xE7o, contexto, suporte a tools)",
271
+ "cli.opt.search": "filtra por trecho do id/nome",
272
+ "cli.opt.toolsOnly": "apenas modelos com suporte a tool-calling",
273
+ "cli.opt.free": "apenas modelos gratuitos",
274
+ "cli.opt.maxPrice": "pre\xE7o m\xE1ximo de entrada (USD por 1M tokens)",
275
+ "cli.opt.sort": "price | price-desc | context | name",
276
+ "cli.opt.limit": "m\xE1ximo de linhas a exibir",
277
+ "cli.arg.addAgentName": "nome \xFAnico para o agente",
278
+ "cli.opt.provider": "openrouter | ollama | openai-compatible | anthropic",
279
+ "cli.opt.model": "id do modelo, ex.: anthropic/claude-3.5-sonnet ou llama3.1",
280
+ "cli.opt.apiKey": 'chave de API ou refer\xEAncia de env como "${OPENROUTER_API_KEY}"',
281
+ "cli.opt.baseUrl": "sobrescreve a URL base do provider",
282
+ "cli.opt.toolMode": "auto | native | emulated",
283
+ "cli.opt.setDefault": "define como agente padr\xE3o",
284
+ "cli.arg.removeAgentName": "nome do agente a remover",
285
+ "cli.arg.runTask": "tarefa para o agente; omita para iniciar uma sess\xE3o interativa",
286
+ "cli.opt.agent": "qual agente configurado usar",
287
+ "cli.opt.mode": "plan | review | bypass (sobrescreve a config)",
288
+ "cli.opt.maxSteps": "n\xFAmero m\xE1ximo de passos do agente",
289
+ "cli.arg.swarmTask": "tarefa de alto n\xEDvel para dividir entre os agentes",
290
+ "cli.opt.agents": "nomes de agentes separados por v\xEDrgula (padr\xE3o: todos)",
291
+ "cli.opt.maxSubtasks": "n\xFAmero m\xE1ximo de subtarefas paralelas",
292
+ "agent.exists": 'J\xE1 existe um agente chamado "{name}". Use remove-agent antes para substitu\xED-lo.',
293
+ "agent.needBaseUrl": 'O provider "{provider}" exige --base-url.',
294
+ "agent.needApiKey": 'O provider "{provider}" exige uma chave de API. Passe --api-key "${ENV_VAR}" (recomendado) ou um valor literal.',
295
+ "agent.added": "\u2713 Agente {name} adicionado",
296
+ "agent.removed": "\u2713 Agente {name} removido",
297
+ "agent.notFound": 'N\xE3o existe agente chamado "{name}".',
298
+ "agent.none": "Nenhum agente configurado. Rode `polypus setup` ou `polypus add-agent`.",
299
+ "agent.listHeader": "Agentes:",
300
+ "agent.permLine": "Permiss\xF5es: modo={mode}, allow=[{allow}]",
301
+ "agent.noneKnown": 'N\xE3o existe agente chamado "{name}". Agentes conhecidos: {names}',
302
+ "agent.needAnthropicKey": 'O agente "{name}" (anthropic) exige uma chave de API.',
303
+ "agent.noBaseUrl": 'O agente "{name}" n\xE3o tem URL base configurada.',
304
+ "agent.noneConfigured": "Nenhum agente configurado. Rode `polypus setup` ou `polypus add-agent` primeiro.",
305
+ "agent.multipleNoDefault": "V\xE1rios agentes configurados mas sem padr\xE3o definido. Passe --agent <nome> ou defina um padr\xE3o. Agentes: {names}",
306
+ "run.status": "agente={name} provider={provider} modelo={model} tool-mode={toolMode} modo-permiss\xE3o={mode}",
307
+ "run.done": "\u2713 Conclu\xEDdo ({steps} passos).",
308
+ "run.stopped": "\u26A0 Parou ap\xF3s {steps} passos sem sinal de conclus\xE3o. Voc\xEA pode continuar com outra instru\xE7\xE3o.",
309
+ "run.confirm": "Permitir {summary}?",
310
+ "run.reprompt": "\u21BB nenhuma chamada de tool \u2014 refor\xE7ando instru\xE7\xF5es (tentativa {attempt})",
311
+ "run.cancelled": "\u25A0 cancelado",
312
+ "repl.welcome": "Sess\xE3o interativa do Polypus.",
313
+ "repl.welcomeHint": " Digite /help para comandos, /exit para sair.",
314
+ "repl.modeChanged": "modo \u2192 {mode}",
315
+ "repl.allowAdded": "allow-list += {glob}",
316
+ "repl.allowShow": "modo={mode} allow=[{allow}]",
317
+ "repl.historyCleared": "hist\xF3rico limpo",
318
+ "repl.unknown": "Comando desconhecido /{cmd}. Digite /help.",
319
+ "repl.agentSwitched": "agente ativo \u2192 {name}",
320
+ "repl.switchedTo": "agente ativo agora \xE9 {name}",
321
+ "repl.noAgentsLeft": "Nenhum agente restante. Use /add para criar um.",
322
+ "repl.needName": "Uso: {usage}",
323
+ "repl.help": [
324
+ "Comandos de barra:",
325
+ " /agents lista os agentes configurados",
326
+ " /agent <nome> troca o agente ativo",
327
+ " /add adiciona um novo agente (wizard)",
328
+ " /remove <nome> remove um agente",
329
+ " /plan muda para o modo plan (somente leitura)",
330
+ " /review muda para o modo review (confirma cada a\xE7\xE3o)",
331
+ " /bypass muda para o modo bypass (aprova automaticamente)",
332
+ " /allow <glob> adiciona um glob de caminho \xE0 allow-list",
333
+ " /allow mostra a allow-list e o modo atuais",
334
+ " /reset limpa o hist\xF3rico da conversa",
335
+ " /help mostra esta ajuda",
336
+ " /exit sair",
337
+ "Qualquer outra coisa \xE9 enviada ao agente como tarefa."
338
+ ].join("\n"),
339
+ "swarm.noAgents": "Nenhum agente configurado. Rode `polypus setup` ou `polypus add-agent` primeiro.",
340
+ "swarm.status": "swarm agentes=[{agents}] workspace={workspace}",
341
+ "swarm.bypassNote": "Os workers rodam em modo bypass dentro de git worktrees isoladas; os branches s\xE3o mesclados no final.",
342
+ "swarm.decomposed": "Dividido em {n} subtarefa(s):",
343
+ "swarm.workerStart": "\u25B6 {id} iniciada por {agent}",
344
+ "swarm.workerDone": "\u2713 {id} conclu\xEDda",
345
+ "swarm.workerStopped": "\u2026 {id} parou",
346
+ "swarm.workerMeta": " ({steps} passos, {changes})",
347
+ "swarm.changesCommitted": "altera\xE7\xF5es commitadas",
348
+ "swarm.noChanges": "sem altera\xE7\xF5es",
349
+ "swarm.merged": " mesclado {branch}",
350
+ "swarm.mergeConflict": " conflito ao mesclar {branch}",
351
+ "swarm.summary": "Resumo:",
352
+ "swarm.allMerged": "\u2713 Todos os branches commitados foram mesclados sem conflito.",
353
+ "swarm.conflictsHeader": "\u26A0 {n} branch(es) tiveram conflitos de merge (mantidos para inspe\xE7\xE3o):",
354
+ "swarm.statusDone": "ok",
355
+ "swarm.statusIncomplete": "incompleta",
356
+ "wizard.title": " configura\xE7\xE3o do polypus ",
357
+ "wizard.intro": [
358
+ "O Polypus comanda qualquer API de IA para ler e escrever c\xF3digo neste tipo de projeto.",
359
+ "Voc\xEA pode adicionar v\xE1rios agentes (chaves/modelos diferentes) e eles trabalham em paralelo.",
360
+ "Dica: referencie chaves de API por vari\xE1veis de ambiente em vez de col\xE1-las aqui."
361
+ ].join("\n"),
362
+ "wizard.welcome": "Bem-vindo",
363
+ "wizard.cancelled": "Configura\xE7\xE3o cancelada.",
364
+ "wizard.language": "Idioma da interface",
365
+ "wizard.addAnother": "Adicionar outro agente?",
366
+ "wizard.defaultAgent": "Agente padr\xE3o",
367
+ "wizard.permMode": "Modo de permiss\xE3o padr\xE3o",
368
+ "wizard.permReview": "review \u2014 confirma cada escrita/comando (padr\xE3o seguro)",
369
+ "wizard.permPlan": "plan \u2014 somente leitura, prop\xF5e mudan\xE7as",
370
+ "wizard.permBypass": "bypass \u2014 aprova tudo automaticamente (use com cuidado)",
371
+ "wizard.allowPaths": "Caminhos edit\xE1veis (globs separados por v\xEDrgula)",
372
+ "wizard.saved": "{n} agente(s) salvos em {path}",
373
+ "wizard.next": 'Rode `polypus run "sua tarefa"` para come\xE7ar, ou `polypus run` para uma sess\xE3o interativa.',
374
+ "wizard.provider": "Provider",
375
+ "wizard.providerOpenrouter": "OpenRouter (hospedado, muitos modelos)",
376
+ "wizard.providerOllama": "Ollama (modelos locais)",
377
+ "wizard.providerCompatible": "Compat\xEDvel com OpenAI (URL base customizada)",
378
+ "wizard.providerAnthropic": "Anthropic (Claude)",
379
+ "wizard.agentName": "Nome do agente",
380
+ "wizard.required": "Obrigat\xF3rio",
381
+ "wizard.nameTaken": "J\xE1 existe um agente com esse nome",
382
+ "wizard.modelId": "Id do modelo",
383
+ "wizard.ollamaDetecting": "Detectando modelos no seu Ollama local\u2026",
384
+ "wizard.ollamaFound": "{n} modelo(s) do Ollama encontrado(s)",
385
+ "wizard.ollamaNone": "Ollama n\xE3o acess\xEDvel \u2014 digite o id do modelo manualmente",
386
+ "wizard.ollamaPick": "Modelo (detectado no seu Ollama)",
387
+ "wizard.ollamaOther": "Outro (digitar manualmente)",
388
+ "wizard.orError": "N\xE3o foi poss\xEDvel acessar o OpenRouter \u2014 digite o id do modelo manualmente",
389
+ "wizard.orSearch": "Buscar por id/nome (opcional)",
390
+ "wizard.orFilters": "Filtros (espa\xE7o alterna, enter confirma)",
391
+ "wizard.orToolsOnly": "Apenas modelos com tools nativas",
392
+ "wizard.orFreeOnly": "Apenas modelos gratuitos",
393
+ "wizard.orSort": "Ordenar por",
394
+ "wizard.orSortPrice": "pre\xE7o \u2014 mais baratos primeiro",
395
+ "wizard.orSortPriceDesc": "pre\xE7o \u2014 mais caros primeiro",
396
+ "wizard.orSortContext": "tamanho do contexto",
397
+ "wizard.orSortName": "nome",
398
+ "wizard.orPick": "Escolha um modelo ({n} resultados)",
399
+ "wizard.orRefilter": "\u21BB mudar filtros",
400
+ "wizard.orManual": "\u270E digitar o id do modelo manualmente",
401
+ "wizard.orNone": "Nenhum modelo corresponde \u2014 ajuste os filtros",
402
+ "wizard.baseUrl": "URL base",
403
+ "wizard.baseUrlRequired": "Obrigat\xF3rio para openai-compatible",
404
+ "wizard.toolMode": "Modo de tool-calling",
405
+ "wizard.toolAuto": "auto \u2014 nativo para hospedados, emulado para locais (recomendado)",
406
+ "wizard.toolNative": "native \u2014 function-calling do provider",
407
+ "wizard.toolEmulated": "emulated \u2014 protocolo XML de tools no prompt (funciona sem suporte a tools)",
408
+ "wizard.keyNotNeeded": "{provider} normalmente n\xE3o precisa de chave. Adicionar mesmo assim?",
409
+ "wizard.apiKey": "Chave de API",
410
+ "wizard.keyEnv": "Referenciar uma vari\xE1vel de ambiente (recomendado)",
411
+ "wizard.keyInline": "Digitar agora (armazenada no arquivo de config)",
412
+ "wizard.keySkip": "Pular por enquanto",
413
+ "wizard.envName": "Nome da vari\xE1vel de ambiente",
414
+ "wizard.envInvalid": "Use letras, d\xEDgitos e sublinhados",
415
+ "wizard.keyPrompt": "Chave de API (armazenada em texto puro no arquivo de config)",
416
+ "prompt.language": "Comunique-se com o usu\xE1rio em {language}.",
417
+ "models.fetching": "Buscando modelos do OpenRouter\u2026",
418
+ "models.fetchError": "N\xE3o foi poss\xEDvel buscar modelos: {msg}",
419
+ "models.none": "Nenhum modelo corresponde aos filtros.",
420
+ "models.shown": "Mostrando {shown} de {total} modelos",
421
+ "models.legend": "\u{1F6E0} = tools nativas \xB7 pre\xE7os = USD por 1M tokens (entrada/sa\xEDda)",
422
+ "models.colTools": "TOOLS",
423
+ "models.colPrice": "PRE\xC7O ent/sa\xED",
424
+ "models.colCtx": "CONTEXTO",
425
+ "models.colModel": "MODELO",
426
+ "ui.thinking": "pensando",
427
+ "ui.running": "executando {tool}",
428
+ "ui.tokens": "{total} tokens (entrada {in} / sa\xEDda {out})",
429
+ "ui.tokensShort": "{total} tok",
430
+ "welcome.tagline": "harness ag\xEAntico \u2014 fa\xE7a qualquer IA escrever c\xF3digo",
431
+ "welcome.agent": "agente",
432
+ "welcome.model": "modelo",
433
+ "welcome.mode": "modo",
434
+ "welcome.workspace": "pasta",
435
+ "welcome.hints": "Digite sua tarefa e tecle Enter \xB7 ESC cancela \xB7 /help \xB7 /exit",
436
+ "welcome.firstRun": "Nenhum agente configurado ainda \u2014 vamos te configurar."
437
+ };
438
+ var CATALOGS = { en, "pt-BR": ptBR };
439
+ var currentLocale = DEFAULT_LOCALE;
440
+ function setLocale(locale) {
441
+ currentLocale = locale;
442
+ }
443
+ function getLocale() {
444
+ return currentLocale;
445
+ }
446
+ function isLocale(value) {
447
+ return typeof value === "string" && LOCALES.includes(value);
448
+ }
449
+ function pickLocale(opts) {
450
+ const candidates = [opts.flag, process.env.POLYPUS_LANG, opts.config];
451
+ for (const c of candidates) {
452
+ if (isLocale(c)) return c;
453
+ }
454
+ return DEFAULT_LOCALE;
455
+ }
456
+ function t(key, params) {
457
+ const template = CATALOGS[currentLocale][key] ?? en[key] ?? key;
458
+ if (!params) return template;
459
+ return template.replace(
460
+ /\{(\w+)\}/g,
461
+ (_, name) => name in params ? String(params[name]) : `{${name}}`
462
+ );
463
+ }
464
+
465
+ // src/core/config/store.ts
466
+ function configDir() {
467
+ return process.env.POLYPUS_HOME ?? join(homedir(), ".polypus");
468
+ }
469
+ function configPath() {
470
+ return join(configDir(), "config.json");
471
+ }
472
+ async function loadConfig() {
473
+ const path = configPath();
474
+ if (!existsSync(path)) return structuredClone(DEFAULT_CONFIG);
475
+ let raw;
476
+ try {
477
+ raw = JSON.parse(await readFile(path, "utf8"));
478
+ } catch (err) {
479
+ throw new Error(
480
+ `Failed to parse config at ${path}: ${err.message}`
481
+ );
482
+ }
483
+ const parsed = PolypusConfig.safeParse(raw);
484
+ if (!parsed.success) {
485
+ throw new Error(
486
+ `Invalid config at ${path}:
487
+ ${parsed.error.issues.map((i) => ` - ${i.path.join(".") || "<root>"}: ${i.message}`).join("\n")}`
488
+ );
489
+ }
490
+ return parsed.data;
491
+ }
492
+ async function saveConfig(config) {
493
+ await mkdir(configDir(), { recursive: true });
494
+ const validated = PolypusConfig.parse(config);
495
+ await writeFile(configPath(), JSON.stringify(validated, null, 2) + "\n", "utf8");
496
+ }
497
+ function findAgent(config, name) {
498
+ return config.agents.find((a) => a.name === name);
499
+ }
500
+ function resolveAgent(config, name) {
501
+ if (name) {
502
+ const agent = findAgent(config, name);
503
+ if (!agent) {
504
+ throw new Error(
505
+ t("agent.noneKnown", {
506
+ name,
507
+ names: config.agents.map((a) => a.name).join(", ") || "(none)"
508
+ })
509
+ );
510
+ }
511
+ return agent;
512
+ }
513
+ if (config.defaultAgent) {
514
+ const agent = findAgent(config, config.defaultAgent);
515
+ if (agent) return agent;
516
+ }
517
+ if (config.agents.length === 1) return config.agents[0];
518
+ if (config.agents.length === 0) {
519
+ throw new Error(t("agent.noneConfigured"));
520
+ }
521
+ throw new Error(
522
+ t("agent.multipleNoDefault", { names: config.agents.map((a) => a.name).join(", ") })
523
+ );
524
+ }
525
+ function resolveSecret(value) {
526
+ if (!value) return void 0;
527
+ const match = /^\$\{([A-Z0-9_]+)\}$/i.exec(value.trim());
528
+ if (match) {
529
+ const env = process.env[match[1]];
530
+ if (!env) {
531
+ throw new Error(
532
+ `Config references env var ${match[1]} but it is not set in the environment.`
533
+ );
534
+ }
535
+ return env;
536
+ }
537
+ return value;
538
+ }
539
+
540
+ // src/core/providers/defaults.ts
541
+ var DEFAULT_BASE_URL = {
542
+ openrouter: "https://openrouter.ai/api/v1",
543
+ ollama: "http://localhost:11434/v1",
544
+ "openai-compatible": void 0,
545
+ anthropic: "https://api.anthropic.com"
546
+ };
547
+ var REQUIRES_API_KEY = {
548
+ openrouter: true,
549
+ ollama: false,
550
+ "openai-compatible": false,
551
+ anthropic: true
552
+ };
553
+ var SUGGESTED_KEY_ENV = {
554
+ openrouter: "OPENROUTER_API_KEY",
555
+ ollama: void 0,
556
+ "openai-compatible": "OPENAI_API_KEY",
557
+ anthropic: "ANTHROPIC_API_KEY"
558
+ };
559
+
560
+ // src/cli/commands/add-agent.ts
561
+ async function addAgent(name, opts) {
562
+ const config = await loadConfig();
563
+ if (findAgent(config, name)) {
564
+ throw new Error(t("agent.exists", { name }));
565
+ }
566
+ const provider = ProviderKind.parse(opts.provider);
567
+ const baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL[provider];
568
+ if (!baseUrl) {
569
+ throw new Error(t("agent.needBaseUrl", { provider }));
570
+ }
571
+ if (REQUIRES_API_KEY[provider] && !opts.apiKey) {
572
+ throw new Error(t("agent.needApiKey", { provider }));
573
+ }
574
+ const agent = AgentConfig.parse({
575
+ name,
576
+ provider,
577
+ model: opts.model,
578
+ apiKey: opts.apiKey,
579
+ baseUrl,
580
+ toolMode: ToolMode.parse(opts.toolMode ?? "auto")
581
+ });
582
+ config.agents.push(agent);
583
+ if (opts.setDefault || config.agents.length === 1) {
584
+ config.defaultAgent = name;
585
+ }
586
+ await saveConfig(config);
587
+ console.log(
588
+ pc.green(t("agent.added", { name: pc.bold(name) })) + ` (${provider} \xB7 ${opts.model})` + (config.defaultAgent === name ? pc.dim(` [${t("common.default")}]`) : "")
589
+ );
590
+ }
591
+
592
+ // src/cli/commands/remove-agent.ts
593
+ import pc2 from "picocolors";
594
+ async function removeAgent(name) {
595
+ const config = await loadConfig();
596
+ if (!findAgent(config, name)) {
597
+ throw new Error(t("agent.notFound", { name }));
598
+ }
599
+ config.agents = config.agents.filter((a) => a.name !== name);
600
+ if (config.defaultAgent === name) {
601
+ config.defaultAgent = config.agents[0]?.name;
602
+ }
603
+ await saveConfig(config);
604
+ console.log(pc2.green(t("agent.removed", { name: pc2.bold(name) })));
605
+ }
606
+
607
+ // src/cli/commands/list-agents.ts
608
+ import pc3 from "picocolors";
609
+ async function listAgents() {
610
+ const config = await loadConfig();
611
+ if (config.agents.length === 0) {
612
+ console.log(pc3.yellow(t("agent.none")));
613
+ return;
614
+ }
615
+ console.log(pc3.bold(t("agent.listHeader")));
616
+ for (const a of config.agents) {
617
+ const isDefault = config.defaultAgent === a.name;
618
+ const key = pc3.dim(` \xB7 ${a.apiKey ? t("common.keySet") : t("common.noKey")}`);
619
+ console.log(
620
+ ` ${isDefault ? pc3.green("\u25CF") : pc3.dim("\u25CB")} ${pc3.bold(a.name)} ` + pc3.dim(`(${a.provider} \xB7 ${a.model} \xB7 ${a.toolMode})`) + key + (isDefault ? pc3.green(` [${t("common.default")}]`) : "")
621
+ );
622
+ }
623
+ console.log(
624
+ pc3.dim(
625
+ "\n" + t("agent.permLine", { mode: config.permissions.mode, allow: config.permissions.allow.join(", ") })
626
+ )
627
+ );
628
+ }
629
+
630
+ // src/cli/commands/run.ts
631
+ import pc7 from "picocolors";
632
+ import * as p2 from "@clack/prompts";
633
+
634
+ // src/core/providers/anthropic.ts
635
+ var AnthropicProvider = class {
636
+ name;
637
+ model;
638
+ baseURL;
639
+ apiKey;
640
+ timeoutMs;
641
+ constructor(opts) {
642
+ this.name = opts.name;
643
+ this.model = opts.model;
644
+ this.baseURL = opts.baseURL.replace(/\/$/, "");
645
+ this.apiKey = opts.apiKey;
646
+ this.timeoutMs = opts.timeoutMs ?? 12e4;
647
+ }
648
+ async chat(req) {
649
+ const system = req.messages.filter((m) => m.role === "system").map((m) => m.content).join("\n\n");
650
+ const messages = groupMessages(req.messages.filter((m) => m.role !== "system"));
651
+ const body = {
652
+ model: this.model,
653
+ max_tokens: req.params?.maxTokens ?? 8192,
654
+ temperature: req.params?.temperature,
655
+ ...system ? { system } : {},
656
+ messages,
657
+ ...req.tools && req.tools.length > 0 ? {
658
+ tools: req.tools.map((t2) => ({
659
+ name: t2.name,
660
+ description: t2.description,
661
+ input_schema: t2.parameters
662
+ }))
663
+ } : {}
664
+ };
665
+ const controller = new AbortController();
666
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
667
+ if (req.signal) {
668
+ if (req.signal.aborted) controller.abort();
669
+ else req.signal.addEventListener("abort", () => controller.abort(), { once: true });
670
+ }
671
+ let res;
672
+ try {
673
+ res = await fetch(`${this.baseURL}/v1/messages`, {
674
+ method: "POST",
675
+ headers: {
676
+ "content-type": "application/json",
677
+ "x-api-key": this.apiKey,
678
+ "anthropic-version": "2023-06-01"
679
+ },
680
+ body: JSON.stringify(body),
681
+ signal: controller.signal
682
+ });
683
+ } finally {
684
+ clearTimeout(timer);
685
+ }
686
+ if (!res.ok) {
687
+ const text3 = await res.text().catch(() => "");
688
+ throw new Error(`Anthropic API ${res.status}: ${text3.slice(0, 500)}`);
689
+ }
690
+ const data = await res.json();
691
+ const text2 = data.content.filter((b) => b.type === "text").map((b) => b.text ?? "").join("");
692
+ const toolCalls = data.content.filter((b) => b.type === "tool_use").map((b) => ({
693
+ id: b.id ?? "",
694
+ name: b.name ?? "",
695
+ arguments: b.input ?? {}
696
+ }));
697
+ return {
698
+ content: text2,
699
+ toolCalls,
700
+ finishReason: data.stop_reason ?? "stop",
701
+ usage: data.usage ? {
702
+ promptTokens: data.usage.input_tokens,
703
+ completionTokens: data.usage.output_tokens
704
+ } : void 0
705
+ };
706
+ }
707
+ };
708
+ function groupMessages(messages) {
709
+ const out = [];
710
+ for (const m of messages) {
711
+ if (m.role === "tool") {
712
+ const block = {
713
+ type: "tool_result",
714
+ tool_use_id: m.toolCallId ?? "",
715
+ content: m.content
716
+ };
717
+ const last = out[out.length - 1];
718
+ if (last && last.role === "user") last.content.push(block);
719
+ else out.push({ role: "user", content: [block] });
720
+ continue;
721
+ }
722
+ if (m.role === "assistant") {
723
+ const blocks = [];
724
+ if (m.content) blocks.push({ type: "text", text: m.content });
725
+ for (const tc of m.toolCalls ?? []) {
726
+ blocks.push({ type: "tool_use", id: tc.id, name: tc.name, input: tc.arguments });
727
+ }
728
+ out.push({ role: "assistant", content: blocks });
729
+ continue;
730
+ }
731
+ out.push({ role: "user", content: [{ type: "text", text: m.content }] });
732
+ }
733
+ return out;
734
+ }
735
+
736
+ // src/core/providers/openai-compatible.ts
737
+ import OpenAI from "openai";
738
+ var OpenAICompatibleProvider = class {
739
+ name;
740
+ model;
741
+ client;
742
+ constructor(opts) {
743
+ this.name = opts.name;
744
+ this.model = opts.model;
745
+ this.client = new OpenAI({
746
+ baseURL: opts.baseURL,
747
+ // Ollama accepts any non-empty key; OpenRouter requires a real one.
748
+ apiKey: opts.apiKey ?? "polypus-no-key",
749
+ timeout: opts.timeoutMs ?? 12e4,
750
+ maxRetries: opts.maxRetries ?? 2
751
+ });
752
+ }
753
+ async chat(req) {
754
+ const messages = req.messages.map(toOpenAIMessage);
755
+ const tools = req.tools?.map((t2) => ({
756
+ type: "function",
757
+ function: {
758
+ name: t2.name,
759
+ description: t2.description,
760
+ parameters: t2.parameters
761
+ }
762
+ }));
763
+ const completion = await this.client.chat.completions.create(
764
+ {
765
+ model: this.model,
766
+ messages,
767
+ ...tools && tools.length > 0 ? { tools } : {},
768
+ temperature: req.params?.temperature,
769
+ // Generous default so large files aren't truncated mid tool-call.
770
+ max_tokens: req.params?.maxTokens ?? 8192
771
+ },
772
+ { signal: req.signal }
773
+ );
774
+ const choice = completion.choices[0];
775
+ const msg = choice?.message;
776
+ const toolCalls = (msg?.tool_calls ?? []).map((tc, i) => ({
777
+ id: tc.id || `call_${i}`,
778
+ name: tc.function.name,
779
+ arguments: safeParseArgs(tc.function.arguments)
780
+ }));
781
+ return {
782
+ content: msg?.content ?? "",
783
+ toolCalls,
784
+ finishReason: choice?.finish_reason ?? "stop",
785
+ usage: completion.usage ? {
786
+ promptTokens: completion.usage.prompt_tokens,
787
+ completionTokens: completion.usage.completion_tokens
788
+ } : void 0
789
+ };
790
+ }
791
+ };
792
+ function toOpenAIMessage(m) {
793
+ switch (m.role) {
794
+ case "tool":
795
+ return {
796
+ role: "tool",
797
+ content: m.content,
798
+ tool_call_id: m.toolCallId ?? ""
799
+ };
800
+ case "assistant":
801
+ return {
802
+ role: "assistant",
803
+ content: m.content || null,
804
+ ...m.toolCalls && m.toolCalls.length > 0 ? {
805
+ tool_calls: m.toolCalls.map((tc) => ({
806
+ id: tc.id,
807
+ type: "function",
808
+ function: {
809
+ name: tc.name,
810
+ arguments: JSON.stringify(tc.arguments)
811
+ }
812
+ }))
813
+ } : {}
814
+ };
815
+ case "system":
816
+ return { role: "system", content: m.content };
817
+ default:
818
+ return { role: "user", content: m.content };
819
+ }
820
+ }
821
+ function safeParseArgs(raw) {
822
+ if (!raw || !raw.trim()) return {};
823
+ try {
824
+ const parsed = JSON.parse(raw);
825
+ if (typeof parsed === "object" && parsed !== null) return parsed;
826
+ return { value: parsed };
827
+ } catch {
828
+ return recoverArgs(raw);
829
+ }
830
+ }
831
+ var SHORT_FIELDS = ["path", "command", "summary"];
832
+ var LONG_FIELDS = ["content", "replace", "search"];
833
+ function recoverArgs(raw) {
834
+ const out = {};
835
+ for (const key of SHORT_FIELDS) {
836
+ const m = new RegExp(`"${key}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`).exec(raw);
837
+ if (m) out[key] = unescapeJsonString(m[1]);
838
+ }
839
+ for (const key of LONG_FIELDS) {
840
+ const opener = new RegExp(`"${key}"\\s*:\\s*"`).exec(raw);
841
+ if (!opener) continue;
842
+ const from = opener.index + opener[0].length;
843
+ const end = raw.lastIndexOf('"');
844
+ if (end > from) out[key] = unescapeJsonString(raw.slice(from, end));
845
+ }
846
+ if (Object.keys(out).length === 0) out._raw = raw;
847
+ return out;
848
+ }
849
+ function unescapeJsonString(s) {
850
+ return s.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\//g, "/").replace(/\\\\/g, "\\");
851
+ }
852
+
853
+ // src/core/providers/registry.ts
854
+ function resolveToolMode(agent) {
855
+ if (agent.toolMode !== "auto") return agent.toolMode;
856
+ return agent.provider === "ollama" ? "emulated" : "native";
857
+ }
858
+ function createProvider(agent) {
859
+ const apiKey = resolveSecret(agent.apiKey);
860
+ const baseURL = agent.baseUrl ?? DEFAULT_BASE_URL[agent.provider];
861
+ if (!baseURL) {
862
+ throw new Error(t("agent.noBaseUrl", { name: agent.name }));
863
+ }
864
+ let provider;
865
+ if (agent.provider === "anthropic") {
866
+ if (!apiKey) throw new Error(t("agent.needAnthropicKey", { name: agent.name }));
867
+ provider = new AnthropicProvider({
868
+ name: agent.name,
869
+ model: agent.model,
870
+ baseURL,
871
+ apiKey
872
+ });
873
+ } else {
874
+ provider = new OpenAICompatibleProvider({
875
+ name: agent.name,
876
+ model: agent.model,
877
+ baseURL,
878
+ apiKey
879
+ });
880
+ }
881
+ return { config: agent, provider, toolMode: resolveToolMode(agent) };
882
+ }
883
+
884
+ // src/core/permissions/allowlist.ts
885
+ import { isAbsolute, relative, resolve, sep } from "path";
886
+ function globToRegExp(glob) {
887
+ let re = "";
888
+ for (let i = 0; i < glob.length; i++) {
889
+ const c = glob[i];
890
+ if (c === "*") {
891
+ if (glob[i + 1] === "*") {
892
+ re += ".*";
893
+ i++;
894
+ if (glob[i + 1] === "/") i++;
895
+ } else {
896
+ re += "[^/]*";
897
+ }
898
+ } else if (c === "?") {
899
+ re += "[^/]";
900
+ } else if (".+^${}()|[]\\".includes(c)) {
901
+ re += "\\" + c;
902
+ } else {
903
+ re += c;
904
+ }
905
+ }
906
+ return new RegExp(`^${re}$`);
907
+ }
908
+ function toPosix(p4) {
909
+ return p4.split(sep).join("/");
910
+ }
911
+ function checkPath(policy, target) {
912
+ const abs = isAbsolute(target) ? target : resolve(policy.workspace, target);
913
+ const rel = toPosix(relative(policy.workspace, abs));
914
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
915
+ return { allowed: false, rel, reason: "path escapes the workspace" };
916
+ }
917
+ for (const d of policy.deny) {
918
+ if (globToRegExp(d).test(rel)) {
919
+ return { allowed: false, rel, reason: `denied by deny-list pattern "${d}"` };
920
+ }
921
+ }
922
+ for (const a of policy.allow) {
923
+ if (globToRegExp(a).test(rel)) return { allowed: true, rel };
924
+ }
925
+ return { allowed: false, rel, reason: "not in the allow-list" };
926
+ }
927
+ function isCommandPreApproved(allowedCommands, command) {
928
+ const c = command.trim();
929
+ return allowedCommands.some((prefix) => c === prefix || c.startsWith(prefix.trim() + " "));
930
+ }
931
+
932
+ // src/core/permissions/modes.ts
933
+ var PermissionEngine = class {
934
+ constructor(opts) {
935
+ this.opts = opts;
936
+ }
937
+ opts;
938
+ get mode() {
939
+ return this.opts.mode;
940
+ }
941
+ /** Reads are allowed in every mode as long as the path is within the allow-list. */
942
+ authorizeRead(target) {
943
+ const d = checkPath(this.opts.policy, target);
944
+ return d.allowed ? { allowed: true } : { allowed: false, reason: d.reason };
945
+ }
946
+ async authorizeWrite(target, preview) {
947
+ const d = checkPath(this.opts.policy, target);
948
+ if (!d.allowed) return { allowed: false, reason: d.reason };
949
+ if (this.opts.mode === "plan") {
950
+ return { allowed: false, reason: "plan mode: file modifications are disabled" };
951
+ }
952
+ if (this.opts.mode === "bypass") return { allowed: true };
953
+ const ok = await this.ask({ kind: "write", summary: `write ${d.rel}`, preview });
954
+ return ok ? { allowed: true } : { allowed: false, reason: "rejected by user" };
955
+ }
956
+ async authorizeCommand(command) {
957
+ if (this.opts.mode === "plan") {
958
+ return { allowed: false, reason: "plan mode: running commands is disabled" };
959
+ }
960
+ if (this.opts.mode === "bypass") return { allowed: true };
961
+ if (isCommandPreApproved(this.opts.allowedCommands, command)) return { allowed: true };
962
+ const ok = await this.ask({ kind: "command", summary: `run: ${command}` });
963
+ return ok ? { allowed: true } : { allowed: false, reason: "rejected by user" };
964
+ }
965
+ async ask(req) {
966
+ if (!this.opts.confirm) return false;
967
+ return this.opts.confirm(req);
968
+ }
969
+ };
970
+
971
+ // src/core/protocol/system-prompt.ts
972
+ function basePreamble(ctx) {
973
+ const modeLine = ctx.mode === "plan" ? "You are in PLAN mode: investigate and propose changes, but do NOT modify files or run commands. Describe the plan and call `finish`." : ctx.mode === "review" ? "You are in REVIEW mode: each file write and command will be shown to the user for approval before it runs." : "You are in BYPASS mode: actions are applied automatically.";
974
+ return [
975
+ "You are Polypus, an autonomous coding agent working inside a real project directory.",
976
+ "",
977
+ `Workspace (current working directory): ${ctx.workspace}`,
978
+ `Editable paths (glob allow-list): ${ctx.allow.join(", ")}`,
979
+ modeLine,
980
+ "",
981
+ "IMPORTANT \u2014 you have real permission to act:",
982
+ "- YES, you ARE allowed to create, read, and modify files in this workspace.",
983
+ "- YES, you ARE allowed to run shell commands (subject to the permission mode above).",
984
+ "- Do not ask for permission and do not say you cannot edit files \u2014 you can. Just emit the tool calls.",
985
+ "- Make the changes directly. When the task is fully done, call the `finish` tool with a short summary.",
986
+ t("prompt.language", { language: LOCALE_NAMES[getLocale()] }),
987
+ ctx.briefing ? `
988
+ Your assigned task:
989
+ ${ctx.briefing}` : ""
990
+ ].join("\n");
991
+ }
992
+ function buildNativeSystemPrompt(ctx) {
993
+ return [
994
+ basePreamble(ctx),
995
+ "",
996
+ "Use the provided tools/functions to read and edit files and run commands. Prefer small, targeted edits."
997
+ ].join("\n");
998
+ }
999
+ function describeParams(tool) {
1000
+ const props = tool.parameters.properties ?? {};
1001
+ const required = new Set(
1002
+ tool.parameters.required ?? []
1003
+ );
1004
+ const lines = Object.entries(props).map(
1005
+ ([k, v]) => ` <arg name="${k}">\u2026</arg> ${required.has(k) ? "(required)" : "(optional)"} ${v.description ?? ""}`.trimEnd()
1006
+ );
1007
+ return lines.length > 0 ? lines.join("\n") : " (no arguments)";
1008
+ }
1009
+ function buildEmulatedSystemPrompt(tools, ctx) {
1010
+ const toolDocs = tools.map(
1011
+ (t2) => `- ${t2.name}: ${t2.description}
1012
+ Call it like:
1013
+ <polypus:tool name="${t2.name}">
1014
+ ${describeParams(t2)}
1015
+ </polypus:tool>`
1016
+ ).join("\n\n");
1017
+ return [
1018
+ basePreamble(ctx),
1019
+ "",
1020
+ "This model has no native tool API, so you act by emitting tool calls as XML blocks.",
1021
+ "STRICT OUTPUT RULES:",
1022
+ '- To act, output one or more <polypus:tool name="..."> blocks and NOTHING else (no markdown code fences around them).',
1023
+ "- Put file contents or code directly inside the relevant <arg> \u2014 angle brackets in code are fine.",
1024
+ "- You may include one or more tool blocks per turn. After you receive the results, continue.",
1025
+ "- When the entire task is complete, emit a finish call:",
1026
+ ' <polypus:tool name="finish"><arg name="summary">what you did</arg></polypus:tool>',
1027
+ "",
1028
+ "Available tools:",
1029
+ "",
1030
+ toolDocs
1031
+ ].join("\n");
1032
+ }
1033
+ function buildReprompt() {
1034
+ return [
1035
+ "You did not emit any tool call. Remember: you ARE allowed and expected to act now.",
1036
+ "Do NOT explain that you cannot edit files \u2014 you can.",
1037
+ 'Respond ONLY with one or more <polypus:tool name="..."> blocks to make the change,',
1038
+ 'or with <polypus:tool name="finish"><arg name="summary">\u2026</arg></polypus:tool> if the task is already complete.'
1039
+ ].join("\n");
1040
+ }
1041
+
1042
+ // src/core/protocol/native.ts
1043
+ var NativeDriver = class {
1044
+ constructor(tools) {
1045
+ this.tools = tools;
1046
+ }
1047
+ tools;
1048
+ kind = "native";
1049
+ systemPrompt(ctx) {
1050
+ return buildNativeSystemPrompt(ctx);
1051
+ }
1052
+ providerTools() {
1053
+ return this.tools;
1054
+ }
1055
+ parse(response) {
1056
+ return { toolCalls: response.toolCalls, text: response.content };
1057
+ }
1058
+ assistantMessage(response, toolCalls) {
1059
+ return { role: "assistant", content: response.content, toolCalls };
1060
+ }
1061
+ toolResultMessage(call, resultText) {
1062
+ return { role: "tool", toolCallId: call.id, name: call.name, content: resultText };
1063
+ }
1064
+ repromptMessage() {
1065
+ return {
1066
+ role: "user",
1067
+ content: "You did not call any tool. Use the available tools to act now, or call `finish` if the task is complete."
1068
+ };
1069
+ }
1070
+ };
1071
+
1072
+ // src/core/protocol/parser.ts
1073
+ var TOOL_OPEN = /<polypus:tool\s+name="([^"]+)"\s*>/g;
1074
+ var TOOL_CLOSE = "</polypus:tool>";
1075
+ var ARG_OPEN = /<arg\s+name="([^"]+)"\s*>/g;
1076
+ function parseEmulatedToolCalls(output, knownToolNames = []) {
1077
+ const toolCalls = [];
1078
+ const proseParts = [];
1079
+ let cursor = 0;
1080
+ let callIndex = 0;
1081
+ TOOL_OPEN.lastIndex = 0;
1082
+ let open;
1083
+ while (open = TOOL_OPEN.exec(output)) {
1084
+ const name = open[1];
1085
+ const blockStart = open.index + open[0].length;
1086
+ const closeIdx = output.indexOf(TOOL_CLOSE, blockStart);
1087
+ if (closeIdx === -1) break;
1088
+ proseParts.push(output.slice(cursor, open.index));
1089
+ const block = output.slice(blockStart, closeIdx);
1090
+ toolCalls.push({
1091
+ id: `emu_${callIndex++}`,
1092
+ name: name.trim(),
1093
+ arguments: parseArgs(block)
1094
+ });
1095
+ cursor = closeIdx + TOOL_CLOSE.length;
1096
+ TOOL_OPEN.lastIndex = cursor;
1097
+ }
1098
+ proseParts.push(output.slice(cursor));
1099
+ let text2 = proseParts.join("").trim();
1100
+ text2 = text2.replace(/<\/?polypus:tool[^>]*>/g, "").trim();
1101
+ if (knownToolNames.length > 0) {
1102
+ for (const name of knownToolNames) {
1103
+ const n = escapeName(name);
1104
+ const tagRe = new RegExp(`<${n}(?:\\s[^>]*)?>([\\s\\S]*?)</${n}>`, "g");
1105
+ let m;
1106
+ while (m = tagRe.exec(text2)) {
1107
+ toolCalls.push({ id: `emu_${callIndex++}`, name, arguments: parseArgs(m[1]) });
1108
+ }
1109
+ text2 = text2.replace(tagRe, "").trim();
1110
+ const labelRe = new RegExp(
1111
+ `(?:^|\\n)[ \\t]*${n}[ \\t]*:?[ \\t]*\\n?((?:[ \\t]*<arg\\b[\\s\\S]*?</arg>[ \\t]*\\n?)+)`,
1112
+ "g"
1113
+ );
1114
+ while (m = labelRe.exec(text2)) {
1115
+ toolCalls.push({ id: `emu_${callIndex++}`, name, arguments: parseArgs(m[1]) });
1116
+ }
1117
+ text2 = text2.replace(labelRe, "").trim();
1118
+ }
1119
+ }
1120
+ return { toolCalls, text: text2 };
1121
+ }
1122
+ function escapeName(name) {
1123
+ return name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1124
+ }
1125
+ function parseArgs(block) {
1126
+ const args = {};
1127
+ ARG_OPEN.lastIndex = 0;
1128
+ const matches = [];
1129
+ let m;
1130
+ while (m = ARG_OPEN.exec(block)) {
1131
+ matches.push({ name: m[1], valueStart: m.index + m[0].length });
1132
+ }
1133
+ for (let i = 0; i < matches.length; i++) {
1134
+ const current = matches[i];
1135
+ const next = matches[i + 1];
1136
+ const region = block.slice(current.valueStart, next ? next.valueStart : block.length);
1137
+ args[current.name.trim()] = trimArgValue(stripLastCloseTag(region));
1138
+ }
1139
+ return args;
1140
+ }
1141
+ function stripLastCloseTag(region) {
1142
+ const idx = region.lastIndexOf("</arg>");
1143
+ return idx === -1 ? region : region.slice(0, idx);
1144
+ }
1145
+ function trimArgValue(value) {
1146
+ return value.replace(/^\r?\n/, "").replace(/\r?\n[ \t]*$/, "");
1147
+ }
1148
+
1149
+ // src/core/protocol/emulated.ts
1150
+ var EmulatedDriver = class {
1151
+ constructor(tools) {
1152
+ this.tools = tools;
1153
+ }
1154
+ tools;
1155
+ kind = "emulated";
1156
+ systemPrompt(ctx) {
1157
+ return buildEmulatedSystemPrompt(this.tools, ctx);
1158
+ }
1159
+ providerTools() {
1160
+ return void 0;
1161
+ }
1162
+ parse(response) {
1163
+ return parseEmulatedToolCalls(
1164
+ response.content,
1165
+ this.tools.map((t2) => t2.name)
1166
+ );
1167
+ }
1168
+ assistantMessage(response) {
1169
+ return { role: "assistant", content: response.content };
1170
+ }
1171
+ toolResultMessage(call, resultText) {
1172
+ return {
1173
+ role: "user",
1174
+ content: `<polypus:tool_result name="${call.name}">
1175
+ ${resultText}
1176
+ </polypus:tool_result>`
1177
+ };
1178
+ }
1179
+ repromptMessage() {
1180
+ return { role: "user", content: buildReprompt() };
1181
+ }
1182
+ };
1183
+ function makeDriver(kind, tools) {
1184
+ return kind === "native" ? new NativeDriver(tools) : new EmulatedDriver(tools);
1185
+ }
1186
+
1187
+ // src/core/tools/edit-file.ts
1188
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
1189
+ import { resolve as resolve2 } from "path";
1190
+ import { z as z2 } from "zod";
1191
+ var Args = z2.object({
1192
+ path: z2.string().min(1),
1193
+ search: z2.string().min(1),
1194
+ replace: z2.string()
1195
+ });
1196
+ var editFileTool = {
1197
+ mutating: true,
1198
+ spec: {
1199
+ name: "edit_file",
1200
+ description: "Replace an exact snippet in a file. 'search' must match the existing text verbatim and uniquely.",
1201
+ parameters: {
1202
+ type: "object",
1203
+ properties: {
1204
+ path: { type: "string", description: "Workspace-relative file path" },
1205
+ search: { type: "string", description: "Exact text to find (must be unique in the file)" },
1206
+ replace: { type: "string", description: "Text to replace it with" }
1207
+ },
1208
+ required: ["path", "search", "replace"]
1209
+ }
1210
+ },
1211
+ async run(rawArgs, ctx) {
1212
+ const args = Args.safeParse(rawArgs);
1213
+ if (!args.success) {
1214
+ return {
1215
+ ok: false,
1216
+ output: "edit_file needs three arguments: 'path', 'search' (exact text to find), and 'replace'. Resend the tool call with all three filled in."
1217
+ };
1218
+ }
1219
+ const abs = resolve2(ctx.workspace, args.data.path);
1220
+ let content;
1221
+ try {
1222
+ content = await readFile2(abs, "utf8");
1223
+ } catch (err) {
1224
+ return { ok: false, output: `Could not read file to edit: ${err.message}` };
1225
+ }
1226
+ const occurrences = content.split(args.data.search).length - 1;
1227
+ if (occurrences === 0) {
1228
+ return { ok: false, output: "The 'search' text was not found. Re-read the file and try an exact snippet." };
1229
+ }
1230
+ if (occurrences > 1) {
1231
+ return {
1232
+ ok: false,
1233
+ output: `The 'search' text matched ${occurrences} times; it must be unique. Include more surrounding context.`
1234
+ };
1235
+ }
1236
+ const updated = content.replace(args.data.search, args.data.replace);
1237
+ const decision = await ctx.permissions.authorizeWrite(
1238
+ args.data.path,
1239
+ `- ${firstLine(args.data.search)}
1240
+ + ${firstLine(args.data.replace)}`
1241
+ );
1242
+ if (!decision.allowed) return { ok: false, output: `Edit denied: ${decision.reason}` };
1243
+ try {
1244
+ await writeFile2(abs, updated, "utf8");
1245
+ return { ok: true, output: `Edited ${args.data.path}.` };
1246
+ } catch (err) {
1247
+ return { ok: false, output: `Could not write edit: ${err.message}` };
1248
+ }
1249
+ }
1250
+ };
1251
+ function firstLine(s) {
1252
+ return s.split("\n")[0] ?? "";
1253
+ }
1254
+
1255
+ // src/core/tools/list-dir.ts
1256
+ import { readdir } from "fs/promises";
1257
+ import { resolve as resolve3 } from "path";
1258
+ import { z as z3 } from "zod";
1259
+ var Args2 = z3.object({ path: z3.string().default(".") });
1260
+ var listDirTool = {
1261
+ mutating: false,
1262
+ spec: {
1263
+ name: "list_dir",
1264
+ description: "List files and subdirectories of a workspace directory.",
1265
+ parameters: {
1266
+ type: "object",
1267
+ properties: { path: { type: "string", description: "Workspace-relative directory (default '.')" } }
1268
+ }
1269
+ },
1270
+ async run(rawArgs, ctx) {
1271
+ const args = Args2.safeParse(rawArgs);
1272
+ const path = args.success ? args.data.path : ".";
1273
+ const decision = ctx.permissions.authorizeRead(path === "." ? "." : path);
1274
+ if (path !== "." && !decision.allowed) {
1275
+ return { ok: false, output: `List denied: ${decision.reason}` };
1276
+ }
1277
+ try {
1278
+ const entries = await readdir(resolve3(ctx.workspace, path), { withFileTypes: true });
1279
+ const lines = entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name).sort();
1280
+ return { ok: true, output: lines.length ? lines.join("\n") : "(empty)" };
1281
+ } catch (err) {
1282
+ return { ok: false, output: `Could not list directory: ${err.message}` };
1283
+ }
1284
+ }
1285
+ };
1286
+
1287
+ // src/core/tools/read-file.ts
1288
+ import { readFile as readFile3 } from "fs/promises";
1289
+ import { resolve as resolve4 } from "path";
1290
+ import { z as z4 } from "zod";
1291
+ var Args3 = z4.object({ path: z4.string().min(1) });
1292
+ var MAX_CHARS = 6e4;
1293
+ var readFileTool = {
1294
+ mutating: false,
1295
+ spec: {
1296
+ name: "read_file",
1297
+ description: "Read the contents of a file in the workspace.",
1298
+ parameters: {
1299
+ type: "object",
1300
+ properties: { path: { type: "string", description: "Workspace-relative file path" } },
1301
+ required: ["path"]
1302
+ }
1303
+ },
1304
+ async run(rawArgs, ctx) {
1305
+ const args = Args3.safeParse(rawArgs);
1306
+ if (!args.success) return { ok: false, output: "Invalid args: 'path' is required." };
1307
+ const decision = ctx.permissions.authorizeRead(args.data.path);
1308
+ if (!decision.allowed) return { ok: false, output: `Read denied: ${decision.reason}` };
1309
+ try {
1310
+ const content = await readFile3(resolve4(ctx.workspace, args.data.path), "utf8");
1311
+ const truncated = content.length > MAX_CHARS;
1312
+ return {
1313
+ ok: true,
1314
+ output: truncated ? content.slice(0, MAX_CHARS) + "\n\u2026[truncated]" : content
1315
+ };
1316
+ } catch (err) {
1317
+ return { ok: false, output: `Could not read file: ${err.message}` };
1318
+ }
1319
+ }
1320
+ };
1321
+
1322
+ // src/core/tools/run-command.ts
1323
+ import { exec } from "child_process";
1324
+ import { promisify } from "util";
1325
+ import { z as z5 } from "zod";
1326
+ var execAsync = promisify(exec);
1327
+ var Args4 = z5.object({ command: z5.string().min(1) });
1328
+ var MAX_OUTPUT = 2e4;
1329
+ var runCommandTool = {
1330
+ mutating: true,
1331
+ spec: {
1332
+ name: "run_command",
1333
+ description: "Run a shell command in the workspace and return its combined stdout/stderr.",
1334
+ parameters: {
1335
+ type: "object",
1336
+ properties: { command: { type: "string", description: "Shell command to execute" } },
1337
+ required: ["command"]
1338
+ }
1339
+ },
1340
+ async run(rawArgs, ctx) {
1341
+ const args = Args4.safeParse(rawArgs);
1342
+ if (!args.success) return { ok: false, output: "Invalid args: 'command' is required." };
1343
+ const decision = await ctx.permissions.authorizeCommand(args.data.command);
1344
+ if (!decision.allowed) return { ok: false, output: `Command denied: ${decision.reason}` };
1345
+ try {
1346
+ const { stdout: stdout2, stderr } = await execAsync(args.data.command, {
1347
+ cwd: ctx.workspace,
1348
+ timeout: 12e4,
1349
+ maxBuffer: 10 * 1024 * 1024,
1350
+ windowsHide: true
1351
+ });
1352
+ return { ok: true, output: clamp(`${stdout2}${stderr ? `
1353
+ [stderr]
1354
+ ${stderr}` : ""}`.trim() || "(no output)") };
1355
+ } catch (err) {
1356
+ const e = err;
1357
+ const body = `${e.stdout ?? ""}${e.stderr ?? ""}`.trim();
1358
+ return {
1359
+ ok: false,
1360
+ output: clamp(`Command failed (exit ${e.code ?? "?"}): ${e.message}
1361
+ ${body}`)
1362
+ };
1363
+ }
1364
+ }
1365
+ };
1366
+ function clamp(s) {
1367
+ return s.length > MAX_OUTPUT ? s.slice(0, MAX_OUTPUT) + "\n\u2026[truncated]" : s;
1368
+ }
1369
+
1370
+ // src/core/tools/types.ts
1371
+ var FINISH_TOOL = {
1372
+ name: "finish",
1373
+ description: "Call when the task is fully complete. Provide a short summary of what was done.",
1374
+ parameters: {
1375
+ type: "object",
1376
+ properties: {
1377
+ summary: { type: "string", description: "Summary of the work completed" }
1378
+ },
1379
+ required: ["summary"]
1380
+ }
1381
+ };
1382
+
1383
+ // src/core/tools/write-file.ts
1384
+ import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
1385
+ import { dirname, resolve as resolve5 } from "path";
1386
+ import { z as z6 } from "zod";
1387
+ var Args5 = z6.object({ path: z6.string().min(1), content: z6.string() });
1388
+ var writeFileTool = {
1389
+ mutating: true,
1390
+ spec: {
1391
+ name: "write_file",
1392
+ description: "Create or overwrite a file with the given content.",
1393
+ parameters: {
1394
+ type: "object",
1395
+ properties: {
1396
+ path: { type: "string", description: "Workspace-relative file path" },
1397
+ content: { type: "string", description: "Full file content" }
1398
+ },
1399
+ required: ["path", "content"]
1400
+ }
1401
+ },
1402
+ async run(rawArgs, ctx) {
1403
+ const args = Args5.safeParse(rawArgs);
1404
+ if (!args.success) {
1405
+ const got = Object.keys(rawArgs ?? {});
1406
+ return {
1407
+ ok: false,
1408
+ output: `write_file needs two arguments: 'path' (the file path) and 'content' (the complete file text). Received: [${got.join(", ") || "none"}]. Resend the tool call with BOTH arguments filled in, and keep the file small enough to fit in one message.`
1409
+ };
1410
+ }
1411
+ const preview = previewContent(args.data.content);
1412
+ const decision = await ctx.permissions.authorizeWrite(args.data.path, preview);
1413
+ if (!decision.allowed) return { ok: false, output: `Write denied: ${decision.reason}` };
1414
+ try {
1415
+ const abs = resolve5(ctx.workspace, args.data.path);
1416
+ await mkdir2(dirname(abs), { recursive: true });
1417
+ await writeFile3(abs, args.data.content, "utf8");
1418
+ const lines = args.data.content.split("\n").length;
1419
+ return { ok: true, output: `Wrote ${args.data.path} (${lines} lines).` };
1420
+ } catch (err) {
1421
+ return { ok: false, output: `Could not write file: ${err.message}` };
1422
+ }
1423
+ }
1424
+ };
1425
+ function previewContent(content) {
1426
+ const lines = content.split("\n");
1427
+ return lines.length > 40 ? lines.slice(0, 40).join("\n") + "\n\u2026" : content;
1428
+ }
1429
+
1430
+ // src/core/tools/registry.ts
1431
+ var TOOLS = {
1432
+ [readFileTool.spec.name]: readFileTool,
1433
+ [listDirTool.spec.name]: listDirTool,
1434
+ [writeFileTool.spec.name]: writeFileTool,
1435
+ [editFileTool.spec.name]: editFileTool,
1436
+ [runCommandTool.spec.name]: runCommandTool
1437
+ };
1438
+ function toolSpecs() {
1439
+ return [...Object.values(TOOLS).map((t2) => t2.spec), FINISH_TOOL];
1440
+ }
1441
+ function getTool(name) {
1442
+ return TOOLS[name];
1443
+ }
1444
+
1445
+ // src/core/agent/loop.ts
1446
+ function looksLikeStall(text2) {
1447
+ const lc = text2.toLowerCase();
1448
+ const markers = [
1449
+ "i can't",
1450
+ "i cannot",
1451
+ "i can not",
1452
+ "i'm unable",
1453
+ "i am unable",
1454
+ "unable to",
1455
+ "cannot create",
1456
+ "can't create",
1457
+ "cannot write",
1458
+ "can't write",
1459
+ "not allowed",
1460
+ "i'll create",
1461
+ "i will create",
1462
+ "let me create",
1463
+ "i'll write",
1464
+ "i will write",
1465
+ "n\xE3o posso",
1466
+ "nao posso",
1467
+ "n\xE3o consigo",
1468
+ "nao consigo",
1469
+ "n\xE3o tenho permiss",
1470
+ "nao tenho permiss",
1471
+ "n\xE3o \xE9 poss\xEDvel",
1472
+ "nao e possivel",
1473
+ "vou criar",
1474
+ "irei criar",
1475
+ "vou escrever",
1476
+ "deixe-me",
1477
+ "deixa eu"
1478
+ ];
1479
+ return markers.some((m) => lc.includes(m));
1480
+ }
1481
+ async function runAgent(opts) {
1482
+ const { agent, permissions, events } = opts;
1483
+ const maxSteps = opts.maxSteps ?? 30;
1484
+ const maxReprompts = opts.maxReprompts ?? 3;
1485
+ const driver = makeDriver(agent.toolMode, toolSpecs());
1486
+ const ctx = { workspace: opts.workspace, permissions };
1487
+ const messages = opts.history && opts.history.length > 0 ? [...opts.history, { role: "user", content: opts.task }] : [
1488
+ { role: "system", content: driver.systemPrompt(opts.promptContext) },
1489
+ { role: "user", content: opts.task }
1490
+ ];
1491
+ let consecutiveNoTool = 0;
1492
+ let lastFailSig = "";
1493
+ let failStreak = 0;
1494
+ const maxToolRetries = opts.maxToolRetries ?? 3;
1495
+ const usage = { promptTokens: 0, completionTokens: 0 };
1496
+ for (let step = 1; step <= maxSteps; step++) {
1497
+ if (opts.signal?.aborted) return { finished: false, reason: "cancelled", steps: step - 1, messages, usage };
1498
+ events?.onStep?.(step);
1499
+ let response;
1500
+ try {
1501
+ response = await agent.provider.chat({
1502
+ messages,
1503
+ tools: driver.providerTools(),
1504
+ params: opts.params,
1505
+ signal: opts.signal
1506
+ });
1507
+ } catch (err) {
1508
+ if (opts.signal?.aborted) return { finished: false, reason: "cancelled", steps: step, messages, usage };
1509
+ throw err;
1510
+ }
1511
+ usage.promptTokens += response.usage?.promptTokens ?? 0;
1512
+ usage.completionTokens += response.usage?.completionTokens ?? 0;
1513
+ events?.onUsage?.(usage);
1514
+ const { toolCalls, text: text2 } = driver.parse(response);
1515
+ messages.push(driver.assistantMessage(response, toolCalls));
1516
+ if (text2) events?.onAssistantText?.(text2);
1517
+ if (toolCalls.length === 0) {
1518
+ const stalled = text2.trim().length === 0 || looksLikeStall(text2);
1519
+ if (stalled) {
1520
+ if (consecutiveNoTool < maxReprompts) {
1521
+ consecutiveNoTool++;
1522
+ events?.onReprompt?.(consecutiveNoTool);
1523
+ messages.push(driver.repromptMessage());
1524
+ continue;
1525
+ }
1526
+ return { finished: false, reason: "stalled", steps: step, messages, usage };
1527
+ }
1528
+ return { finished: false, reason: "reply", steps: step, messages, usage };
1529
+ }
1530
+ consecutiveNoTool = 0;
1531
+ for (const call of toolCalls) {
1532
+ if (opts.signal?.aborted) return { finished: false, reason: "cancelled", steps: step, messages, usage };
1533
+ events?.onToolCall?.(call);
1534
+ if (call.name === "finish") {
1535
+ const summary = String(call.arguments.summary ?? "").trim();
1536
+ return { finished: true, reason: "finished", summary, steps: step, messages, usage };
1537
+ }
1538
+ const tool = getTool(call.name);
1539
+ const result = tool ? await tool.run(call.arguments, ctx) : { ok: false, output: `Unknown tool "${call.name}". Available: ${toolSpecs().map((t2) => t2.name).join(", ")}` };
1540
+ events?.onToolResult?.(call, result);
1541
+ messages.push(driver.toolResultMessage(call, result.output));
1542
+ if (result.ok) {
1543
+ failStreak = 0;
1544
+ lastFailSig = "";
1545
+ } else {
1546
+ const sig = `${call.name}:${JSON.stringify(call.arguments)}`;
1547
+ failStreak = sig === lastFailSig ? failStreak + 1 : 1;
1548
+ lastFailSig = sig;
1549
+ if (failStreak >= maxToolRetries) {
1550
+ return { finished: false, reason: "stalled", steps: step, messages, usage };
1551
+ }
1552
+ }
1553
+ }
1554
+ }
1555
+ return { finished: false, reason: "maxsteps", steps: maxSteps, messages, usage };
1556
+ }
1557
+
1558
+ // src/ui/repl.ts
1559
+ import * as readline from "readline/promises";
1560
+ import { stdin, stdout } from "process";
1561
+ import pc6 from "picocolors";
1562
+
1563
+ // src/ui/wizard.ts
1564
+ import * as p from "@clack/prompts";
1565
+ import pc4 from "picocolors";
1566
+
1567
+ // src/core/providers/ollama.ts
1568
+ function normalizeHost(host) {
1569
+ let h = host.trim().replace(/\/v1\/?$/, "").replace(/\/$/, "");
1570
+ if (!/^https?:\/\//.test(h)) h = `http://${h}`;
1571
+ return h;
1572
+ }
1573
+ function ollamaHost() {
1574
+ return normalizeHost(process.env.OLLAMA_HOST ?? "http://localhost:11434");
1575
+ }
1576
+ async function listOllamaModels(host = ollamaHost(), timeoutMs = 2e3) {
1577
+ const controller = new AbortController();
1578
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1579
+ try {
1580
+ const res = await fetch(`${normalizeHost(host)}/api/tags`, { signal: controller.signal });
1581
+ if (!res.ok) return [];
1582
+ const data = await res.json();
1583
+ return (data.models ?? []).map((m) => m.name).filter((n) => Boolean(n));
1584
+ } catch {
1585
+ return [];
1586
+ } finally {
1587
+ clearTimeout(timer);
1588
+ }
1589
+ }
1590
+
1591
+ // src/core/providers/openrouter.ts
1592
+ var MODELS_URL = "https://openrouter.ai/api/v1/models";
1593
+ async function listOpenRouterModels(apiKey, timeoutMs = 8e3) {
1594
+ const controller = new AbortController();
1595
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1596
+ try {
1597
+ const res = await fetch(MODELS_URL, {
1598
+ signal: controller.signal,
1599
+ headers: apiKey ? { authorization: `Bearer ${apiKey}` } : {}
1600
+ });
1601
+ if (!res.ok) throw new Error(`OpenRouter ${res.status}: ${await res.text().catch(() => "")}`);
1602
+ const data = await res.json();
1603
+ return (data.data ?? []).map(normalize).filter((m) => m.id.length > 0);
1604
+ } finally {
1605
+ clearTimeout(timer);
1606
+ }
1607
+ }
1608
+ function normalize(m) {
1609
+ const promptPrice = toPerMillion(m.pricing?.prompt);
1610
+ const completionPrice = toPerMillion(m.pricing?.completion);
1611
+ return {
1612
+ id: m.id ?? "",
1613
+ name: m.name ?? m.id ?? "",
1614
+ promptPrice,
1615
+ completionPrice,
1616
+ contextLength: m.context_length ?? 0,
1617
+ supportsTools: (m.supported_parameters ?? []).includes("tools"),
1618
+ free: promptPrice === 0 && completionPrice === 0
1619
+ };
1620
+ }
1621
+ function toPerMillion(price) {
1622
+ const n = Number(price ?? "0");
1623
+ return Number.isFinite(n) ? n * 1e6 : 0;
1624
+ }
1625
+ function filterModels(models2, f) {
1626
+ const term = f.search?.trim().toLowerCase();
1627
+ let out = models2.filter((m) => {
1628
+ if (term && !m.id.toLowerCase().includes(term) && !m.name.toLowerCase().includes(term)) {
1629
+ return false;
1630
+ }
1631
+ if (f.tools === "tools" && !m.supportsTools) return false;
1632
+ if (f.tools === "no-tools" && m.supportsTools) return false;
1633
+ if (f.freeOnly && !m.free) return false;
1634
+ if (f.maxPrice !== void 0 && (m.promptPrice < 0 || m.promptPrice > f.maxPrice)) return false;
1635
+ return true;
1636
+ });
1637
+ const key = (m) => m.promptPrice < 0 ? Number.POSITIVE_INFINITY : m.promptPrice;
1638
+ const sort = f.sort ?? "price";
1639
+ out = out.sort((a, b) => {
1640
+ switch (sort) {
1641
+ case "price-desc":
1642
+ return key(b) - key(a);
1643
+ case "context":
1644
+ return b.contextLength - a.contextLength;
1645
+ case "name":
1646
+ return a.id.localeCompare(b.id);
1647
+ default:
1648
+ return key(a) - key(b);
1649
+ }
1650
+ });
1651
+ return out;
1652
+ }
1653
+ function fmtPrice(perMillion) {
1654
+ if (perMillion < 0) return "var";
1655
+ if (perMillion === 0) return "free";
1656
+ if (perMillion < 1) return `$${perMillion.toFixed(2)}`;
1657
+ if (perMillion < 100) return `$${perMillion.toFixed(perMillion % 1 ? 1 : 0)}`;
1658
+ return `$${Math.round(perMillion)}`;
1659
+ }
1660
+ function fmtContext(n) {
1661
+ if (n >= 1e6) return `${Math.round(n / 1e5) / 10}M`;
1662
+ if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
1663
+ return String(n);
1664
+ }
1665
+
1666
+ // src/ui/wizard.ts
1667
+ function bail(value) {
1668
+ if (p.isCancel(value)) {
1669
+ p.cancel(t("wizard.cancelled"));
1670
+ process.exit(0);
1671
+ }
1672
+ }
1673
+ async function runWizard() {
1674
+ const config = await loadConfig();
1675
+ const locale = await p.select({
1676
+ message: "Interface language / Idioma da interface",
1677
+ options: LOCALES.map((l) => ({ value: l, label: LOCALE_NAMES[l] })),
1678
+ initialValue: config.locale
1679
+ });
1680
+ bail(locale);
1681
+ config.locale = Locale.parse(locale);
1682
+ setLocale(config.locale);
1683
+ p.intro(pc4.bgCyan(pc4.black(t("wizard.title"))));
1684
+ p.note(t("wizard.intro"), t("wizard.welcome"));
1685
+ for (; ; ) {
1686
+ const agent = await promptAgent(config.agents.map((a) => a.name));
1687
+ config.agents.push(agent);
1688
+ const again = await p.confirm({ message: t("wizard.addAnother"), initialValue: false });
1689
+ bail(again);
1690
+ if (!again) break;
1691
+ }
1692
+ if (config.agents.length > 1) {
1693
+ const def = await p.select({
1694
+ message: t("wizard.defaultAgent"),
1695
+ options: config.agents.map((a) => ({ value: a.name, label: a.name }))
1696
+ });
1697
+ bail(def);
1698
+ config.defaultAgent = def;
1699
+ } else {
1700
+ config.defaultAgent = config.agents[0]?.name;
1701
+ }
1702
+ const mode = await p.select({
1703
+ message: t("wizard.permMode"),
1704
+ options: [
1705
+ { value: "review", label: t("wizard.permReview") },
1706
+ { value: "plan", label: t("wizard.permPlan") },
1707
+ { value: "bypass", label: t("wizard.permBypass") }
1708
+ ],
1709
+ initialValue: "review"
1710
+ });
1711
+ bail(mode);
1712
+ config.permissions.mode = PermissionMode.parse(mode);
1713
+ const allow = await p.text({
1714
+ message: t("wizard.allowPaths"),
1715
+ initialValue: config.permissions.allow.join(", "),
1716
+ placeholder: "**/*"
1717
+ });
1718
+ bail(allow);
1719
+ config.permissions.allow = String(allow).split(",").map((s) => s.trim()).filter(Boolean);
1720
+ await saveConfig(config);
1721
+ p.outro(pc4.green(t("wizard.saved", { n: config.agents.length, path: configPath() })));
1722
+ console.log(pc4.dim(t("wizard.next")));
1723
+ }
1724
+ async function addAgentInteractive() {
1725
+ const config = await loadConfig();
1726
+ const agent = await promptAgent(config.agents.map((a) => a.name));
1727
+ config.agents.push(agent);
1728
+ if (!config.defaultAgent) config.defaultAgent = agent.name;
1729
+ await saveConfig(config);
1730
+ return agent.name;
1731
+ }
1732
+ async function promptAgent(existingNames) {
1733
+ const provider = await p.select({
1734
+ message: t("wizard.provider"),
1735
+ options: [
1736
+ { value: "openrouter", label: t("wizard.providerOpenrouter") },
1737
+ { value: "ollama", label: t("wizard.providerOllama") },
1738
+ { value: "openai-compatible", label: t("wizard.providerCompatible") },
1739
+ { value: "anthropic", label: t("wizard.providerAnthropic") }
1740
+ ]
1741
+ });
1742
+ bail(provider);
1743
+ const providerKind = ProviderKind.parse(provider);
1744
+ const name = await p.text({
1745
+ message: t("wizard.agentName"),
1746
+ placeholder: providerKind,
1747
+ validate: (v) => {
1748
+ if (!v.trim()) return t("wizard.required");
1749
+ if (existingNames.includes(v.trim())) return t("wizard.nameTaken");
1750
+ return void 0;
1751
+ }
1752
+ });
1753
+ bail(name);
1754
+ const model = await promptModel(providerKind);
1755
+ let baseUrl = DEFAULT_BASE_URL[providerKind];
1756
+ if (providerKind === "openai-compatible") {
1757
+ const b = await p.text({
1758
+ message: t("wizard.baseUrl"),
1759
+ placeholder: "https://my-gateway.example.com/v1",
1760
+ validate: (v) => v.trim() ? void 0 : t("wizard.baseUrlRequired")
1761
+ });
1762
+ bail(b);
1763
+ baseUrl = String(b).trim();
1764
+ }
1765
+ const apiKey = await promptApiKey(providerKind);
1766
+ const toolMode = await p.select({
1767
+ message: t("wizard.toolMode"),
1768
+ options: [
1769
+ { value: "auto", label: t("wizard.toolAuto") },
1770
+ { value: "native", label: t("wizard.toolNative") },
1771
+ { value: "emulated", label: t("wizard.toolEmulated") }
1772
+ ],
1773
+ initialValue: "auto"
1774
+ });
1775
+ bail(toolMode);
1776
+ return AgentConfig.parse({
1777
+ name: String(name).trim(),
1778
+ provider: providerKind,
1779
+ model: String(model).trim(),
1780
+ baseUrl,
1781
+ apiKey,
1782
+ toolMode: ToolMode.parse(toolMode)
1783
+ });
1784
+ }
1785
+ var OTHER = "__other__";
1786
+ var REFILTER = "__refilter__";
1787
+ var MANUAL = "__manual__";
1788
+ async function promptModel(provider) {
1789
+ if (provider === "openrouter") {
1790
+ const picked = await browseOpenRouter();
1791
+ if (picked) return picked;
1792
+ }
1793
+ if (provider === "ollama") {
1794
+ const spin = p.spinner();
1795
+ spin.start(t("wizard.ollamaDetecting"));
1796
+ const models2 = await listOllamaModels();
1797
+ spin.stop(models2.length ? t("wizard.ollamaFound", { n: models2.length }) : t("wizard.ollamaNone"));
1798
+ if (models2.length > 0) {
1799
+ const choice = await p.select({
1800
+ message: t("wizard.ollamaPick"),
1801
+ options: [
1802
+ ...models2.map((m) => ({ value: m, label: m })),
1803
+ { value: OTHER, label: t("wizard.ollamaOther") }
1804
+ ]
1805
+ });
1806
+ bail(choice);
1807
+ if (choice !== OTHER) return choice;
1808
+ }
1809
+ }
1810
+ const placeholder = provider === "openrouter" ? "anthropic/claude-3.5-sonnet" : provider === "ollama" ? "llama3.1:8b" : provider === "anthropic" ? "claude-3-5-sonnet-latest" : "gpt-4o-mini";
1811
+ const model = await p.text({
1812
+ message: t("wizard.modelId"),
1813
+ placeholder,
1814
+ validate: (v) => v.trim() ? void 0 : t("wizard.required")
1815
+ });
1816
+ bail(model);
1817
+ return String(model).trim();
1818
+ }
1819
+ async function browseOpenRouter() {
1820
+ const spin = p.spinner();
1821
+ spin.start(t("models.fetching"));
1822
+ let all;
1823
+ try {
1824
+ all = await listOpenRouterModels(process.env.OPENROUTER_API_KEY);
1825
+ spin.stop(t("models.shown", { shown: all.length, total: all.length }));
1826
+ } catch {
1827
+ spin.stop(t("wizard.orError"));
1828
+ return void 0;
1829
+ }
1830
+ for (; ; ) {
1831
+ const search = await p.text({ message: t("wizard.orSearch"), placeholder: "claude, qwen, gpt\u2026" });
1832
+ bail(search);
1833
+ const flags = await p.multiselect({
1834
+ message: t("wizard.orFilters"),
1835
+ options: [
1836
+ { value: "tools", label: t("wizard.orToolsOnly") },
1837
+ { value: "free", label: t("wizard.orFreeOnly") }
1838
+ ],
1839
+ initialValues: ["tools"],
1840
+ required: false
1841
+ });
1842
+ bail(flags);
1843
+ const sort = await p.select({
1844
+ message: t("wizard.orSort"),
1845
+ initialValue: "price",
1846
+ options: [
1847
+ { value: "price", label: t("wizard.orSortPrice") },
1848
+ { value: "price-desc", label: t("wizard.orSortPriceDesc") },
1849
+ { value: "context", label: t("wizard.orSortContext") },
1850
+ { value: "name", label: t("wizard.orSortName") }
1851
+ ]
1852
+ });
1853
+ bail(sort);
1854
+ const flagList = flags;
1855
+ const results = filterModels(all, {
1856
+ search: String(search ?? ""),
1857
+ tools: flagList.includes("tools") ? "tools" : "any",
1858
+ freeOnly: flagList.includes("free"),
1859
+ sort
1860
+ });
1861
+ if (results.length === 0) {
1862
+ p.note(t("wizard.orNone"));
1863
+ continue;
1864
+ }
1865
+ const choice = await p.select({
1866
+ message: t("wizard.orPick", { n: results.length }),
1867
+ maxItems: 12,
1868
+ options: [
1869
+ ...results.slice(0, 40).map((m) => ({
1870
+ value: m.id,
1871
+ label: m.id,
1872
+ hint: `${fmtPrice(m.promptPrice)}/${fmtPrice(m.completionPrice)}${m.supportsTools ? " \xB7 \u{1F6E0}" : ""} \xB7 ${fmtContext(m.contextLength)}`
1873
+ })),
1874
+ { value: REFILTER, label: t("wizard.orRefilter") },
1875
+ { value: MANUAL, label: t("wizard.orManual") }
1876
+ ]
1877
+ });
1878
+ bail(choice);
1879
+ if (choice === REFILTER) continue;
1880
+ if (choice === MANUAL) return void 0;
1881
+ return choice;
1882
+ }
1883
+ }
1884
+ async function promptApiKey(provider) {
1885
+ if (!REQUIRES_API_KEY[provider]) {
1886
+ const need = await p.confirm({
1887
+ message: t("wizard.keyNotNeeded", { provider }),
1888
+ initialValue: false
1889
+ });
1890
+ bail(need);
1891
+ if (!need) return void 0;
1892
+ }
1893
+ const method = await p.select({
1894
+ message: t("wizard.apiKey"),
1895
+ options: [
1896
+ { value: "env", label: t("wizard.keyEnv") },
1897
+ { value: "inline", label: t("wizard.keyInline") },
1898
+ { value: "none", label: t("wizard.keySkip") }
1899
+ ],
1900
+ initialValue: "env"
1901
+ });
1902
+ bail(method);
1903
+ if (method === "none") return void 0;
1904
+ if (method === "env") {
1905
+ const envName = await p.text({
1906
+ message: t("wizard.envName"),
1907
+ initialValue: SUGGESTED_KEY_ENV[provider] ?? "OPENAI_API_KEY",
1908
+ validate: (v) => /^[A-Z0-9_]+$/i.test(v.trim()) ? void 0 : t("wizard.envInvalid")
1909
+ });
1910
+ bail(envName);
1911
+ return `\${${String(envName).trim()}}`;
1912
+ }
1913
+ const key = await p.password({
1914
+ message: t("wizard.keyPrompt"),
1915
+ validate: (v) => v.trim() ? void 0 : t("wizard.required")
1916
+ });
1917
+ bail(key);
1918
+ return String(key).trim();
1919
+ }
1920
+
1921
+ // src/ui/banner.ts
1922
+ import pc5 from "picocolors";
1923
+ var RESET = "\x1B[0m";
1924
+ var useColor = (Boolean(process.stdout.isTTY) || Boolean(process.env.FORCE_COLOR)) && !process.env.NO_COLOR;
1925
+ var animated = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && !process.env.POLYPUS_NO_ANIM;
1926
+ var AUTHOR = {
1927
+ name: "Gabriel Rios",
1928
+ github: "github.com/GaberRB",
1929
+ linkedin: "linkedin.com/in/gabriel-riosb"
1930
+ };
1931
+ function rgb(r, g, b, bold = false) {
1932
+ return (s) => useColor ? `\x1B[${bold ? "1;" : ""}38;2;${r};${g};${b}m${s}${RESET}` : s;
1933
+ }
1934
+ var c0 = rgb(224, 209, 255);
1935
+ var c1 = rgb(199, 176, 253);
1936
+ var c2 = rgb(171, 142, 250);
1937
+ var c3 = rgb(146, 102, 245);
1938
+ var c4 = rgb(122, 74, 222);
1939
+ var lens = rgb(255, 255, 255, true);
1940
+ var blush = rgb(255, 138, 190, true);
1941
+ var GRAD = [c0, c1, c1, c2, c2, c2, c3, c3, c3, c4, c4];
1942
+ var ART = [
1943
+ " \u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584 ",
1944
+ " \u259F\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2599 ",
1945
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ",
1946
+ " \u2588\u2588\u2665 (\u25C9) \u203F (\u25C9) \u2665\u2588\u2588 ",
1947
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ",
1948
+ " \u259C\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u259B ",
1949
+ " \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 ",
1950
+ " \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 ",
1951
+ " \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 ",
1952
+ " \u2570\u255D \u255A\u255D \u255A\u255D \u255A\u255D \u255A\u256E "
1953
+ ];
1954
+ var TENTACLE_FRAMES = [
1955
+ " \u2570\u255D \u255A\u255D \u255A\u255D \u255A\u255D \u255A\u256E ",
1956
+ " \u255A\u256E \u2570\u255D \u255A\u255D \u2570\u255D \u255A\u255D ",
1957
+ " \u2570\u255D \u255A\u255D \u2570\u255D \u255A\u255D \u255A\u256E "
1958
+ ];
1959
+ var WIDTH = Math.max(...[...ART].map((l) => [...l].length));
1960
+ function center(line) {
1961
+ const pad = WIDTH - [...line].length;
1962
+ const left = Math.floor(pad / 2);
1963
+ return " ".repeat(left) + line + " ".repeat(pad - left);
1964
+ }
1965
+ var GLYPHS = {
1966
+ P: ["\u2588\u2588\u2588\u2588\u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588\u2588\u2588\u2588\u2588", "\u2588\u2588 ", "\u2588\u2588 "],
1967
+ O: ["\u2588\u2588\u2588\u2588\u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588\u2588\u2588\u2588\u2588"],
1968
+ L: ["\u2588\u2588 ", "\u2588\u2588 ", "\u2588\u2588 ", "\u2588\u2588 ", "\u2588\u2588\u2588\u2588\u2588\u2588"],
1969
+ Y: ["\u2588\u2588 \u2588\u2588", "\u2588\u2588 \u2588\u2588", " \u2588\u2588\u2588\u2588 ", " \u2588\u2588 ", " \u2588\u2588 "],
1970
+ U: ["\u2588\u2588 \u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588 \u2588\u2588", "\u2588\u2588\u2588\u2588\u2588\u2588"],
1971
+ S: ["\u2588\u2588\u2588\u2588\u2588\u2588", "\u2588\u2588 ", "\u2588\u2588\u2588\u2588\u2588\u2588", " \u2588\u2588", "\u2588\u2588\u2588\u2588\u2588\u2588"]
1972
+ };
1973
+ var WORD_GRAD = [
1974
+ rgb(199, 176, 253, true),
1975
+ rgb(171, 142, 250, true),
1976
+ rgb(171, 142, 250, true),
1977
+ rgb(146, 102, 245, true),
1978
+ rgb(146, 102, 245, true)
1979
+ ];
1980
+ function wordmarkLines(text2) {
1981
+ const letters = [...text2].map((ch) => GLYPHS[ch]).filter(Boolean);
1982
+ return [0, 1, 2, 3, 4].map(
1983
+ (row) => (WORD_GRAD[row] ?? c3)(letters.map((g) => g[row]).join(" "))
1984
+ );
1985
+ }
1986
+ function colorChar(rowIdx, ch) {
1987
+ if (ch === " ") return ch;
1988
+ if (ch === "\u25C9" || ch === "(" || ch === ")") return lens(ch);
1989
+ if (ch === "\u2665") return blush(ch);
1990
+ if (ch === "\u203F") return c0(ch);
1991
+ return (GRAD[rowIdx] ?? c3)(ch);
1992
+ }
1993
+ function renderArtRow(rowIdx, line) {
1994
+ return [...center(line)].map((ch) => colorChar(rowIdx, ch)).join("");
1995
+ }
1996
+ var tagline = () => c1(t("welcome.tagline")) + pc5.dim(" v0.1.0");
1997
+ function authorLine() {
1998
+ return pc5.dim("by ") + c2(AUTHOR.name) + pc5.dim(" \xB7 ") + c1(AUTHOR.github) + pc5.dim(" \xB7 ") + c1(AUTHOR.linkedin);
1999
+ }
2000
+ function banner() {
2001
+ const art = ART.map((line, i) => renderArtRow(i, line)).join("\n");
2002
+ const word = wordmarkLines("POLYPUS").join("\n");
2003
+ return `
2004
+ ${art}
2005
+
2006
+ ${word}
2007
+
2008
+ ${tagline()}
2009
+ ${authorLine()}
2010
+ `;
2011
+ }
2012
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2013
+ async function animateIntro() {
2014
+ if (!animated) {
2015
+ console.log(banner());
2016
+ return;
2017
+ }
2018
+ process.stdout.write("\n");
2019
+ for (let i = 0; i < ART.length; i++) {
2020
+ console.log(renderArtRow(i, ART[i]));
2021
+ await sleep(45);
2022
+ }
2023
+ const last = ART.length - 1;
2024
+ for (let k = 0; k < 6; k++) {
2025
+ const frame = renderArtRow(last, TENTACLE_FRAMES[k % TENTACLE_FRAMES.length]);
2026
+ process.stdout.write(`\x1B[1A\r\x1B[K${frame}
2027
+ `);
2028
+ await sleep(85);
2029
+ }
2030
+ process.stdout.write("\n");
2031
+ for (const line of wordmarkLines("POLYPUS")) {
2032
+ console.log(line);
2033
+ await sleep(40);
2034
+ }
2035
+ process.stdout.write("\n");
2036
+ console.log(" " + tagline());
2037
+ await sleep(140);
2038
+ console.log(" " + authorLine());
2039
+ console.log("");
2040
+ }
2041
+ async function printWelcome(info) {
2042
+ await animateIntro();
2043
+ const bar = c2("\u2502 ");
2044
+ const label = (s) => pc5.dim(s.padEnd(8));
2045
+ const rule = c3(" " + "\u2500".repeat(46));
2046
+ console.log(rule);
2047
+ console.log(" " + bar + label(t("welcome.agent")) + pc5.bold(info.agentName));
2048
+ console.log(
2049
+ " " + bar + label(t("welcome.model")) + `${info.model} ` + pc5.dim(`(${info.provider} \xB7 ${info.toolMode})`)
2050
+ );
2051
+ console.log(" " + bar + label(t("welcome.mode")) + modeColor(info.mode));
2052
+ console.log(" " + bar + label(t("welcome.workspace")) + pc5.dim(info.workspace));
2053
+ console.log(rule);
2054
+ console.log(" " + pc5.dim(t("welcome.hints")) + "\n");
2055
+ }
2056
+ function modeColor(mode) {
2057
+ if (mode === "bypass") return pc5.yellow(mode);
2058
+ if (mode === "plan") return pc5.cyan(mode);
2059
+ return pc5.green(mode);
2060
+ }
2061
+ function promptLabel(mode) {
2062
+ return c2("\u{1F419} polypus") + pc5.dim(`(${mode})`) + c3(" \u203A ");
2063
+ }
2064
+
2065
+ // src/ui/repl.ts
2066
+ async function startRepl(ctx) {
2067
+ for (; ; ) {
2068
+ const line = await readLine(promptLabel(ctx.session.mode));
2069
+ if (line === null) break;
2070
+ const trimmed = line.trim();
2071
+ if (!trimmed) continue;
2072
+ if (!trimmed.startsWith("/")) {
2073
+ await ctx.runTask(trimmed);
2074
+ continue;
2075
+ }
2076
+ const [cmd = "", ...rest] = trimmed.slice(1).split(/\s+/);
2077
+ const arg = rest.join(" ").trim();
2078
+ if (cmd === "exit" || cmd === "quit") break;
2079
+ if (cmd === "add") {
2080
+ const name = await addAgentInteractive().catch((e) => {
2081
+ console.log(pc6.red(`\u2717 ${e.message}`));
2082
+ return void 0;
2083
+ });
2084
+ await ctx.reload();
2085
+ if (name) {
2086
+ ctx.session.agentName = name;
2087
+ console.log(pc6.green(t("repl.switchedTo", { name })));
2088
+ }
2089
+ continue;
2090
+ }
2091
+ await handleCommand(cmd, arg, ctx);
2092
+ }
2093
+ }
2094
+ async function readLine(prompt) {
2095
+ const rl = readline.createInterface({ input: stdin, output: stdout });
2096
+ try {
2097
+ return await rl.question(prompt);
2098
+ } catch {
2099
+ return null;
2100
+ } finally {
2101
+ rl.close();
2102
+ }
2103
+ }
2104
+ async function handleCommand(cmd, arg, ctx) {
2105
+ const { session } = ctx;
2106
+ switch (cmd) {
2107
+ case "help":
2108
+ console.log(t("repl.help"));
2109
+ return;
2110
+ case "plan":
2111
+ case "review":
2112
+ case "bypass":
2113
+ session.mode = cmd;
2114
+ console.log(pc6.dim(t("repl.modeChanged", { mode: cmd })));
2115
+ return;
2116
+ case "allow":
2117
+ if (arg) {
2118
+ session.allow = [...session.allow, arg];
2119
+ console.log(pc6.dim(t("repl.allowAdded", { glob: arg })));
2120
+ } else {
2121
+ console.log(pc6.dim(t("repl.allowShow", { mode: session.mode, allow: session.allow.join(", ") })));
2122
+ }
2123
+ return;
2124
+ case "reset":
2125
+ session.history = [];
2126
+ console.log(pc6.dim(t("repl.historyCleared")));
2127
+ return;
2128
+ case "agents":
2129
+ printAgents(ctx.getConfig(), session.agentName);
2130
+ return;
2131
+ case "agent": {
2132
+ if (!arg) {
2133
+ console.log(pc6.yellow(t("repl.needName", { usage: "/agent <name>" })));
2134
+ return;
2135
+ }
2136
+ if (!findAgent(ctx.getConfig(), arg)) {
2137
+ console.log(pc6.red(t("agent.notFound", { name: arg })));
2138
+ return;
2139
+ }
2140
+ session.agentName = arg;
2141
+ console.log(pc6.green(t("repl.agentSwitched", { name: arg })));
2142
+ return;
2143
+ }
2144
+ case "remove": {
2145
+ if (!arg) {
2146
+ console.log(pc6.yellow(t("repl.needName", { usage: "/remove <name>" })));
2147
+ return;
2148
+ }
2149
+ await removeAgent2(arg, ctx);
2150
+ return;
2151
+ }
2152
+ default:
2153
+ console.log(pc6.yellow(t("repl.unknown", { cmd })));
2154
+ }
2155
+ }
2156
+ function printAgents(config, activeName) {
2157
+ if (config.agents.length === 0) {
2158
+ console.log(pc6.yellow(t("agent.none")));
2159
+ return;
2160
+ }
2161
+ console.log(pc6.bold(t("agent.listHeader")));
2162
+ for (const a of config.agents) {
2163
+ const active = a.name === activeName;
2164
+ console.log(
2165
+ ` ${active ? pc6.green("\u25CF") : pc6.dim("\u25CB")} ${pc6.bold(a.name)} ` + pc6.dim(`(${a.provider} \xB7 ${a.model} \xB7 ${a.toolMode})`) + (config.defaultAgent === a.name ? pc6.dim(` [${t("common.default")}]`) : "")
2166
+ );
2167
+ }
2168
+ }
2169
+ async function removeAgent2(name, ctx) {
2170
+ const config = await loadConfig();
2171
+ if (!findAgent(config, name)) {
2172
+ console.log(pc6.red(t("agent.notFound", { name })));
2173
+ return;
2174
+ }
2175
+ config.agents = config.agents.filter((a) => a.name !== name);
2176
+ if (config.defaultAgent === name) config.defaultAgent = config.agents[0]?.name;
2177
+ await saveConfig(config);
2178
+ await ctx.reload();
2179
+ console.log(pc6.green(t("agent.removed", { name })));
2180
+ if (ctx.session.agentName === name) {
2181
+ const next = config.defaultAgent ?? config.agents[0]?.name;
2182
+ if (next) {
2183
+ ctx.session.agentName = next;
2184
+ console.log(pc6.dim(t("repl.switchedTo", { name: next })));
2185
+ } else {
2186
+ console.log(pc6.yellow(t("repl.noAgentsLeft")));
2187
+ }
2188
+ }
2189
+ }
2190
+
2191
+ // src/ui/spinner.ts
2192
+ var RESET2 = "\x1B[0m";
2193
+ var isTTY = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
2194
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2195
+ var violet = (s) => isTTY ? `\x1B[38;2;167;139;250m${s}${RESET2}` : s;
2196
+ var dim = (s) => isTTY ? `\x1B[2m${s}${RESET2}` : s;
2197
+ var Spinner = class {
2198
+ timer;
2199
+ frame = 0;
2200
+ startedAt = 0;
2201
+ label = "";
2202
+ suffix = "";
2203
+ /** Extra dim text appended after the elapsed time (e.g. token count). */
2204
+ setSuffix(suffix) {
2205
+ this.suffix = suffix;
2206
+ }
2207
+ /** Start (or, if already running, just update the label). */
2208
+ start(label) {
2209
+ this.label = label;
2210
+ if (!isTTY) return;
2211
+ if (this.timer) return;
2212
+ this.startedAt = Date.now();
2213
+ this.render();
2214
+ this.timer = setInterval(() => this.render(), 90);
2215
+ this.timer.unref?.();
2216
+ }
2217
+ /** Erase the spinner line and stop animating. */
2218
+ stop() {
2219
+ if (this.timer) {
2220
+ clearInterval(this.timer);
2221
+ this.timer = void 0;
2222
+ }
2223
+ if (isTTY) process.stdout.write("\r\x1B[K");
2224
+ }
2225
+ render() {
2226
+ const f = violet(FRAMES[this.frame = (this.frame + 1) % FRAMES.length]);
2227
+ const secs = Math.floor((Date.now() - this.startedAt) / 1e3);
2228
+ const time = secs > 0 ? dim(` (${secs}s)`) : "";
2229
+ const suffix = this.suffix ? dim(` \xB7 ${this.suffix}`) : "";
2230
+ process.stdout.write(`\r\x1B[K${f} \u{1F419} ${dim(this.label + "\u2026")}${time}${suffix}`);
2231
+ }
2232
+ };
2233
+
2234
+ // src/cli/commands/run.ts
2235
+ async function run(task, opts) {
2236
+ let config = await loadConfig();
2237
+ const agentConfig = resolveAgent(config, opts.agent);
2238
+ const workspace = process.cwd();
2239
+ const session = {
2240
+ agentName: agentConfig.name,
2241
+ mode: opts.mode ?? config.permissions.mode,
2242
+ allow: config.permissions.allow,
2243
+ deny: config.permissions.deny,
2244
+ allowedCommands: config.permissions.allowedCommands,
2245
+ maxSteps: opts.maxSteps ? Number(opts.maxSteps) : void 0,
2246
+ history: []
2247
+ };
2248
+ const runTask = async (taskText) => {
2249
+ const active = resolveAgent(config, session.agentName);
2250
+ const resolved2 = createProvider(active);
2251
+ await executeTask(taskText, resolved2, workspace, session);
2252
+ };
2253
+ if (task) {
2254
+ const resolved2 = createProvider(agentConfig);
2255
+ console.log(
2256
+ pc7.dim(
2257
+ t("run.status", {
2258
+ name: resolved2.config.name,
2259
+ provider: resolved2.config.provider,
2260
+ model: resolved2.config.model,
2261
+ toolMode: resolved2.toolMode,
2262
+ mode: session.mode
2263
+ })
2264
+ )
2265
+ );
2266
+ await executeTask(task, resolved2, workspace, session);
2267
+ return;
2268
+ }
2269
+ const resolved = createProvider(agentConfig);
2270
+ await printWelcome({
2271
+ agentName: resolved.config.name,
2272
+ provider: resolved.config.provider,
2273
+ model: resolved.config.model,
2274
+ toolMode: resolved.toolMode,
2275
+ mode: session.mode,
2276
+ workspace
2277
+ });
2278
+ const ctx = {
2279
+ session,
2280
+ runTask,
2281
+ getConfig: () => config,
2282
+ reload: async () => {
2283
+ config = await loadConfig();
2284
+ }
2285
+ };
2286
+ await startRepl(ctx);
2287
+ }
2288
+ async function executeTask(task, resolved, workspace, session) {
2289
+ const spinner3 = new Spinner();
2290
+ const controller = new AbortController();
2291
+ const cancel2 = listenForCancel(controller);
2292
+ const permissions = new PermissionEngine({
2293
+ mode: session.mode,
2294
+ policy: { workspace, allow: session.allow, deny: session.deny },
2295
+ allowedCommands: session.allowedCommands,
2296
+ confirm: async (req) => {
2297
+ spinner3.stop();
2298
+ cancel2.pause();
2299
+ const ok = await confirmAction(req);
2300
+ cancel2.resume();
2301
+ return ok;
2302
+ }
2303
+ });
2304
+ spinner3.start(t("ui.thinking"));
2305
+ let result;
2306
+ try {
2307
+ result = await runAgent({
2308
+ task,
2309
+ workspace,
2310
+ agent: resolved,
2311
+ permissions,
2312
+ promptContext: { workspace, mode: session.mode, allow: session.allow },
2313
+ history: session.history,
2314
+ maxSteps: session.maxSteps,
2315
+ signal: controller.signal,
2316
+ events: renderEvents(spinner3)
2317
+ });
2318
+ } finally {
2319
+ spinner3.stop();
2320
+ cancel2.dispose();
2321
+ }
2322
+ session.history = result.messages;
2323
+ if (result.reason === "finished") {
2324
+ console.log(pc7.green("\n" + t("run.done", { steps: result.steps })) + (result.summary ? ` ${result.summary}` : ""));
2325
+ } else if (result.reason === "cancelled") {
2326
+ console.log(pc7.dim("\n" + t("run.cancelled")));
2327
+ } else if (result.reason === "stalled" || result.reason === "maxsteps") {
2328
+ console.log(pc7.yellow("\n" + t("run.stopped", { steps: result.steps })));
2329
+ }
2330
+ if (result.usage.promptTokens || result.usage.completionTokens) {
2331
+ const total = result.usage.promptTokens + result.usage.completionTokens;
2332
+ console.log(
2333
+ pc7.dim(
2334
+ "\u21B3 " + t("ui.tokens", {
2335
+ total: fmtTokens(total),
2336
+ in: fmtTokens(result.usage.promptTokens),
2337
+ out: fmtTokens(result.usage.completionTokens)
2338
+ })
2339
+ )
2340
+ );
2341
+ }
2342
+ }
2343
+ function fmtTokens(n) {
2344
+ return n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n);
2345
+ }
2346
+ function listenForCancel(controller) {
2347
+ const stdin2 = process.stdin;
2348
+ if (!stdin2.isTTY) return { pause() {
2349
+ }, resume() {
2350
+ }, dispose() {
2351
+ } };
2352
+ const onData = (buf) => {
2353
+ if (buf.length === 1 && (buf[0] === 27 || buf[0] === 3)) controller.abort();
2354
+ };
2355
+ let active = false;
2356
+ const attach = () => {
2357
+ if (active) return;
2358
+ stdin2.setRawMode(true);
2359
+ stdin2.resume();
2360
+ stdin2.on("data", onData);
2361
+ active = true;
2362
+ };
2363
+ const detach = () => {
2364
+ if (!active) return;
2365
+ stdin2.off("data", onData);
2366
+ stdin2.setRawMode(false);
2367
+ stdin2.pause();
2368
+ active = false;
2369
+ };
2370
+ attach();
2371
+ return { pause: detach, resume: attach, dispose: detach };
2372
+ }
2373
+ async function confirmAction(req) {
2374
+ if (req.preview) console.log(pc7.dim(req.preview));
2375
+ const answer = await p2.confirm({ message: t("run.confirm", { summary: req.summary }) });
2376
+ if (p2.isCancel(answer)) return false;
2377
+ return answer === true;
2378
+ }
2379
+ function renderEvents(spinner3) {
2380
+ return {
2381
+ onStep() {
2382
+ spinner3.start(t("ui.thinking"));
2383
+ },
2384
+ onUsage(usage) {
2385
+ const total = usage.promptTokens + usage.completionTokens;
2386
+ if (total > 0) spinner3.setSuffix(t("ui.tokensShort", { total: fmtTokens(total) }));
2387
+ },
2388
+ onAssistantText(text2) {
2389
+ spinner3.stop();
2390
+ if (text2.trim()) console.log(pc7.cyan(text2.trim()));
2391
+ },
2392
+ onToolCall(call) {
2393
+ spinner3.stop();
2394
+ const arg = call.name === "run_command" ? call.arguments.command : call.arguments.path;
2395
+ console.log(pc7.dim(` \u2192 ${call.name}${arg ? ` ${String(arg)}` : ""}`));
2396
+ spinner3.start(t("ui.running", { tool: call.name }));
2397
+ },
2398
+ onToolResult(_call, result) {
2399
+ spinner3.stop();
2400
+ const head = result.output.split("\n")[0] ?? "";
2401
+ console.log((result.ok ? pc7.green(" \u2713 ") : pc7.red(" \u2717 ")) + pc7.dim(head.slice(0, 120)));
2402
+ },
2403
+ onReprompt(attempt) {
2404
+ spinner3.stop();
2405
+ console.log(pc7.yellow(" " + t("run.reprompt", { attempt })));
2406
+ }
2407
+ };
2408
+ }
2409
+
2410
+ // src/cli/commands/setup.ts
2411
+ async function setup() {
2412
+ await runWizard();
2413
+ }
2414
+
2415
+ // src/cli/commands/swarm.ts
2416
+ import pc8 from "picocolors";
2417
+
2418
+ // src/core/git/worktree.ts
2419
+ import { mkdtemp } from "fs/promises";
2420
+ import { tmpdir } from "os";
2421
+ import { join as join2 } from "path";
2422
+ import { simpleGit } from "simple-git";
2423
+ async function ensureRepo(workspace) {
2424
+ const git = simpleGit(workspace);
2425
+ if (!await git.checkIsRepo()) {
2426
+ await git.init();
2427
+ }
2428
+ const identity = await identityArgs(git);
2429
+ const hasHead = await git.raw(["rev-parse", "--verify", "HEAD"]).then(() => true).catch(() => false);
2430
+ if (!hasHead) {
2431
+ await git.raw([...identity, "commit", "--allow-empty", "-m", "polypus: initial commit"]);
2432
+ }
2433
+ return git;
2434
+ }
2435
+ async function identityArgs(git) {
2436
+ const email = await git.raw(["config", "user.email"]).catch(() => "");
2437
+ if (email.trim()) return [];
2438
+ return ["-c", "user.email=polypus@local", "-c", "user.name=Polypus"];
2439
+ }
2440
+ async function createWorktree(git, label) {
2441
+ const branch = `polypus/${label}-${Date.now().toString(36)}`;
2442
+ const path = await mkdtemp(join2(tmpdir(), "polypus-wt-"));
2443
+ await git.raw(["worktree", "add", "-b", branch, path, "HEAD"]);
2444
+ return { path, branch };
2445
+ }
2446
+ async function commitWorktree(wt, message) {
2447
+ const wtGit = simpleGit(wt.path);
2448
+ await wtGit.add(["-A"]);
2449
+ const status = await wtGit.status();
2450
+ if (status.staged.length === 0 && status.files.length === 0) return false;
2451
+ const identity = await identityArgs(wtGit);
2452
+ await wtGit.raw([...identity, "commit", "-m", message]);
2453
+ return true;
2454
+ }
2455
+ async function mergeWorktreeBranch(git, branch) {
2456
+ try {
2457
+ const identity = await identityArgs(git);
2458
+ await git.raw([...identity, "merge", "--no-edit", branch]);
2459
+ return { branch, ok: true, conflicts: [] };
2460
+ } catch (err) {
2461
+ const status = await git.status().catch(() => void 0);
2462
+ const conflicts = status?.conflicted ?? [];
2463
+ await git.raw(["merge", "--abort"]).catch(() => void 0);
2464
+ if (conflicts.length === 0) {
2465
+ throw err;
2466
+ }
2467
+ return { branch, ok: false, conflicts };
2468
+ }
2469
+ }
2470
+ async function removeWorktree(git, wt) {
2471
+ await git.raw(["worktree", "remove", wt.path, "--force"]).catch(() => void 0);
2472
+ await git.raw(["branch", "-D", wt.branch]).catch(() => void 0);
2473
+ }
2474
+
2475
+ // src/core/agent/worker.ts
2476
+ async function runWorker(subtask, agent, wt, allow, deny, events) {
2477
+ const permissions = new PermissionEngine({
2478
+ mode: "bypass",
2479
+ policy: { workspace: wt.path, allow, deny },
2480
+ allowedCommands: []
2481
+ });
2482
+ const result = await runAgent({
2483
+ task: subtask.brief,
2484
+ workspace: wt.path,
2485
+ agent,
2486
+ permissions,
2487
+ promptContext: { workspace: wt.path, mode: "bypass", allow, briefing: subtask.brief },
2488
+ events
2489
+ });
2490
+ const committed = await commitWorktree(wt, `polypus(${subtask.id}): ${subtask.title}`);
2491
+ return {
2492
+ subtask,
2493
+ agentName: agent.config.name,
2494
+ branch: wt.branch,
2495
+ finished: result.finished,
2496
+ summary: result.summary,
2497
+ committed,
2498
+ steps: result.steps
2499
+ };
2500
+ }
2501
+
2502
+ // src/core/agent/orchestrator.ts
2503
+ async function runSwarm(opts) {
2504
+ const lead = opts.agents[0];
2505
+ if (!lead) throw new Error("Swarm requires at least one agent.");
2506
+ const maxSubtasks = opts.maxSubtasks ?? Math.max(opts.agents.length, 2);
2507
+ const git = await ensureRepo(opts.workspace);
2508
+ const subtasks = await decompose(lead, opts.task, maxSubtasks);
2509
+ opts.events?.onDecomposed?.(subtasks);
2510
+ const worktrees = [];
2511
+ for (const subtask of subtasks) {
2512
+ worktrees.push(await createWorktree(git, subtask.id));
2513
+ }
2514
+ const outcomes = await Promise.all(
2515
+ subtasks.map(async (subtask, i) => {
2516
+ const agent = opts.agents[i % opts.agents.length];
2517
+ const wt = worktrees[i];
2518
+ opts.events?.onWorkerStart?.(subtask, agent.config.name);
2519
+ const outcome = await runWorker(
2520
+ subtask,
2521
+ agent,
2522
+ wt,
2523
+ opts.allow,
2524
+ opts.deny,
2525
+ opts.events?.workerEvents?.(subtask)
2526
+ );
2527
+ opts.events?.onWorkerDone?.(outcome);
2528
+ return outcome;
2529
+ })
2530
+ );
2531
+ const merges = [];
2532
+ for (const outcome of outcomes) {
2533
+ if (!outcome.committed) continue;
2534
+ const merge = await mergeWorktreeBranch(git, outcome.branch);
2535
+ merges.push(merge);
2536
+ opts.events?.onMerge?.(merge);
2537
+ }
2538
+ const conflicted = new Set(merges.filter((m) => !m.ok).map((m) => m.branch));
2539
+ for (const wt of worktrees) {
2540
+ if (conflicted.has(wt.branch)) {
2541
+ await git.raw(["worktree", "remove", wt.path, "--force"]).catch(() => void 0);
2542
+ } else {
2543
+ await removeWorktree(git, wt);
2544
+ }
2545
+ }
2546
+ return { subtasks, outcomes, merges };
2547
+ }
2548
+ var DECOMPOSE_SYSTEM = [
2549
+ "You are a tech lead splitting a coding task into independent subtasks that can be done in parallel.",
2550
+ 'Return ONLY a JSON array. Each item: {"title": string, "brief": string}.',
2551
+ "Make subtasks touch DIFFERENT files/areas to minimize merge conflicts.",
2552
+ "Keep the list small (prefer 2-4 items). Each brief must be self-contained and actionable."
2553
+ ].join("\n");
2554
+ async function decompose(lead, task, maxSubtasks) {
2555
+ try {
2556
+ const res = await lead.provider.chat({
2557
+ messages: [
2558
+ { role: "system", content: DECOMPOSE_SYSTEM },
2559
+ { role: "user", content: `Task:
2560
+ ${task}
2561
+
2562
+ Return at most ${maxSubtasks} subtasks as a JSON array.` }
2563
+ ],
2564
+ params: { temperature: 0 }
2565
+ });
2566
+ const parsed = extractJsonArray(res.content);
2567
+ if (parsed && parsed.length > 0) {
2568
+ return parsed.slice(0, maxSubtasks).map((item, i) => ({
2569
+ id: `t${i + 1}`,
2570
+ title: String(item.title ?? `subtask ${i + 1}`),
2571
+ brief: String(item.brief ?? item.title ?? task)
2572
+ }));
2573
+ }
2574
+ } catch {
2575
+ }
2576
+ return [{ id: "t1", title: "task", brief: task }];
2577
+ }
2578
+ function extractJsonArray(text2) {
2579
+ const start = text2.indexOf("[");
2580
+ const end = text2.lastIndexOf("]");
2581
+ if (start === -1 || end <= start) return null;
2582
+ try {
2583
+ const parsed = JSON.parse(text2.slice(start, end + 1));
2584
+ return Array.isArray(parsed) ? parsed : null;
2585
+ } catch {
2586
+ return null;
2587
+ }
2588
+ }
2589
+
2590
+ // src/cli/commands/swarm.ts
2591
+ async function swarm(task, opts) {
2592
+ const config = await loadConfig();
2593
+ const selected = opts.agents ? opts.agents.split(",").map((s) => s.trim()).filter(Boolean) : config.agents.map((a) => a.name);
2594
+ if (selected.length === 0) {
2595
+ throw new Error(t("swarm.noAgents"));
2596
+ }
2597
+ const resolved = selected.map((name) => {
2598
+ const a = config.agents.find((x) => x.name === name);
2599
+ if (!a) throw new Error(t("agent.notFound", { name }));
2600
+ return createProvider(a);
2601
+ });
2602
+ console.log(
2603
+ pc8.dim(t("swarm.status", { agents: resolved.map((a) => a.config.name).join(", "), workspace: process.cwd() }))
2604
+ );
2605
+ console.log(pc8.yellow(t("swarm.bypassNote") + "\n"));
2606
+ const result = await runSwarm({
2607
+ task,
2608
+ workspace: process.cwd(),
2609
+ agents: resolved,
2610
+ allow: config.permissions.allow,
2611
+ deny: config.permissions.deny,
2612
+ maxSubtasks: opts.maxSubtasks ? Number(opts.maxSubtasks) : void 0,
2613
+ events: renderSwarmEvents()
2614
+ });
2615
+ console.log(pc8.bold("\n" + t("swarm.summary")));
2616
+ for (const o of result.outcomes) {
2617
+ const status = o.finished ? pc8.green(t("swarm.statusDone")) : pc8.yellow(t("swarm.statusIncomplete"));
2618
+ const committed = o.committed ? "" : pc8.dim(` (${t("swarm.noChanges")})`);
2619
+ console.log(` ${pc8.bold(o.subtask.id)} [${o.agentName}] ${status}${committed} \u2014 ${o.subtask.title}`);
2620
+ }
2621
+ const conflicts = result.merges.filter((m) => !m.ok);
2622
+ if (conflicts.length > 0) {
2623
+ console.log(pc8.red("\n" + t("swarm.conflictsHeader", { n: conflicts.length })));
2624
+ for (const m of conflicts) {
2625
+ console.log(pc8.red(` ${m.branch}: ${m.conflicts.join(", ")}`));
2626
+ }
2627
+ } else {
2628
+ console.log(pc8.green("\n" + t("swarm.allMerged")));
2629
+ }
2630
+ }
2631
+ function renderSwarmEvents() {
2632
+ return {
2633
+ onDecomposed(subtasks) {
2634
+ console.log(pc8.bold(t("swarm.decomposed", { n: subtasks.length })));
2635
+ for (const s of subtasks) console.log(pc8.dim(` ${s.id}: ${s.title}`));
2636
+ console.log("");
2637
+ },
2638
+ onWorkerStart(subtask, agentName) {
2639
+ console.log(pc8.cyan(t("swarm.workerStart", { id: subtask.id, agent: agentName })));
2640
+ },
2641
+ onWorkerDone(o) {
2642
+ const head = o.finished ? pc8.green(t("swarm.workerDone", { id: o.subtask.id })) : pc8.yellow(t("swarm.workerStopped", { id: o.subtask.id }));
2643
+ const changes = o.committed ? t("swarm.changesCommitted") : t("swarm.noChanges");
2644
+ console.log(head + pc8.dim(t("swarm.workerMeta", { steps: o.steps, changes })));
2645
+ },
2646
+ onMerge(m) {
2647
+ console.log(m.ok ? pc8.dim(t("swarm.merged", { branch: m.branch })) : pc8.red(t("swarm.mergeConflict", { branch: m.branch })));
2648
+ }
2649
+ };
2650
+ }
2651
+
2652
+ // src/cli/commands/models.ts
2653
+ import pc9 from "picocolors";
2654
+ import * as p3 from "@clack/prompts";
2655
+ async function models(opts) {
2656
+ const apiKey = await resolveOpenRouterKey();
2657
+ const spin = p3.spinner();
2658
+ spin.start(t("models.fetching"));
2659
+ let all;
2660
+ try {
2661
+ all = await listOpenRouterModels(apiKey);
2662
+ spin.stop(pc9.green("\u2713 OpenRouter"));
2663
+ } catch (err) {
2664
+ spin.stop(pc9.red(t("models.fetchError", { msg: err.message })), 2);
2665
+ return;
2666
+ }
2667
+ const filtered = filterModels(all, {
2668
+ search: opts.search,
2669
+ tools: opts.tools ? "tools" : "any",
2670
+ freeOnly: Boolean(opts.free),
2671
+ maxPrice: opts.maxPrice !== void 0 ? Number(opts.maxPrice) : void 0,
2672
+ sort: opts.sort ?? "price"
2673
+ });
2674
+ const limit = opts.limit ? Number(opts.limit) : 30;
2675
+ printModelsTable(filtered, limit, all.length);
2676
+ }
2677
+ function printModelsTable(models2, limit, total) {
2678
+ console.log(pc9.dim(t("models.legend")));
2679
+ if (models2.length === 0) {
2680
+ console.log(pc9.yellow(t("models.none")));
2681
+ return;
2682
+ }
2683
+ const rows = models2.slice(0, limit);
2684
+ console.log(
2685
+ " " + pc9.dim(t("models.colTools").padEnd(6)) + pc9.dim(t("models.colPrice").padEnd(16)) + pc9.dim(t("models.colCtx").padEnd(9)) + pc9.dim(t("models.colModel"))
2686
+ );
2687
+ for (const m of rows) {
2688
+ console.log(" " + modelRow(m));
2689
+ }
2690
+ console.log(pc9.dim("\n" + t("models.shown", { shown: rows.length, total })));
2691
+ }
2692
+ function modelRow(m) {
2693
+ const tools = m.supportsTools ? pc9.green("\u{1F6E0}".padEnd(5)) : pc9.dim("\u2014".padEnd(5));
2694
+ const price = `${fmtPrice(m.promptPrice)}/${fmtPrice(m.completionPrice)}`;
2695
+ const priceColored = (m.free ? pc9.green : pc9.yellow)(price.padEnd(16));
2696
+ const ctx = pc9.cyan(fmtContext(m.contextLength).padEnd(9));
2697
+ return `${tools} ${priceColored}${ctx}${pc9.bold(m.id)}`;
2698
+ }
2699
+ async function resolveOpenRouterKey() {
2700
+ if (process.env.OPENROUTER_API_KEY) return process.env.OPENROUTER_API_KEY;
2701
+ try {
2702
+ const config = await loadConfig();
2703
+ const agent = config.agents.find((a) => a.provider === "openrouter" && a.apiKey);
2704
+ return agent ? resolveSecret(agent.apiKey) : void 0;
2705
+ } catch {
2706
+ return void 0;
2707
+ }
2708
+ }
2709
+
2710
+ // src/cli/index.ts
2711
+ import { join as join3 } from "path";
2712
+
2713
+ // src/core/config/dotenv.ts
2714
+ import { existsSync as existsSync2, readFileSync } from "fs";
2715
+ function loadDotenv(paths) {
2716
+ for (const path of paths) {
2717
+ if (!existsSync2(path)) continue;
2718
+ let text2;
2719
+ try {
2720
+ text2 = readFileSync(path, "utf8");
2721
+ } catch {
2722
+ continue;
2723
+ }
2724
+ for (const rawLine of text2.split(/\r?\n/)) {
2725
+ const line = rawLine.trim();
2726
+ if (!line || line.startsWith("#")) continue;
2727
+ const eq = line.indexOf("=");
2728
+ if (eq === -1) continue;
2729
+ const key = line.slice(0, eq).trim();
2730
+ let value = line.slice(eq + 1).trim();
2731
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
2732
+ value = value.slice(1, -1);
2733
+ }
2734
+ if (key && process.env[key] === void 0) process.env[key] = value;
2735
+ }
2736
+ }
2737
+ }
2738
+
2739
+ // src/cli/index.ts
2740
+ async function launchInteractive() {
2741
+ const config = await loadConfig();
2742
+ if (config.agents.length === 0) {
2743
+ console.log(banner());
2744
+ console.log(" " + pc10.yellow(t("welcome.firstRun")) + "\n");
2745
+ await setup();
2746
+ }
2747
+ await run(void 0, {});
2748
+ }
2749
+ function flagLocale(argv) {
2750
+ const i = argv.indexOf("--lang");
2751
+ if (i !== -1 && argv[i + 1]) return argv[i + 1];
2752
+ const eq = argv.find((a) => a.startsWith("--lang="));
2753
+ return eq?.split("=")[1];
2754
+ }
2755
+ async function resolveLocale() {
2756
+ let configLocale;
2757
+ try {
2758
+ configLocale = (await loadConfig()).locale;
2759
+ } catch {
2760
+ }
2761
+ setLocale(pickLocale({ flag: flagLocale(process.argv), config: configLocale }));
2762
+ }
2763
+ function buildProgram() {
2764
+ const program = new Command();
2765
+ program.name("polypus").description(t("cli.description")).version("0.1.0").option("--lang <locale>", t("cli.opt.lang")).action(() => launchInteractive());
2766
+ program.command("setup").description(t("cli.cmd.setup")).action(() => setup());
2767
+ program.command("add-agent").argument("<name>", t("cli.arg.addAgentName")).requiredOption("--provider <provider>", t("cli.opt.provider")).requiredOption("--model <model>", t("cli.opt.model")).option("--api-key <key>", t("cli.opt.apiKey")).option("--base-url <url>", t("cli.opt.baseUrl")).option("--tool-mode <mode>", t("cli.opt.toolMode"), "auto").option("--set-default", t("cli.opt.setDefault")).description(t("cli.cmd.addAgent")).action((name, opts) => addAgent(name, opts));
2768
+ program.command("remove-agent").argument("<name>", t("cli.arg.removeAgentName")).description(t("cli.cmd.removeAgent")).action((name) => removeAgent(name));
2769
+ program.command("list-agents").alias("agents").description(t("cli.cmd.listAgents")).action(() => listAgents());
2770
+ program.command("run").argument("[task]", t("cli.arg.runTask")).option("--agent <name>", t("cli.opt.agent")).option("--mode <mode>", t("cli.opt.mode")).option("--max-steps <n>", t("cli.opt.maxSteps")).description(t("cli.cmd.run")).action((task, opts) => run(task, opts));
2771
+ program.command("swarm").argument("<task>", t("cli.arg.swarmTask")).option("--agents <names>", t("cli.opt.agents")).option("--max-subtasks <n>", t("cli.opt.maxSubtasks")).description(t("cli.cmd.swarm")).action((task, opts) => swarm(task, opts));
2772
+ program.command("models").option("--search <text>", t("cli.opt.search")).option("--tools", t("cli.opt.toolsOnly")).option("--free", t("cli.opt.free")).option("--max-price <usd>", t("cli.opt.maxPrice")).option("--sort <order>", t("cli.opt.sort")).option("--limit <n>", t("cli.opt.limit")).description(t("cli.cmd.models")).action((opts) => models(opts));
2773
+ return program;
2774
+ }
2775
+ async function main() {
2776
+ try {
2777
+ loadDotenv([join3(configDir(), ".env"), join3(process.cwd(), ".env")]);
2778
+ await resolveLocale();
2779
+ await buildProgram().parseAsync(process.argv);
2780
+ } catch (err) {
2781
+ console.error(pc10.red(`\u2717 ${err.message}`));
2782
+ process.exitCode = 1;
2783
+ }
2784
+ }
2785
+ void main();
2786
+ //# sourceMappingURL=index.js.map