@prohost/cli 0.2.0 → 0.2.2
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/CHANGELOG.md +102 -0
- package/README.md +71 -17
- package/dist/agent/claude.d.ts +2 -7
- package/dist/agent/claude.js +17 -29
- package/dist/agent/codex.d.ts +161 -0
- package/dist/agent/codex.js +369 -0
- package/dist/agent/contract.d.ts +62 -3
- package/dist/agent/contract.js +66 -3
- package/dist/agent/mcp.d.ts +23 -0
- package/dist/agent/mcp.js +23 -0
- package/dist/agent/prompt.d.ts +11 -0
- package/dist/agent/prompt.js +97 -4
- package/dist/agent/run.js +77 -19
- package/dist/agent/shell.d.ts +23 -0
- package/dist/agent/shell.js +27 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/agent/prompt.js
CHANGED
|
@@ -6,6 +6,19 @@
|
|
|
6
6
|
* a shell script that greps it. The reply contract is stated explicitly so an
|
|
7
7
|
* agent that would otherwise narrate its reasoning returns something sendable.
|
|
8
8
|
*/
|
|
9
|
+
/**
|
|
10
|
+
* Describe one attachment.
|
|
11
|
+
*
|
|
12
|
+
* The URL is the point — it is the only part the agent can act on — so it goes
|
|
13
|
+
* last, where it is easy to copy out of, and it is never abbreviated.
|
|
14
|
+
*/
|
|
15
|
+
function renderAttachment(attachment) {
|
|
16
|
+
// `image/jpeg` and a bare `image` both count; anything else, including a
|
|
17
|
+
// missing type, is described as a file rather than guessed at.
|
|
18
|
+
const kind = attachment.type?.toLowerCase().startsWith('image') ? 'image' : 'file';
|
|
19
|
+
const name = attachment.name ? ` "${attachment.name}"` : '';
|
|
20
|
+
return `attached ${kind}${name}: ${attachment.url}`;
|
|
21
|
+
}
|
|
9
22
|
/**
|
|
10
23
|
* Render one line of thread history.
|
|
11
24
|
*
|
|
@@ -13,12 +26,18 @@
|
|
|
13
26
|
* a row with a role and no author — which is what a system row like
|
|
14
27
|
* `{role: "event", text: "Reservation confirmed"}` looks like — reads as
|
|
15
28
|
* `event: …` rather than the nonsense `someone (event): …`.
|
|
29
|
+
*
|
|
30
|
+
* Attachments render inline, after the text. An image-only message has no text
|
|
31
|
+
* at all, so the parts are joined rather than concatenated — otherwise the line
|
|
32
|
+
* opens with a stray space where the message would have been.
|
|
16
33
|
*/
|
|
17
34
|
function renderMessage(message) {
|
|
18
35
|
const who = message.author ?? message.role ?? 'someone';
|
|
19
36
|
const role = message.author && message.role ? ` (${message.role})` : '';
|
|
20
37
|
const when = message.at ? `[${message.at}] ` : '';
|
|
21
|
-
|
|
38
|
+
const attachments = (message.attachments ?? []).map((file) => `[${renderAttachment(file)}]`);
|
|
39
|
+
const body = [message.text, ...attachments].filter((part) => part.length > 0).join(' ');
|
|
40
|
+
return `${when}${who}${role}: ${body}`;
|
|
22
41
|
}
|
|
23
42
|
/** Render one remembered fact. */
|
|
24
43
|
function renderMemory(memory) {
|
|
@@ -28,8 +47,9 @@ function renderMemory(memory) {
|
|
|
28
47
|
export function buildPrompt(run, context) {
|
|
29
48
|
const name = run.agent_name ?? context.agentName;
|
|
30
49
|
const canReply = Boolean(run.reply_surface !== 'none' && run.reply_path && run.reply_body_key);
|
|
50
|
+
const title = run.agent_title ? ` (${run.agent_title})` : '';
|
|
31
51
|
const lines = [
|
|
32
|
-
`You are "${name}", an AI teammate in a ProhostAI workspace.`,
|
|
52
|
+
`You are "${name}"${title}, an AI teammate in a ProhostAI workspace.`,
|
|
33
53
|
canReply && run.reply_surface === 'conversation'
|
|
34
54
|
? 'You were mentioned in a conversation and someone is waiting on a reply.'
|
|
35
55
|
: run.reply_surface === 'none'
|
|
@@ -39,6 +59,20 @@ export function buildPrompt(run, context) {
|
|
|
39
59
|
: `You have been given work on a ${run.reply_surface}. There is nowhere to post a reply.`,
|
|
40
60
|
'',
|
|
41
61
|
];
|
|
62
|
+
// The operator's own configuration comes first, above everything the server
|
|
63
|
+
// generated. It is who this agent *is* — persona, house rules, escalation
|
|
64
|
+
// policy — and an agent that meets its standing instructions only after four
|
|
65
|
+
// blocks of run context reads them as an afterthought rather than as its
|
|
66
|
+
// brief.
|
|
67
|
+
//
|
|
68
|
+
// Rendered as instructions, not as quoted data: the text is written by the
|
|
69
|
+
// person who runs this machine, in their own agent profile, and reaches us
|
|
70
|
+
// over the same HMAC-verified channel as the rest of the run — the same trust
|
|
71
|
+
// as the `--exec` command they typed. It is fenced anyway, so the agent can
|
|
72
|
+
// always tell where its operator stops talking and the server starts.
|
|
73
|
+
if (run.system_prompt) {
|
|
74
|
+
lines.push('Your operator configured these standing instructions for you, in your', 'ProhostAI agent profile. Follow them — they define how you work here.', '--- begin operator instructions ---', run.system_prompt, '--- end operator instructions ---', 'Everything below this line is generated by ProhostAI for this one run. It', 'describes what is actually true about this turn and your access to it,', 'which the instructions above do not change.', '');
|
|
75
|
+
}
|
|
42
76
|
const facts = [];
|
|
43
77
|
if (run.kind)
|
|
44
78
|
facts.push(`Why you woke up: ${run.kind}`);
|
|
@@ -72,14 +106,26 @@ export function buildPrompt(run, context) {
|
|
|
72
106
|
if (run.recent_messages?.length) {
|
|
73
107
|
lines.push('Conversation so far (oldest first):', ...run.recent_messages.map(renderMessage), '--- end of conversation history ---', '');
|
|
74
108
|
}
|
|
109
|
+
const triggerAttachments = run.trigger_attachments ?? [];
|
|
75
110
|
if (run.trigger_text) {
|
|
76
111
|
// The server caps this excerpt, so say so rather than let an agent assume
|
|
77
112
|
// it has the whole message.
|
|
78
113
|
lines.push('Message that woke you (may be truncated by the server):', run.trigger_text, '');
|
|
79
114
|
}
|
|
115
|
+
else if (triggerAttachments.length > 0) {
|
|
116
|
+
// Distinct from "the server didn't send the text": here the message really
|
|
117
|
+
// had none. An agent told the text was merely missing goes looking for it,
|
|
118
|
+
// or apologises for not having it, when the picture below IS the message.
|
|
119
|
+
lines.push('The message that woke you had no text — what follows is all of it.', '');
|
|
120
|
+
}
|
|
80
121
|
else {
|
|
81
122
|
lines.push('The message text was not included in the event.', '');
|
|
82
123
|
}
|
|
124
|
+
if (triggerAttachments.length > 0) {
|
|
125
|
+
lines.push('Sent with that message:', ...triggerAttachments.map((file) => `- ${renderAttachment(file)}`), '');
|
|
126
|
+
}
|
|
127
|
+
const hasAttachments = triggerAttachments.length > 0 ||
|
|
128
|
+
Boolean(run.recent_messages?.some((message) => message.attachments?.length));
|
|
83
129
|
// An agent that doesn't know it is headless narrates as if it were mid-
|
|
84
130
|
// session — "let me connect to that", "I'll check once the tools load" —
|
|
85
131
|
// and that text is what gets posted to a customer, describing a lookup that
|
|
@@ -92,7 +138,48 @@ export function buildPrompt(run, context) {
|
|
|
92
138
|
: ' nothing will stop to ask you for permission: your tools run as soon\n' +
|
|
93
139
|
' as you call them. Do the work rather than proposing it — but that\n' +
|
|
94
140
|
' also means a mistaken action happens for real, so check before you\n' +
|
|
95
|
-
' change or delete anything.', '- Nothing about your access changes after this turn. Do not say a tool is', ' "connecting", "loading", or about to become available, and do not', ' promise to follow up.',
|
|
141
|
+
' change or delete anything.', '- Nothing about your access changes after this turn. Do not say a tool is', ' "connecting", "loading", or about to become available, and do not', ' promise to follow up.',
|
|
142
|
+
// Claude Code lazy-loads part of its catalog — WebSearch/WebFetch among
|
|
143
|
+
// them — so a run that inspects its loaded tools concludes it cannot
|
|
144
|
+
// browse and says so to a customer, when one ToolSearch call away it
|
|
145
|
+
// could. Field-observed on the first 0.2.0 deployment; phrased
|
|
146
|
+
// conditionally because other `--exec` brains have no ToolSearch.
|
|
147
|
+
'- If you have a ToolSearch tool, part of your toolkit is deferred and', ' invisible until loaded — web browsing (WebSearch, WebFetch) usually', ' is. Search ToolSearch for a capability before telling anyone you', ' lack it.',
|
|
148
|
+
// The counterpart to the ToolSearch bullet, and the same failure one level
|
|
149
|
+
// up: a brain that assumes it is boxed in reports "I have no access to the
|
|
150
|
+
// codebase" from a machine with the checkout, the credentials and the CLI
|
|
151
|
+
// all sitting right there. Only true when tools are ungated — under
|
|
152
|
+
// --safe-tools the looking-around itself would sit waiting on an approval
|
|
153
|
+
// nobody is there to give, so the licence is withheld rather than dangled.
|
|
154
|
+
...(context.toolsGated
|
|
155
|
+
? []
|
|
156
|
+
: [
|
|
157
|
+
"- You are running on your operator's own machine, in their shell, with",
|
|
158
|
+
' whatever they have installed — not in an empty sandbox. Before you',
|
|
159
|
+
' tell anyone you have no access to some repository, codebase, log, or',
|
|
160
|
+
' capability, take a QUICK look for it: the usual places a checkout',
|
|
161
|
+
' lives, `gh auth status`, whether the command is on PATH. A quick',
|
|
162
|
+
' look, not an audit — this run has a hard time limit and somebody',
|
|
163
|
+
' waiting at the end of it.',
|
|
164
|
+
]),
|
|
165
|
+
// Only when there is something to look at. Said unconditionally it is a
|
|
166
|
+
// paragraph of advice about a situation that does not exist, which is how a
|
|
167
|
+
// prompt teaches an agent to mention attachments nobody sent.
|
|
168
|
+
...(!hasAttachments
|
|
169
|
+
? []
|
|
170
|
+
: context.toolsGated
|
|
171
|
+
? [
|
|
172
|
+
'- Attachments above are given as URLs, not as pictures you can see.',
|
|
173
|
+
' Fetching one needs a tool that may not run this turn, so if you',
|
|
174
|
+
' could not view it, say so rather than guessing at what it shows.',
|
|
175
|
+
]
|
|
176
|
+
: [
|
|
177
|
+
'- Attachments above are given as URLs, and a URL is not something you',
|
|
178
|
+
' can see. To actually look at one, download it to a local file first',
|
|
179
|
+
' (`curl -sL "<url>" -o /tmp/attachment.jpg`) and then open that file',
|
|
180
|
+
' with your file-reading tool — your vision works on local files, not',
|
|
181
|
+
' on links. Never describe an image you have not opened.',
|
|
182
|
+
]), "- State limits plainly. If you cannot look something up, say what you don't", ' know and answer with what you do — a short honest answer beats a', ' confident invented one.',
|
|
96
183
|
// Deliberately does not list which domains are reachable. A paired
|
|
97
184
|
// credential holds a narrow, server-chosen scope set — the current default
|
|
98
185
|
// covers conversations, tasks, cleanings and approvals but NOT reservations
|
|
@@ -101,7 +188,13 @@ export function buildPrompt(run, context) {
|
|
|
101
188
|
// domains the credential can't reach is the same failure as naming an MCP
|
|
102
189
|
// endpoint it can't call: the agent reports a lookup it never made.
|
|
103
190
|
context.mcpServerName
|
|
104
|
-
? `- You have ProhostAI tools
|
|
191
|
+
? `- You have ProhostAI tools${
|
|
192
|
+
// Named by pattern only where the harness has verified how this brain
|
|
193
|
+
// spells them; otherwise by server, which the agent can match against
|
|
194
|
+
// its own tool list without being told a name that might be wrong.
|
|
195
|
+
context.mcpToolPattern
|
|
196
|
+
? ` (${context.mcpToolPattern})`
|
|
197
|
+
: `, from the MCP server registered as "${context.mcpServerName}"`}. They are the sanctioned ` +
|
|
105
198
|
'way to look up workspace data, so use them rather than guessing. Your credential is ' +
|
|
106
199
|
'deliberately narrow and does not reach every part of the workspace: if a tool returns ' +
|
|
107
200
|
'a permission error, that is a real limit rather than a glitch — say what you could not ' +
|
package/dist/agent/run.js
CHANGED
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
import os from 'node:os';
|
|
21
21
|
import WebSocket from 'ws';
|
|
22
22
|
import { completeRun, postReply } from './api.js';
|
|
23
|
-
import {
|
|
23
|
+
import { claudeCommand, isClaudeExec, looksLikeResumeFailure, parseClaudeOutput, writeMcpConfig, } from './claude.js';
|
|
24
|
+
import { MCP_API_KEY_ENV_VAR, codexCommand, isCodexExec, looksLikeCodexResumeFailure, parseCodexOutput, } from './codex.js';
|
|
25
|
+
import { MCP_SERVER_NAME, MCP_TOOL_PATTERN } from './mcp.js';
|
|
24
26
|
import { EVENT_AGENT_RUN_REQUESTED, EVENT_MENTION_CREATED, EVENT_MESSAGE_TEAM_CHAT, eventData, parseRunRequest, } from './contract.js';
|
|
25
27
|
import { ensureWorkspace, streamUrlFor, workspacePath } from './credentials.js';
|
|
26
28
|
import { execAgent } from './exec.js';
|
|
@@ -125,13 +127,23 @@ function resolveWorkdir(options, log) {
|
|
|
125
127
|
function buildRuntime(options, log) {
|
|
126
128
|
const workdir = resolveWorkdir(options, log);
|
|
127
129
|
const sessions = SessionStore.load(options.env, log);
|
|
128
|
-
const
|
|
130
|
+
const brain = isClaudeExec(options.exec)
|
|
131
|
+
? 'claude'
|
|
132
|
+
: isCodexExec(options.exec)
|
|
133
|
+
? 'codex'
|
|
134
|
+
: 'plain';
|
|
129
135
|
const skipPermissions = options.safeTools !== true;
|
|
130
|
-
if (
|
|
131
|
-
return { workdir, sessions,
|
|
132
|
-
|
|
136
|
+
if (brain === 'plain') {
|
|
137
|
+
return { workdir, sessions, brain, skipPermissions, mcpWired: false };
|
|
138
|
+
}
|
|
133
139
|
const mcpUrl = options.credentials.mcp_url;
|
|
134
|
-
|
|
140
|
+
let mcpConfigPath;
|
|
141
|
+
let codexMcpUrl;
|
|
142
|
+
let execEnv;
|
|
143
|
+
if (!mcpUrl) {
|
|
144
|
+
log(`${timestamp()} ! this pairing carries no MCP endpoint; the agent will run without ProhostAI tools`);
|
|
145
|
+
}
|
|
146
|
+
else if (brain === 'claude') {
|
|
135
147
|
try {
|
|
136
148
|
mcpConfigPath = writeMcpConfig({
|
|
137
149
|
mcpUrl,
|
|
@@ -149,21 +161,37 @@ function buildRuntime(options, log) {
|
|
|
149
161
|
}
|
|
150
162
|
}
|
|
151
163
|
else {
|
|
152
|
-
|
|
164
|
+
// Codex needs no file: the server is a `-c` override on each invocation and
|
|
165
|
+
// the key is read from the child's environment. Nothing to fail, so nothing
|
|
166
|
+
// to fall back from.
|
|
167
|
+
codexMcpUrl = mcpUrl;
|
|
168
|
+
execEnv = { [MCP_API_KEY_ENV_VAR]: options.credentials.api_key };
|
|
169
|
+
}
|
|
170
|
+
const mcpWired = Boolean(mcpConfigPath ?? codexMcpUrl);
|
|
171
|
+
const tools = mcpWired ? `, ProhostAI tools wired in as ${MCP_SERVER_NAME}` : '';
|
|
172
|
+
const where = workdir ? ` (workspace: ${workdir})` : '';
|
|
173
|
+
log(brain === 'claude'
|
|
174
|
+
? `${timestamp()} ⚙ Claude Code detected — resuming a session per conversation${tools}${where}`
|
|
175
|
+
: `${timestamp()} ⚙ Codex detected — resuming a thread per conversation${tools}${where}`);
|
|
176
|
+
if (brain === 'codex' && mcpWired) {
|
|
177
|
+
// The one asymmetry worth an operator knowing about, because it is a
|
|
178
|
+
// security property they may be assuming they have. Claude Code is told to
|
|
179
|
+
// load our config and nothing else; Codex has no equivalent switch, so its
|
|
180
|
+
// own `~/.codex/config.toml` servers come along.
|
|
181
|
+
log(`${timestamp()} ! Codex has no --strict-mcp-config equivalent: this run also holds ` +
|
|
182
|
+
'whatever MCP servers your ~/.codex/config.toml defines.');
|
|
153
183
|
}
|
|
154
|
-
log(`${timestamp()} ⚙ Claude Code detected — resuming a session per conversation` +
|
|
155
|
-
(mcpConfigPath ? `, ProhostAI tools wired in as ${MCP_SERVER_NAME}` : '') +
|
|
156
|
-
`${workdir ? ` (workspace: ${workdir})` : ''}`);
|
|
157
184
|
// Said out loud every start-up, not buried in --help. This is the line that
|
|
158
185
|
// tells an operator their machine will run tool calls, unprompted, on text
|
|
159
186
|
// written by someone else in their workspace.
|
|
187
|
+
const bypassFlag = brain === 'claude' ? '--dangerously-skip-permissions' : '--yolo';
|
|
160
188
|
log(skipPermissions
|
|
161
189
|
? `${timestamp()} ⚠ tools run WITHOUT permission prompts ` +
|
|
162
|
-
|
|
190
|
+
`(${bypassFlag}). The agent acts as you, with your local ` +
|
|
163
191
|
'access, on instructions from your workspace. Use --safe-tools to gate them.'
|
|
164
192
|
: `${timestamp()} ⚙ --safe-tools: tools are permission-gated; anything needing ` +
|
|
165
193
|
'approval will not run (nobody is at a terminal to approve it).');
|
|
166
|
-
return { workdir, sessions,
|
|
194
|
+
return { workdir, sessions, brain, mcpConfigPath, codexMcpUrl, execEnv, mcpWired, skipPermissions };
|
|
167
195
|
}
|
|
168
196
|
/**
|
|
169
197
|
* Run the agent command once and turn its output into a reply.
|
|
@@ -187,6 +215,7 @@ async function runTurn(run, prompt, options, runtime, log) {
|
|
|
187
215
|
prompt,
|
|
188
216
|
timeoutMs: remainingMs(),
|
|
189
217
|
cwd: runtime.workdir,
|
|
218
|
+
env: runtime.execEnv,
|
|
190
219
|
spawnImpl: options.spawnImpl,
|
|
191
220
|
});
|
|
192
221
|
// Wall clock for the whole turn, retry included — what the operator waited,
|
|
@@ -199,7 +228,7 @@ async function runTurn(run, prompt, options, runtime, log) {
|
|
|
199
228
|
detail: result.stderr.trim().slice(-500),
|
|
200
229
|
durationMs: elapsed(),
|
|
201
230
|
});
|
|
202
|
-
if (
|
|
231
|
+
if (runtime.brain === 'plain') {
|
|
203
232
|
const result = await exec(options.exec);
|
|
204
233
|
if (!result.ok)
|
|
205
234
|
return failure(result);
|
|
@@ -209,29 +238,55 @@ async function runTurn(run, prompt, options, runtime, log) {
|
|
|
209
238
|
: { ok: false, reason: 'agent produced no output', durationMs: elapsed() };
|
|
210
239
|
}
|
|
211
240
|
// No session bookkeeping on a dry run. `--dry-run` promises to make no
|
|
212
|
-
// writes, and a resumed turn is a write:
|
|
241
|
+
// writes, and a resumed turn is a write: both brains record it in their
|
|
213
242
|
// transcript whether or not we post the reply, so remembering the session
|
|
214
243
|
// would leave the next *real* run continuing from an answer nobody saw.
|
|
215
244
|
const key = options.dryRun ? undefined : sessionKeyFor(run);
|
|
216
245
|
const sessionId = runtime.sessions.get(key);
|
|
217
|
-
const
|
|
218
|
-
|
|
246
|
+
const codex = runtime.brain === 'codex';
|
|
247
|
+
// Same three questions for both brains — resume this thread, wire these
|
|
248
|
+
// tools, gate or don't — asked with each one's own flags.
|
|
249
|
+
const build = (resume) => codex
|
|
250
|
+
? codexCommand(options.exec, {
|
|
251
|
+
sessionId: resume,
|
|
252
|
+
mcpUrl: runtime.codexMcpUrl,
|
|
253
|
+
skipPermissions: runtime.skipPermissions,
|
|
254
|
+
})
|
|
255
|
+
: claudeCommand(options.exec, {
|
|
256
|
+
sessionId: resume,
|
|
257
|
+
mcpConfigPath: runtime.mcpConfigPath,
|
|
258
|
+
skipPermissions: runtime.skipPermissions,
|
|
259
|
+
});
|
|
260
|
+
let result = await exec(build(sessionId));
|
|
219
261
|
// A remembered session may simply be gone — transcripts are cleaned up and
|
|
220
262
|
// the ledger can't know. One retry without it, because the alternative is a
|
|
221
263
|
// surface that fails every run forever over an optimisation.
|
|
264
|
+
const resumeFailed = codex ? looksLikeCodexResumeFailure : looksLikeResumeFailure;
|
|
222
265
|
if (!result.ok &&
|
|
223
266
|
sessionId &&
|
|
224
|
-
|
|
267
|
+
resumeFailed(`${result.stderr}\n${result.stdout}`, sessionId) &&
|
|
225
268
|
// Skipped rather than squeezed into a token budget: a retry with no time
|
|
226
269
|
// left would be killed on the spot and reported as a timeout, which is a
|
|
227
270
|
// worse error message than the resume failure it replaced.
|
|
228
271
|
remainingMs() > 0) {
|
|
229
272
|
log(` … session ${sessionId.slice(0, 12)} could not be resumed; starting a fresh one`);
|
|
230
273
|
runtime.sessions.forget(key);
|
|
231
|
-
result = await exec(
|
|
274
|
+
result = await exec(build());
|
|
232
275
|
}
|
|
233
276
|
if (!result.ok)
|
|
234
277
|
return failure(result);
|
|
278
|
+
if (codex) {
|
|
279
|
+
const output = parseCodexOutput(result.stdout);
|
|
280
|
+
if (!output.ok) {
|
|
281
|
+
return {
|
|
282
|
+
ok: false,
|
|
283
|
+
reason: 'the agent did not produce a usable reply',
|
|
284
|
+
detail: output.error,
|
|
285
|
+
durationMs: elapsed(),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
return { ok: true, reply: output.text, durationMs: elapsed(), sessionId: output.sessionId };
|
|
289
|
+
}
|
|
235
290
|
const parsed = parseClaudeOutput(result.stdout);
|
|
236
291
|
// A denied tool is not an error the agent reports — the turn succeeds and the
|
|
237
292
|
// reply just quietly lacks whatever the tool would have found. Printing it is
|
|
@@ -265,7 +320,10 @@ async function handleRun(run, options, runtime, api, log, sleep) {
|
|
|
265
320
|
// Named only when the tools are actually reachable from this invocation.
|
|
266
321
|
// Telling an agent about an endpoint it has no client for is what produced
|
|
267
322
|
// replies describing lookups that never happened.
|
|
268
|
-
mcpServerName: runtime.
|
|
323
|
+
mcpServerName: runtime.mcpWired ? MCP_SERVER_NAME : undefined,
|
|
324
|
+
// Only Claude Code's spelling has been verified against the binary, so only
|
|
325
|
+
// Claude Code gets told a pattern. See PromptContext.mcpToolPattern.
|
|
326
|
+
mcpToolPattern: runtime.brain === 'claude' && runtime.mcpWired ? MCP_TOOL_PATTERN : undefined,
|
|
269
327
|
toolsGated: !runtime.skipPermissions,
|
|
270
328
|
});
|
|
271
329
|
const result = await runTurn(run, prompt, options, runtime, log);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quoting for the `shell: true` command lines the brain harnesses build.
|
|
3
|
+
*
|
|
4
|
+
* Shared by `claude.ts` and `codex.ts` because both append values — a session
|
|
5
|
+
* id we stored, a path under `$PROHOST_HOME`, a TOML config override — to a
|
|
6
|
+
* string that a shell will parse.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Quote a value for a `shell: true` command line on this platform.
|
|
10
|
+
*
|
|
11
|
+
* Every value that goes through here is ours or the operator's — never
|
|
12
|
+
* event-derived text. It is quoted anyway: a home directory with a space in it
|
|
13
|
+
* is ordinary, and "the inputs are trusted" is the assumption that stops being
|
|
14
|
+
* true the first time someone extends this.
|
|
15
|
+
*
|
|
16
|
+
* Windows needs its own branch. `shell: true` there runs `cmd.exe`, where a
|
|
17
|
+
* single quote is an ordinary character rather than a quoting one — POSIX
|
|
18
|
+
* quoting would hand the child a config path with literal apostrophes around
|
|
19
|
+
* it, and the tools would simply never load.
|
|
20
|
+
*/
|
|
21
|
+
export declare function shellQuote(value: string, platform: NodeJS.Platform): string;
|
|
22
|
+
/** Pipes, lists, redirections, subshells — anything that makes "append to the end" wrong. */
|
|
23
|
+
export declare const SHELL_CONTROL_OPERATORS: RegExp;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quoting for the `shell: true` command lines the brain harnesses build.
|
|
3
|
+
*
|
|
4
|
+
* Shared by `claude.ts` and `codex.ts` because both append values — a session
|
|
5
|
+
* id we stored, a path under `$PROHOST_HOME`, a TOML config override — to a
|
|
6
|
+
* string that a shell will parse.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Quote a value for a `shell: true` command line on this platform.
|
|
10
|
+
*
|
|
11
|
+
* Every value that goes through here is ours or the operator's — never
|
|
12
|
+
* event-derived text. It is quoted anyway: a home directory with a space in it
|
|
13
|
+
* is ordinary, and "the inputs are trusted" is the assumption that stops being
|
|
14
|
+
* true the first time someone extends this.
|
|
15
|
+
*
|
|
16
|
+
* Windows needs its own branch. `shell: true` there runs `cmd.exe`, where a
|
|
17
|
+
* single quote is an ordinary character rather than a quoting one — POSIX
|
|
18
|
+
* quoting would hand the child a config path with literal apostrophes around
|
|
19
|
+
* it, and the tools would simply never load.
|
|
20
|
+
*/
|
|
21
|
+
export function shellQuote(value, platform) {
|
|
22
|
+
if (platform === 'win32')
|
|
23
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
24
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
25
|
+
}
|
|
26
|
+
/** Pipes, lists, redirections, subshells — anything that makes "append to the end" wrong. */
|
|
27
|
+
export const SHELL_CONTROL_OPERATORS = /[|&;<>(){}`]|\$\(/;
|
package/dist/version.d.ts
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
* `package.json`, so a release bump that forgets this file fails the suite
|
|
8
8
|
* instead of shipping a `User-Agent` that lies about which build is calling.
|
|
9
9
|
*/
|
|
10
|
-
export declare const CLI_VERSION = "0.2.
|
|
10
|
+
export declare const CLI_VERSION = "0.2.2";
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
|
-
export declare const USER_AGENT = "prohost-cli/0.2.
|
|
12
|
+
export declare const USER_AGENT = "prohost-cli/0.2.2";
|
package/dist/version.js
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
* `package.json`, so a release bump that forgets this file fails the suite
|
|
8
8
|
* instead of shipping a `User-Agent` that lies about which build is calling.
|
|
9
9
|
*/
|
|
10
|
-
export const CLI_VERSION = '0.2.
|
|
10
|
+
export const CLI_VERSION = '0.2.2';
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
12
|
export const USER_AGENT = `prohost-cli/${CLI_VERSION}`;
|