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,203 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import TextInput from "ink-text-input";
|
|
4
|
+
import type { Config, ProviderName, ProviderConfig } from "../config/schema.ts";
|
|
5
|
+
import { PROVIDER_LIST, PROVIDER_META, type ProviderMeta } from "../providers/catalog.ts";
|
|
6
|
+
import { providerRequiresKey } from "../providers/registry.ts";
|
|
7
|
+
import { ModelPicker } from "./ModelPicker.tsx";
|
|
8
|
+
import { theme } from "./theme.ts";
|
|
9
|
+
import { WELCOME } from "./figures.ts";
|
|
10
|
+
|
|
11
|
+
// Which of the just-entered providers are actually usable (have a key, or are keyless).
|
|
12
|
+
function readyProviders(creds: Partial<Record<ProviderName, ProviderConfig>>): ProviderName[] {
|
|
13
|
+
return (Object.keys(creds) as ProviderName[]).filter((name) =>
|
|
14
|
+
providerRequiresKey(name) ? Boolean(creds[name]?.apiKey) : true,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface OnboardingResult {
|
|
19
|
+
providers: Partial<Record<ProviderName, ProviderConfig>>;
|
|
20
|
+
defaultModel: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Step 1 — multi-select the providers to configure. Arrows/jk move, space toggles,
|
|
24
|
+
// enter confirms (needs at least one).
|
|
25
|
+
function SelectStep({
|
|
26
|
+
initial,
|
|
27
|
+
onConfirm,
|
|
28
|
+
}: {
|
|
29
|
+
initial: Set<ProviderName>;
|
|
30
|
+
onConfirm: (selected: ProviderMeta[]) => void;
|
|
31
|
+
}) {
|
|
32
|
+
const [cursor, setCursor] = useState(0);
|
|
33
|
+
const [selected, setSelected] = useState<Set<ProviderName>>(new Set(initial));
|
|
34
|
+
|
|
35
|
+
useInput((input, key) => {
|
|
36
|
+
if (key.upArrow || input === "k") setCursor((c) => (c - 1 + PROVIDER_LIST.length) % PROVIDER_LIST.length);
|
|
37
|
+
else if (key.downArrow || input === "j") setCursor((c) => (c + 1) % PROVIDER_LIST.length);
|
|
38
|
+
else if (input === " ") {
|
|
39
|
+
const name = PROVIDER_LIST[cursor].name;
|
|
40
|
+
setSelected((s) => {
|
|
41
|
+
const next = new Set(s);
|
|
42
|
+
next.has(name) ? next.delete(name) : next.add(name);
|
|
43
|
+
return next;
|
|
44
|
+
});
|
|
45
|
+
} else if (key.return && selected.size > 0) {
|
|
46
|
+
onConfirm(PROVIDER_LIST.filter((p) => selected.has(p.name)));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Box flexDirection="column">
|
|
52
|
+
<Text color={theme.dim}>
|
|
53
|
+
Select the providers you want to use — <Text color={theme.accent}>space</Text> to toggle,{" "}
|
|
54
|
+
<Text color={theme.accent}>enter</Text> to continue.
|
|
55
|
+
</Text>
|
|
56
|
+
<Box flexDirection="column" marginTop={1}>
|
|
57
|
+
{PROVIDER_LIST.map((p, i) => {
|
|
58
|
+
const on = selected.has(p.name);
|
|
59
|
+
const active = i === cursor;
|
|
60
|
+
return (
|
|
61
|
+
<Text key={p.name} color={active ? theme.accent : undefined}>
|
|
62
|
+
{active ? "❯ " : " "}
|
|
63
|
+
{on ? "◉" : "○"} {p.label.padEnd(16)}
|
|
64
|
+
<Text color={theme.dim}> {p.requiresKey ? `key: ${p.keyHint}` : p.keyHint}</Text>
|
|
65
|
+
</Text>
|
|
66
|
+
);
|
|
67
|
+
})}
|
|
68
|
+
</Box>
|
|
69
|
+
{selected.size === 0 && (
|
|
70
|
+
<Text color={theme.dim}>{"\n"}Select at least one provider to continue.</Text>
|
|
71
|
+
)}
|
|
72
|
+
</Box>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Step 2 — walk the chosen providers one at a time, collecting a masked API key (or a
|
|
77
|
+
// base URL for keyless/local providers). Empty key = skip that provider's credential.
|
|
78
|
+
function KeyStep({
|
|
79
|
+
providers,
|
|
80
|
+
onDone,
|
|
81
|
+
}: {
|
|
82
|
+
providers: ProviderMeta[];
|
|
83
|
+
onDone: (creds: Partial<Record<ProviderName, ProviderConfig>>) => void;
|
|
84
|
+
}) {
|
|
85
|
+
const [index, setIndex] = useState(0);
|
|
86
|
+
const [value, setValue] = useState("");
|
|
87
|
+
const [creds, setCreds] = useState<Partial<Record<ProviderName, ProviderConfig>>>({});
|
|
88
|
+
const meta = providers[index];
|
|
89
|
+
|
|
90
|
+
function submit(raw: string) {
|
|
91
|
+
const v = raw.trim();
|
|
92
|
+
const entry: ProviderConfig = meta.requiresKey
|
|
93
|
+
? v
|
|
94
|
+
? { apiKey: v }
|
|
95
|
+
: {}
|
|
96
|
+
: { baseURL: v || meta.baseURLDefault };
|
|
97
|
+
const nextCreds = { ...creds, [meta.name]: entry };
|
|
98
|
+
setCreds(nextCreds);
|
|
99
|
+
setValue("");
|
|
100
|
+
if (index + 1 < providers.length) setIndex(index + 1);
|
|
101
|
+
else onDone(nextCreds);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Box flexDirection="column">
|
|
106
|
+
<Text color={theme.dim}>
|
|
107
|
+
Step {index + 1} of {providers.length} —{" "}
|
|
108
|
+
<Text color={theme.accent}>{meta.label}</Text>
|
|
109
|
+
</Text>
|
|
110
|
+
<Text color={theme.dim}>
|
|
111
|
+
{meta.requiresKey
|
|
112
|
+
? `Paste your API key (${meta.keyHint}). Enter to skip.`
|
|
113
|
+
: `Base URL for Ollama. Enter to use the default.`}
|
|
114
|
+
</Text>
|
|
115
|
+
<Box marginTop={1} borderStyle="round" borderColor={theme.accent} paddingX={1}>
|
|
116
|
+
<Text color={theme.accent}>{"> "}</Text>
|
|
117
|
+
<TextInput
|
|
118
|
+
value={value}
|
|
119
|
+
onChange={setValue}
|
|
120
|
+
onSubmit={submit}
|
|
121
|
+
mask={meta.requiresKey ? "*" : undefined}
|
|
122
|
+
placeholder={meta.requiresKey ? "sk-…" : meta.baseURLDefault}
|
|
123
|
+
/>
|
|
124
|
+
</Box>
|
|
125
|
+
</Box>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Step 3 — pick the default model from the providers just configured, with the model
|
|
130
|
+
// list fetched live using the entered keys. Esc (or a fetch failure) falls back to the
|
|
131
|
+
// provider's catalog default, so onboarding always completes.
|
|
132
|
+
function ModelStep({
|
|
133
|
+
creds,
|
|
134
|
+
ready,
|
|
135
|
+
fallback,
|
|
136
|
+
onDone,
|
|
137
|
+
}: {
|
|
138
|
+
creds: Partial<Record<ProviderName, ProviderConfig>>;
|
|
139
|
+
ready: ProviderName[];
|
|
140
|
+
fallback: string;
|
|
141
|
+
onDone: (spec: string) => void;
|
|
142
|
+
}) {
|
|
143
|
+
// ModelPicker only reads config.providers; a synthetic config is enough here.
|
|
144
|
+
const synthetic = { providers: creds } as Config;
|
|
145
|
+
return (
|
|
146
|
+
<Box flexDirection="column">
|
|
147
|
+
<Text color={theme.dim}>Choose your default model — you can change it later with /model.</Text>
|
|
148
|
+
<Box marginTop={1}>
|
|
149
|
+
<ModelPicker
|
|
150
|
+
config={synthetic}
|
|
151
|
+
providers={ready}
|
|
152
|
+
onSelect={onDone}
|
|
153
|
+
onCancel={() => onDone(fallback)}
|
|
154
|
+
/>
|
|
155
|
+
</Box>
|
|
156
|
+
</Box>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function Onboarding({
|
|
161
|
+
initialSelected = [],
|
|
162
|
+
onComplete,
|
|
163
|
+
}: {
|
|
164
|
+
initialSelected?: ProviderName[];
|
|
165
|
+
onComplete: (result: OnboardingResult) => void;
|
|
166
|
+
}) {
|
|
167
|
+
const [chosen, setChosen] = useState<ProviderMeta[] | null>(null);
|
|
168
|
+
const [creds, setCreds] = useState<Partial<Record<ProviderName, ProviderConfig>> | null>(null);
|
|
169
|
+
|
|
170
|
+
function onKeysDone(entered: Partial<Record<ProviderName, ProviderConfig>>) {
|
|
171
|
+
const ready = readyProviders(entered);
|
|
172
|
+
// No usable credentials (every key skipped): nothing to pick from — finish on the
|
|
173
|
+
// chosen provider's catalog default rather than showing an empty picker.
|
|
174
|
+
if (ready.length === 0) {
|
|
175
|
+
onComplete({ providers: entered, defaultModel: chosen![0].defaultModel });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
setCreds(entered);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<Box flexDirection="column" paddingX={1} paddingTop={1}>
|
|
183
|
+
<Text bold color={theme.accent}>
|
|
184
|
+
{WELCOME} Welcome to Privateer — let's set up your providers
|
|
185
|
+
</Text>
|
|
186
|
+
<Text color={theme.dim}>Bring your own keys. They're saved to ~/.privateer/config.json.</Text>
|
|
187
|
+
<Box marginTop={1}>
|
|
188
|
+
{chosen === null ? (
|
|
189
|
+
<SelectStep initial={new Set(initialSelected)} onConfirm={setChosen} />
|
|
190
|
+
) : creds === null ? (
|
|
191
|
+
<KeyStep providers={chosen} onDone={onKeysDone} />
|
|
192
|
+
) : (
|
|
193
|
+
<ModelStep
|
|
194
|
+
creds={creds}
|
|
195
|
+
ready={readyProviders(creds)}
|
|
196
|
+
fallback={PROVIDER_META[readyProviders(creds)[0]].defaultModel}
|
|
197
|
+
onDone={(spec) => onComplete({ providers: creds, defaultModel: spec })}
|
|
198
|
+
/>
|
|
199
|
+
)}
|
|
200
|
+
</Box>
|
|
201
|
+
</Box>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { theme } from "./theme.ts";
|
|
4
|
+
|
|
5
|
+
// Shown after a turn completes while in plan mode: the agent has presented a plan,
|
|
6
|
+
// and the user chooses to approve (leave plan mode), keep planning, or chat about
|
|
7
|
+
// the plan (return to the prompt, staying in plan mode, to ask questions).
|
|
8
|
+
export function PlanConfirm({
|
|
9
|
+
onApprove,
|
|
10
|
+
onKeep,
|
|
11
|
+
onChat,
|
|
12
|
+
}: {
|
|
13
|
+
onApprove: () => void;
|
|
14
|
+
onKeep: () => void;
|
|
15
|
+
onChat: () => void;
|
|
16
|
+
}) {
|
|
17
|
+
useInput((input, key) => {
|
|
18
|
+
if (input === "a" || key.return) onApprove();
|
|
19
|
+
else if (input === "c") onChat();
|
|
20
|
+
else if (input === "k" || key.escape) onKeep();
|
|
21
|
+
});
|
|
22
|
+
return (
|
|
23
|
+
<Box flexDirection="column" borderStyle="round" borderColor={theme.accent} paddingX={1}>
|
|
24
|
+
<Text color={theme.accent}>Plan ready — review it above.</Text>
|
|
25
|
+
<Text>
|
|
26
|
+
<Text color={theme.accent}>a</Text>
|
|
27
|
+
<Text color={theme.dim}> approve & exit plan mode</Text>
|
|
28
|
+
<Text color={theme.dim}>{" "}</Text>
|
|
29
|
+
<Text color={theme.accent}>c</Text>
|
|
30
|
+
<Text color={theme.dim}> chat about the plan</Text>
|
|
31
|
+
<Text color={theme.dim}>{" "}</Text>
|
|
32
|
+
<Text color={theme.accent}>k</Text>
|
|
33
|
+
<Text color={theme.dim}> keep planning</Text>
|
|
34
|
+
</Text>
|
|
35
|
+
</Box>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import Spinner from "ink-spinner";
|
|
4
|
+
import { theme } from "./theme.ts";
|
|
5
|
+
import { WELCOME } from "./figures.ts";
|
|
6
|
+
import { runDeviceLogin, type DeviceCode, type PrivateerUser } from "../auth/privateer.ts";
|
|
7
|
+
|
|
8
|
+
// Device-authorization login screen. Shows the user_code to approve in the
|
|
9
|
+
// Privateer app, polls in the background, and resolves once approved. Esc
|
|
10
|
+
// cancels. Works the same for email and wallet accounts — all the signing
|
|
11
|
+
// happens in the app, never here.
|
|
12
|
+
export function PrivateerLogin({
|
|
13
|
+
onComplete,
|
|
14
|
+
onCancel,
|
|
15
|
+
}: {
|
|
16
|
+
onComplete: (user: PrivateerUser) => void;
|
|
17
|
+
onCancel: () => void;
|
|
18
|
+
}) {
|
|
19
|
+
const [code, setCode] = useState<DeviceCode | null>(null);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
const [slow, setSlow] = useState(false);
|
|
22
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const ac = new AbortController();
|
|
26
|
+
abortRef.current = ac;
|
|
27
|
+
let cancelled = false;
|
|
28
|
+
|
|
29
|
+
runDeviceLogin({
|
|
30
|
+
onCode: (c) => !cancelled && setCode(c),
|
|
31
|
+
onPoll: (state) => !cancelled && setSlow(state === "slow_down"),
|
|
32
|
+
signal: ac.signal,
|
|
33
|
+
})
|
|
34
|
+
.then((user) => {
|
|
35
|
+
if (!cancelled) onComplete(user);
|
|
36
|
+
})
|
|
37
|
+
.catch((err) => {
|
|
38
|
+
if (cancelled) return;
|
|
39
|
+
if (ac.signal.aborted) return; // user cancelled — handled by useInput
|
|
40
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return () => {
|
|
44
|
+
cancelled = true;
|
|
45
|
+
ac.abort();
|
|
46
|
+
};
|
|
47
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
useInput((_input, key) => {
|
|
51
|
+
if (key.escape || (key.return && error)) {
|
|
52
|
+
abortRef.current?.abort();
|
|
53
|
+
onCancel();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Box flexDirection="column" paddingX={1} paddingTop={1}>
|
|
59
|
+
<Text bold color={theme.accent}>
|
|
60
|
+
{WELCOME} Sign in to your Privateer account
|
|
61
|
+
</Text>
|
|
62
|
+
<Text color={theme.dim}>
|
|
63
|
+
Inference runs on your account and is billed to your subscription — no API key needed.
|
|
64
|
+
</Text>
|
|
65
|
+
|
|
66
|
+
{error ? (
|
|
67
|
+
<Box flexDirection="column" marginTop={1}>
|
|
68
|
+
<Text color={theme.error ?? "red"}>{error}</Text>
|
|
69
|
+
<Text color={theme.dim}>Press Esc or Enter to go back.</Text>
|
|
70
|
+
</Box>
|
|
71
|
+
) : !code ? (
|
|
72
|
+
<Box marginTop={1}>
|
|
73
|
+
<Text color={theme.accent}>
|
|
74
|
+
<Spinner type="dots" />
|
|
75
|
+
</Text>
|
|
76
|
+
<Text color={theme.dim}> Starting secure login…</Text>
|
|
77
|
+
</Box>
|
|
78
|
+
) : (
|
|
79
|
+
<Box flexDirection="column" marginTop={1}>
|
|
80
|
+
<Text color={theme.dim}>1. Open the Privateer app (or web) where you're signed in.</Text>
|
|
81
|
+
<Text color={theme.dim}>
|
|
82
|
+
2. Go to <Text color={theme.accent}>Settings → Link a terminal</Text>
|
|
83
|
+
{code.verification_uri ? (
|
|
84
|
+
<Text color={theme.dim}>
|
|
85
|
+
{" "}
|
|
86
|
+
({code.verification_uri})
|
|
87
|
+
</Text>
|
|
88
|
+
) : null}
|
|
89
|
+
</Text>
|
|
90
|
+
<Text color={theme.dim}>3. Enter this code (or just open the link above):</Text>
|
|
91
|
+
<Box marginY={1} borderStyle="round" borderColor={theme.accent} paddingX={2}>
|
|
92
|
+
<Text bold color={theme.accent}>
|
|
93
|
+
{code.user_code}
|
|
94
|
+
</Text>
|
|
95
|
+
</Box>
|
|
96
|
+
{/* Deliberately NOT an animated spinner here: ink-spinner re-renders the
|
|
97
|
+
whole frame ~12×/s, which wipes any text selection on the code above
|
|
98
|
+
and makes it almost impossible to copy. A static line keeps the code
|
|
99
|
+
selectable; this view only re-renders on a real state change. */}
|
|
100
|
+
<Box>
|
|
101
|
+
<Text color={theme.dim}>
|
|
102
|
+
Waiting for approval…{slow ? " (slowing down)" : ""} · Esc to cancel
|
|
103
|
+
</Text>
|
|
104
|
+
</Box>
|
|
105
|
+
</Box>
|
|
106
|
+
)}
|
|
107
|
+
</Box>
|
|
108
|
+
);
|
|
109
|
+
}
|