privateer-agent 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/LICENSE +21 -0
- package/README.md +474 -0
- package/bin/privateer.mjs +11 -0
- package/package.json +74 -0
- package/src/agents/loader.ts +49 -0
- package/src/auth/privateer.ts +393 -0
- package/src/commands/custom.ts +75 -0
- package/src/commands/registry.ts +499 -0
- package/src/components/AgentGroupView.tsx +104 -0
- package/src/components/App.tsx +1376 -0
- package/src/components/ApprovalPrompt.tsx +38 -0
- package/src/components/Banner.tsx +58 -0
- package/src/components/Markdown.tsx +183 -0
- package/src/components/ModeHint.tsx +40 -0
- package/src/components/ModelPicker.tsx +269 -0
- package/src/components/Onboarding.tsx +203 -0
- package/src/components/PlanConfirm.tsx +37 -0
- package/src/components/PrivateerLogin.tsx +109 -0
- package/src/components/PromptInput.tsx +602 -0
- package/src/components/RewindPicker.tsx +69 -0
- package/src/components/Root.tsx +95 -0
- package/src/components/SessionPicker.tsx +64 -0
- package/src/components/StatusBar.tsx +121 -0
- package/src/components/TodoPanel.tsx +36 -0
- package/src/components/ToolCallView.tsx +109 -0
- package/src/components/Transcript.tsx +203 -0
- package/src/components/figures.ts +13 -0
- package/src/components/promptModel.ts +73 -0
- package/src/components/spinnerVerbs.ts +46 -0
- package/src/components/theme.ts +55 -0
- package/src/components/types.ts +34 -0
- package/src/components/useTeeShield.ts +104 -0
- package/src/components/useTerminalWidth.ts +24 -0
- package/src/components/useZdrShield.ts +126 -0
- package/src/config/load.ts +115 -0
- package/src/config/paths.ts +61 -0
- package/src/config/schema.ts +94 -0
- package/src/context/outputStyles.ts +42 -0
- package/src/context/projectInfo.ts +59 -0
- package/src/context/systemPrompt.ts +167 -0
- package/src/engine/QueryEngine.ts +399 -0
- package/src/engine/errors.ts +197 -0
- package/src/engine/events.ts +74 -0
- package/src/engine/router.ts +165 -0
- package/src/hooks/engine.ts +155 -0
- package/src/main.tsx +167 -0
- package/src/mcp/client.ts +236 -0
- package/src/mcp/oauth.ts +245 -0
- package/src/memory/auto.ts +146 -0
- package/src/memory/checkpoints.ts +227 -0
- package/src/memory/store.ts +127 -0
- package/src/permissions/danger.ts +56 -0
- package/src/permissions/gate.ts +38 -0
- package/src/permissions/mode.ts +39 -0
- package/src/permissions/protected.ts +29 -0
- package/src/permissions/uiGate.ts +73 -0
- package/src/providers/attestation.ts +149 -0
- package/src/providers/capabilities.ts +104 -0
- package/src/providers/catalog.ts +66 -0
- package/src/providers/models.ts +183 -0
- package/src/providers/registry.ts +71 -0
- package/src/providers/resolve.ts +78 -0
- package/src/remote/relayClient.ts +283 -0
- package/src/session.ts +264 -0
- package/src/tools/bash.ts +98 -0
- package/src/tools/context.ts +114 -0
- package/src/tools/edit.ts +67 -0
- package/src/tools/exec.ts +60 -0
- package/src/tools/glob.ts +39 -0
- package/src/tools/grep.ts +86 -0
- package/src/tools/index.ts +69 -0
- package/src/tools/memory.ts +53 -0
- package/src/tools/processRegistry.ts +77 -0
- package/src/tools/read.ts +42 -0
- package/src/tools/saveAttachment.ts +53 -0
- package/src/tools/task.ts +52 -0
- package/src/tools/todo.ts +36 -0
- package/src/tools/todoStore.ts +31 -0
- package/src/tools/walk.ts +44 -0
- package/src/tools/web.ts +145 -0
- package/src/tools/write.ts +40 -0
- package/src/util/attachmentStore.ts +72 -0
- package/src/util/images.ts +343 -0
- package/src/util/limit.ts +32 -0
- package/src/util/redact.ts +44 -0
- package/src/version.ts +13 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { DEFAULT_DENYLIST } from "../permissions/danger.ts";
|
|
3
|
+
|
|
4
|
+
// A provider's credentials/endpoint. All fields optional so config can be sparse
|
|
5
|
+
// and filled in from environment variables at load time.
|
|
6
|
+
export const ProviderConfig = z.object({
|
|
7
|
+
apiKey: z.string().optional(),
|
|
8
|
+
baseURL: z.string().optional(),
|
|
9
|
+
// OpenRouter only: when true, every request is pinned to a Zero-Data-Retention
|
|
10
|
+
// endpoint (provider.zdr) so prompts can't be retained. Models without a ZDR
|
|
11
|
+
// endpoint then become unusable (and render red in the picker). Ignored by
|
|
12
|
+
// other providers.
|
|
13
|
+
enforceZdr: z.boolean().optional(),
|
|
14
|
+
});
|
|
15
|
+
export type ProviderConfig = z.infer<typeof ProviderConfig>;
|
|
16
|
+
|
|
17
|
+
export const PERMISSION_MODES = ["default", "acceptEdits", "bypass", "plan"] as const;
|
|
18
|
+
export type PermissionMode = (typeof PERMISSION_MODES)[number];
|
|
19
|
+
|
|
20
|
+
export const Config = z.object({
|
|
21
|
+
// "provider:model", e.g. "openrouter:anthropic/claude-opus-4.8".
|
|
22
|
+
defaultModel: z.string().default("anthropic:claude-opus-4-8"),
|
|
23
|
+
permissionMode: z.enum(PERMISSION_MODES).default("acceptEdits"),
|
|
24
|
+
providers: z
|
|
25
|
+
.object({
|
|
26
|
+
openrouter: ProviderConfig.optional(),
|
|
27
|
+
anthropic: ProviderConfig.optional(),
|
|
28
|
+
openai: ProviderConfig.optional(),
|
|
29
|
+
ollama: ProviderConfig.optional(),
|
|
30
|
+
nearai: ProviderConfig.optional(),
|
|
31
|
+
// Privateer account: inference billed to the user's account, no API key.
|
|
32
|
+
// Auth lives in ~/.privateer/credentials.json (see src/auth/privateer.ts),
|
|
33
|
+
// not here; this entry only carries an optional server baseURL override.
|
|
34
|
+
privateer: ProviderConfig.optional(),
|
|
35
|
+
})
|
|
36
|
+
.default({}),
|
|
37
|
+
// Confine file access to the working directory. When true (the default), the agent
|
|
38
|
+
// reads/searches/edits only within cwd; reaching outside (an absolute path or `../`
|
|
39
|
+
// escape) requires explicit per-location approval. Set false to let it roam freely.
|
|
40
|
+
confineToCwd: z.boolean().default(true),
|
|
41
|
+
// Bash command prefixes that are auto-approved (e.g. "git status", "ls").
|
|
42
|
+
allowlist: z.array(z.string()).default([]),
|
|
43
|
+
// Regex sources for commands that ALWAYS require confirmation, even under
|
|
44
|
+
// bypass or an allowlist entry (destructive/exfil shapes). Extend per-project.
|
|
45
|
+
denylist: z.array(z.string()).default(DEFAULT_DENYLIST),
|
|
46
|
+
// Hard cap on agent tool-loop steps per turn.
|
|
47
|
+
maxSteps: z.number().int().positive().default(50),
|
|
48
|
+
// Approx token budget for the conversation context (used to trigger auto-compaction).
|
|
49
|
+
contextBudget: z.number().int().positive().default(120_000),
|
|
50
|
+
// Fraction of contextBudget at which to auto-compact older history (0–1).
|
|
51
|
+
compactRatio: z.number().positive().max(1).default(0.8),
|
|
52
|
+
// Modal (vim) editing in the prompt input.
|
|
53
|
+
vim: z.boolean().default(false),
|
|
54
|
+
// Active output style (persona) by name; loaded from .privateer/output-styles.
|
|
55
|
+
outputStyle: z.string().optional(),
|
|
56
|
+
// Max `task` sub-agents allowed to run concurrently when the model fans them out.
|
|
57
|
+
maxSubagents: z.number().int().positive().default(4),
|
|
58
|
+
// Anthropic extended-thinking budget in tokens (opt-in; Anthropic models only).
|
|
59
|
+
thinkingBudget: z.number().int().positive().optional(),
|
|
60
|
+
// Per-turn model routing. The `default` route is `defaultModel` above; these are
|
|
61
|
+
// the specialized routes the router switches to based on the turn's data/shape.
|
|
62
|
+
// See src/engine/router.ts for the selection rules (vision > long > fast > default).
|
|
63
|
+
router: z
|
|
64
|
+
.object({
|
|
65
|
+
// Per-modality routes — turns whose input includes that kind go to this model.
|
|
66
|
+
vision: z.string().optional(), // image input
|
|
67
|
+
document: z.string().optional(), // PDF / document input
|
|
68
|
+
audio: z.string().optional(), // audio input
|
|
69
|
+
video: z.string().optional(), // video input
|
|
70
|
+
long: z.string().optional(), // large conversations
|
|
71
|
+
fast: z.string().optional(), // short, cheap turns
|
|
72
|
+
// Estimated-token threshold that triggers the `long` route. Defaults to half
|
|
73
|
+
// the contextBudget when unset (resolved in the session, not here).
|
|
74
|
+
longThreshold: z.number().int().positive().optional(),
|
|
75
|
+
// Prompts at or below this many characters are eligible for the `fast` route.
|
|
76
|
+
fastMaxChars: z.number().int().positive().default(280),
|
|
77
|
+
// Referenced text/code files at or below this many bytes are inlined into the
|
|
78
|
+
// prompt (read-as-text); larger ones are left as a path for the read tool.
|
|
79
|
+
inlineTextMaxBytes: z.number().int().positive().default(65_536),
|
|
80
|
+
// Hybrid auto-detect: when a modality route is unset and the default model can't
|
|
81
|
+
// handle that modality, pick a capable model automatically.
|
|
82
|
+
auto: z.boolean().default(true),
|
|
83
|
+
})
|
|
84
|
+
.optional(),
|
|
85
|
+
// Shell command whose stdout becomes the status line; receives session JSON on stdin.
|
|
86
|
+
statusLine: z.string().optional(),
|
|
87
|
+
})
|
|
88
|
+
// Preserve unknown keys so layered settings files can carry forward-compatible
|
|
89
|
+
// sections (hooks, mcpServers, statusLine, …) before they have explicit schemas.
|
|
90
|
+
.catchall(z.unknown());
|
|
91
|
+
export type Config = z.infer<typeof Config>;
|
|
92
|
+
|
|
93
|
+
export const KNOWN_PROVIDERS = ["openrouter", "anthropic", "openai", "ollama", "nearai", "privateer"] as const;
|
|
94
|
+
export type ProviderName = (typeof KNOWN_PROVIDERS)[number];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { globalPaths, projectPaths } from "../config/paths.ts";
|
|
4
|
+
import { walkFiles } from "../tools/walk.ts";
|
|
5
|
+
import { parseFrontmatter } from "../commands/custom.ts";
|
|
6
|
+
|
|
7
|
+
// A persona/behavior preset loaded from .privateer/output-styles/<name>.md. Its body
|
|
8
|
+
// replaces the default tone section of the system prompt while the tool policy,
|
|
9
|
+
// security stance, and environment grounding stay intact.
|
|
10
|
+
export interface OutputStyle {
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
body: string;
|
|
14
|
+
scope: "project" | "user";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function loadFromDir(dir: string, scope: "project" | "user"): OutputStyle[] {
|
|
18
|
+
if (!existsSync(dir)) return [];
|
|
19
|
+
const out: OutputStyle[] = [];
|
|
20
|
+
for (const rel of walkFiles(dir)) {
|
|
21
|
+
if (!rel.endsWith(".md")) continue;
|
|
22
|
+
const { meta, body } = parseFrontmatter(readFileSync(join(dir, rel), "utf8"));
|
|
23
|
+
out.push({
|
|
24
|
+
name: rel.replace(/\.md$/, "").split("/").join(":"),
|
|
25
|
+
description: meta.description,
|
|
26
|
+
body: body.trim(),
|
|
27
|
+
scope,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function loadOutputStyles(cwd: string = process.cwd()): OutputStyle[] {
|
|
34
|
+
const byName = new Map<string, OutputStyle>();
|
|
35
|
+
for (const s of loadFromDir(globalPaths().outputStyles, "user")) byName.set(s.name, s);
|
|
36
|
+
for (const s of loadFromDir(projectPaths(cwd).outputStyles, "project")) byName.set(s.name, s);
|
|
37
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function findOutputStyle(name: string, cwd: string = process.cwd()): OutputStyle | undefined {
|
|
41
|
+
return loadOutputStyles(cwd).find((s) => s.name === name);
|
|
42
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { walkFiles } from "../tools/walk.ts";
|
|
3
|
+
|
|
4
|
+
// Lightweight, synchronous environment probes used to enrich the system prompt at
|
|
5
|
+
// session start. Everything here fails soft: outside a git repo, or if `git` is
|
|
6
|
+
// missing, the git block is simply omitted rather than throwing.
|
|
7
|
+
|
|
8
|
+
function git(cwd: string, args: string[]): string | null {
|
|
9
|
+
try {
|
|
10
|
+
return execFileSync("git", args, {
|
|
11
|
+
cwd,
|
|
12
|
+
encoding: "utf8",
|
|
13
|
+
timeout: 3_000,
|
|
14
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
15
|
+
}).trim();
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface GitInfo {
|
|
22
|
+
branch: string;
|
|
23
|
+
status: string; // short porcelain, possibly truncated
|
|
24
|
+
recent: string; // last few commit subjects
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A compact git snapshot, or null when cwd isn't a working tree.
|
|
28
|
+
export function gitStatus(cwd: string): GitInfo | null {
|
|
29
|
+
const inside = git(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
30
|
+
if (inside !== "true") return null;
|
|
31
|
+
|
|
32
|
+
const branch = git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]) || "(detached)";
|
|
33
|
+
const raw = git(cwd, ["status", "--porcelain"]) ?? "";
|
|
34
|
+
const lines = raw ? raw.split("\n") : [];
|
|
35
|
+
const status =
|
|
36
|
+
lines.length === 0
|
|
37
|
+
? "(clean)"
|
|
38
|
+
: lines.slice(0, 20).join("\n") +
|
|
39
|
+
(lines.length > 20 ? `\n… (+${lines.length - 20} more)` : "");
|
|
40
|
+
const recent = git(cwd, ["log", "--oneline", "-5"]) ?? "";
|
|
41
|
+
|
|
42
|
+
return { branch, status, recent };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A shallow snapshot of the project's files (respecting walk's skip list), capped
|
|
46
|
+
// so the prompt stays small. Gives the model a sense of layout before it explores.
|
|
47
|
+
export function dirSnapshot(cwd: string, limit = 40): string {
|
|
48
|
+
let files: string[];
|
|
49
|
+
try {
|
|
50
|
+
files = walkFiles(cwd);
|
|
51
|
+
} catch {
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
if (files.length === 0) return "";
|
|
55
|
+
files.sort();
|
|
56
|
+
const shown = files.slice(0, limit);
|
|
57
|
+
const more = files.length > limit ? `\n… (+${files.length - limit} more files)` : "";
|
|
58
|
+
return shown.join("\n") + more;
|
|
59
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { gitStatus, dirSnapshot } from "./projectInfo.ts";
|
|
4
|
+
import { loadMemoryContext } from "../memory/auto.ts";
|
|
5
|
+
|
|
6
|
+
// The system prompt is assembled from modular sections: static segments first,
|
|
7
|
+
// then a dynamic environment block. Static sections (identity, tone, tool
|
|
8
|
+
// policy) come first so they stay byte-stable across turns and cache well; the
|
|
9
|
+
// dynamic environment block (cwd, git status, snapshot) comes last. `buildSystemPrompt`
|
|
10
|
+
// stays a pure synchronous string builder — the only I/O is reading project files and
|
|
11
|
+
// the soft git/dir probes in projectInfo.ts.
|
|
12
|
+
|
|
13
|
+
const IDENTITY = `You are Privateer, a provider-agnostic terminal coding agent. You help with software \
|
|
14
|
+
engineering tasks directly from the user's terminal, with the same working style as a senior \
|
|
15
|
+
engineer pairing over a shared shell.`;
|
|
16
|
+
|
|
17
|
+
const TONE = `Tone and style:
|
|
18
|
+
- Be concise and direct. Minimize preamble and postamble — no "Sure!", no "Here is what I'll do" \
|
|
19
|
+
unless asked. Let your actions and their results speak.
|
|
20
|
+
- Prefer doing over explaining. When a task is clear, use your tools to accomplish it rather than \
|
|
21
|
+
describing how the user could.
|
|
22
|
+
- Keep prose short. Answer the question that was asked; don't volunteer tangents.
|
|
23
|
+
- When you finish a task, stop. Don't summarize work the user just watched you do.`;
|
|
24
|
+
|
|
25
|
+
const SECURITY = `Security:
|
|
26
|
+
- Assist with defensive security, debugging, and legitimate engineering. Refuse to help create or \
|
|
27
|
+
improve malware, exploits aimed at systems the user doesn't own, or other clearly malicious uses.
|
|
28
|
+
- Never expose or exfiltrate secrets. Don't print API keys or credentials you encounter.`;
|
|
29
|
+
|
|
30
|
+
const TOOL_POLICY = `Using your tools:
|
|
31
|
+
- Explore before you change: use 'glob' to find files by name and 'grep' to search contents. \
|
|
32
|
+
Read a file with 'read' before editing it.
|
|
33
|
+
- Prefer 'edit' (exact-string replace) over 'write' for changes to existing files; reserve 'write' \
|
|
34
|
+
for new files or full rewrites.
|
|
35
|
+
- Batch independent reads/searches rather than going one at a time.
|
|
36
|
+
- For multi-step work, call 'todo' to lay out and track the plan; keep exactly one item \
|
|
37
|
+
in_progress and mark items completed as you finish them. This keeps the user oriented.
|
|
38
|
+
- For broad, open-ended search or investigation, delegate to a 'task' sub-agent so the details \
|
|
39
|
+
stay out of the main conversation; it returns just a summary.
|
|
40
|
+
- Use 'bash' for builds, tests, git, and other CLI work. Avoid long-running or interactive commands.
|
|
41
|
+
- Each tool call is a separate model round-trip, so chain related shell steps into one \
|
|
42
|
+
'bash' call with '&&' rather than firing them one at a time (e.g. \
|
|
43
|
+
'git add -A && git commit -m "…" && git push', not three calls). Keep commands you need \
|
|
44
|
+
to inspect the output of (a failing test, a diff you'll act on) separate.
|
|
45
|
+
- Scope a commit to how the user asked. An unqualified "commit" / "commit and push" means the \
|
|
46
|
+
whole working tree (stage all changes) — not just files you touched this turn. A scoped request \
|
|
47
|
+
("commit the README", "commit the screenshot") means stage only what that names. When an \
|
|
48
|
+
unqualified commit would sweep in a lot of unrelated changes, say what you're including in one line \
|
|
49
|
+
before doing it.
|
|
50
|
+
- When you create a git commit, end the message with a blank line followed by this trailer so the \
|
|
51
|
+
work is attributed to Privateer as a co-author:
|
|
52
|
+
Co-Authored-By: Privateer <291203302+privateer-first-mate@users.noreply.github.com>
|
|
53
|
+
- Use 'web_fetch' to read a known URL when the user provides one or you need current docs.
|
|
54
|
+
- Mutating actions (write/edit/bash) may require user approval; that's expected — proceed and let \
|
|
55
|
+
the gate handle it.`;
|
|
56
|
+
|
|
57
|
+
const MEMORY = `Memory:
|
|
58
|
+
- You have a persistent memory across sessions. When an index of saved memories is \
|
|
59
|
+
present below, treat each line as something you already know; read the named .md file \
|
|
60
|
+
with 'read' when an entry looks relevant to the current task.
|
|
61
|
+
- Use the 'memory' tool to record durable facts worth remembering long-term: stable user \
|
|
62
|
+
preferences, project conventions, and feedback on how to work — not transient details \
|
|
63
|
+
about the current task. Prefer updating an existing memory (reuse its name) over \
|
|
64
|
+
creating a near-duplicate. Default to project scope; use global only for facts that hold \
|
|
65
|
+
across every project.`;
|
|
66
|
+
|
|
67
|
+
const RECAP = `Recaps:
|
|
68
|
+
- End every response with a single final line that begins with "recap: " — one plain-language \
|
|
69
|
+
sentence summarizing what the user has accomplished so far this session (their goals and the \
|
|
70
|
+
changes they've driven), not a restatement of what you just did. Keep it to one line; if nothing \
|
|
71
|
+
has happened yet, recap the user's stated goal.`;
|
|
72
|
+
|
|
73
|
+
const PLAN_MODE = `Plan mode is active. Your write, edit, and bash tools are disabled — do not attempt to \
|
|
74
|
+
modify files or run commands. Investigate with read, glob, and grep, then present a clear, \
|
|
75
|
+
step-by-step implementation plan as your final message. Do not start implementing; wait for the \
|
|
76
|
+
user to approve the plan first.`;
|
|
77
|
+
|
|
78
|
+
export interface SystemPromptOptions {
|
|
79
|
+
cwd: string;
|
|
80
|
+
model: string;
|
|
81
|
+
// Optional output-style body: replaces the default tone/persona section.
|
|
82
|
+
outputStyleBody?: string;
|
|
83
|
+
// When true, append the plan-mode mandate (read-only, produce a plan).
|
|
84
|
+
planMode?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// System prompt for a `task` sub-agent: same environment grounding, but a read-only,
|
|
88
|
+
// report-back mandate. It shares the parent's identity/security stance but swaps the
|
|
89
|
+
// tool policy for the restricted subset.
|
|
90
|
+
export function buildSubAgentPrompt(opts: SystemPromptOptions & { description: string }): string {
|
|
91
|
+
return [
|
|
92
|
+
IDENTITY,
|
|
93
|
+
SECURITY,
|
|
94
|
+
`You are running as a read-only sub-agent for the task: "${opts.description}".`,
|
|
95
|
+
`You have read, glob, and grep only — you cannot modify files or run commands. Investigate ` +
|
|
96
|
+
`thoroughly and efficiently, then return a concise, self-contained summary of your findings ` +
|
|
97
|
+
`(reference concrete file paths and line numbers). Do not ask the user questions; you run ` +
|
|
98
|
+
`autonomously and your final message is your whole report.`,
|
|
99
|
+
`Environment:\n- cwd: ${opts.cwd}\n- platform: ${process.platform}`,
|
|
100
|
+
].join("\n\n");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// System prompt for a user-defined sub-agent: the agent's own instructions plus the
|
|
104
|
+
// shared identity/security stance and an autonomous report-back mandate.
|
|
105
|
+
export function buildAgentPrompt(
|
|
106
|
+
opts: SystemPromptOptions & { description: string; instructions: string },
|
|
107
|
+
): string {
|
|
108
|
+
return [
|
|
109
|
+
IDENTITY,
|
|
110
|
+
opts.instructions,
|
|
111
|
+
SECURITY,
|
|
112
|
+
`You are running as a sub-agent for the task: "${opts.description}". Work autonomously and ` +
|
|
113
|
+
`return a concise, self-contained final report (reference concrete file paths). Do not ask ` +
|
|
114
|
+
`the user questions — your final message is your whole report.`,
|
|
115
|
+
`Environment:\n- cwd: ${opts.cwd}\n- platform: ${process.platform}`,
|
|
116
|
+
].join("\n\n");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function buildSystemPrompt(opts: SystemPromptOptions): string {
|
|
120
|
+
// An active output style replaces the default tone/persona section.
|
|
121
|
+
const persona = opts.outputStyleBody?.trim() || TONE;
|
|
122
|
+
const parts: string[] = [IDENTITY, persona, SECURITY, TOOL_POLICY, MEMORY, RECAP];
|
|
123
|
+
if (opts.planMode) parts.push(PLAN_MODE);
|
|
124
|
+
|
|
125
|
+
// --- Dynamic environment section ---
|
|
126
|
+
const env: string[] = [
|
|
127
|
+
`Environment:`,
|
|
128
|
+
`- cwd: ${opts.cwd}`,
|
|
129
|
+
`- model: ${opts.model}`,
|
|
130
|
+
`- launched in: ${process.cwd()}`,
|
|
131
|
+
`- platform: ${process.platform}`,
|
|
132
|
+
`- date: ${new Date().toISOString().slice(0, 10)}`,
|
|
133
|
+
`\nTreat cwd as the project scope: interpret relative paths from it and keep your ` +
|
|
134
|
+
`exploration, searches, and edits inside it. The file tools are confined to cwd — a path ` +
|
|
135
|
+
`that resolves outside it (an absolute path elsewhere, a sibling directory, or a '../' ` +
|
|
136
|
+
`escape) is blocked unless the user has explicitly asked you to work there, in which case ` +
|
|
137
|
+
`they'll be prompted to approve it. Don't reach outside cwd on your own; if a task seems to ` +
|
|
138
|
+
`need a file outside it, ask the user rather than guessing.`,
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const git = gitStatus(opts.cwd);
|
|
142
|
+
if (git) {
|
|
143
|
+
env.push(`- git branch: ${git.branch}`);
|
|
144
|
+
env.push(`\nGit status (porcelain):\n${git.status}`);
|
|
145
|
+
if (git.recent) env.push(`\nRecent commits:\n${git.recent}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const snapshot = dirSnapshot(opts.cwd);
|
|
149
|
+
if (snapshot) env.push(`\nProject files (partial):\n${snapshot}`);
|
|
150
|
+
|
|
151
|
+
parts.push(env.join("\n"));
|
|
152
|
+
|
|
153
|
+
// Project context file, our CLAUDE.md analog. Loaded last so user-authored
|
|
154
|
+
// standing instructions carry the most weight.
|
|
155
|
+
const ctxFile = join(opts.cwd, "PRIVATEER.md");
|
|
156
|
+
if (existsSync(ctxFile)) {
|
|
157
|
+
parts.push(`Project context from PRIVATEER.md:\n${readFileSync(ctxFile, "utf8").trim()}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Recalled memory index, our auto-memory analog. Read a listed .md for full detail.
|
|
161
|
+
const memory = loadMemoryContext(opts.cwd);
|
|
162
|
+
if (memory) {
|
|
163
|
+
parts.push(`Persistent memory (index — read a file for detail):\n${memory}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return parts.join("\n\n");
|
|
167
|
+
}
|