@giovannijecha/jecode 0.8.5 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -9
- package/assets/tokenizers/LICENSE +21 -0
- package/assets/tokenizers/o200k-base.tiktoken.gz +0 -0
- package/dist/cli-info.js +8 -14
- package/dist/commands.js +7 -3
- package/dist/config.js +47 -53
- package/dist/context/automatic.js +16 -1
- package/dist/context/budget.js +5 -2
- package/dist/context/compactor.js +91 -37
- package/dist/context/diagnostics.js +108 -0
- package/dist/context/lifetime.js +18 -0
- package/dist/context/manager.js +67 -0
- package/dist/context/manual.js +11 -10
- package/dist/context/measurement.js +75 -0
- package/dist/context/policy.js +13 -10
- package/dist/context/request-observation.js +46 -0
- package/dist/context/request-projection.js +8 -34
- package/dist/context/request.js +22 -0
- package/dist/context/tokenizer/bpe.js +68 -0
- package/dist/context/tokenizer/o200k.js +54 -0
- package/dist/context/tokenizer/vocabulary.js +34 -0
- package/dist/controller-request.js +47 -45
- package/dist/controller.js +13 -2
- package/dist/credential-commands.js +3 -3
- package/dist/input-boundary.js +0 -62
- package/dist/launch.js +13 -9
- package/dist/model-command.js +7 -17
- package/dist/oauth-result-page.js +68 -0
- package/dist/openai-account-command.js +14 -12
- package/dist/openai-account.js +7 -5
- package/dist/openai-oauth-callback.js +15 -54
- package/dist/openai-oauth-tokens.js +9 -7
- package/dist/openai-oauth.js +8 -6
- package/dist/permission-command.js +9 -24
- package/dist/permissions.js +10 -8
- package/dist/provider-commands.js +1 -33
- package/dist/provider-errors.js +1 -10
- package/dist/provider-label.js +3 -10
- package/dist/providers/anthropic-wire.js +1 -1
- package/dist/providers/anthropic.js +3 -2
- package/dist/providers/index.js +1 -5
- package/dist/providers/input-measurement.js +85 -0
- package/dist/providers/ollama-context.js +42 -0
- package/dist/providers/ollama-endpoint.js +7 -34
- package/dist/providers/ollama-wire.js +1 -1
- package/dist/providers/ollama.js +15 -147
- package/dist/providers/openai-codex.js +7 -4
- package/dist/providers/openai-stream.js +20 -8
- package/dist/providers/openai-summary.js +37 -0
- package/dist/providers/openai-wire.js +1 -1
- package/dist/providers/openai.js +3 -1
- package/dist/sessions/bucket.js +55 -0
- package/dist/sessions/catalog-io.js +162 -0
- package/dist/sessions/catalog.js +3 -1
- package/dist/sessions/codec-messages.js +122 -0
- package/dist/sessions/codec-transcript.js +93 -0
- package/dist/sessions/codec-values.js +52 -0
- package/dist/sessions/codec.js +4 -257
- package/dist/sessions/files.js +158 -0
- package/dist/sessions/load.js +90 -0
- package/dist/sessions/snapshot.js +33 -0
- package/dist/sessions/store.js +42 -461
- package/dist/settings.js +17 -16
- package/dist/start.js +36 -53
- package/dist/timeline.js +2 -0
- package/dist/tools/file-read.js +192 -0
- package/dist/tools/file-summary.js +9 -0
- package/dist/tools/{fs.js → file-write.js} +5 -193
- package/dist/tools/glob.js +107 -0
- package/dist/tools/index.js +2 -1
- package/dist/tools/search.js +1 -105
- package/dist/tui/activity.js +1 -1
- package/dist/tui/app-input.js +34 -17
- package/dist/tui/app-workflows.js +13 -361
- package/dist/tui/app.js +55 -28
- package/dist/tui/approve.js +3 -2
- package/dist/tui/blocks.js +1 -2
- package/dist/tui/command-workflow.js +112 -0
- package/dist/tui/components/command-menu.js +7 -10
- package/dist/tui/components/menu.js +82 -43
- package/dist/tui/components/messages.js +16 -9
- package/dist/tui/components/status.js +1 -1
- package/dist/tui/components/tool-evidence.js +107 -0
- package/dist/tui/components/tool-motion.js +32 -0
- package/dist/tui/components/tool.js +48 -202
- package/dist/tui/frame.js +8 -3
- package/dist/tui/help.js +3 -2
- package/dist/tui/picker-layout.js +44 -0
- package/dist/tui/picker.js +7 -71
- package/dist/tui/screen.js +7 -0
- package/dist/tui/session-view.js +0 -1
- package/dist/tui/tool-details.js +135 -0
- package/dist/tui/transcript-grammar.js +1 -1
- package/dist/tui/transcript-view.js +30 -112
- package/dist/tui/turn-workflow.js +235 -0
- package/dist/tui/turn.js +7 -140
- package/dist/tui/view.js +4 -4
- package/dist/tui/workflow-types.js +2 -0
- package/dist/ui/render.js +0 -4
- package/package.json +16 -12
- package/dist/batch-view.js +0 -17
- package/dist/batch.js +0 -267
- package/dist/ollama-settings-command.js +0 -74
- package/dist/tui/motion.js +0 -32
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Loopback callback used by the browser OAuth flow.
|
|
2
2
|
import { timingSafeEqual } from "node:crypto";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
3
|
import { createServer } from "node:http";
|
|
5
4
|
import { leadingText } from "./text-boundary.js";
|
|
5
|
+
import { providerLabel } from "./provider-label.js";
|
|
6
|
+
import { oauthResultPage } from "./oauth-result-page.js";
|
|
7
|
+
const ACCOUNT_LABEL = providerLabel("openai-codex");
|
|
6
8
|
const CALLBACK_PORTS = [1455, 1457];
|
|
7
9
|
export const OPENAI_CALLBACK_PATH = "/auth/callback";
|
|
8
10
|
export async function openAICallback(state) {
|
|
@@ -26,7 +28,14 @@ export async function openAICallback(state) {
|
|
|
26
28
|
outgoing.writeHead(400).end("Invalid sign-in callback.");
|
|
27
29
|
return;
|
|
28
30
|
}
|
|
29
|
-
|
|
31
|
+
let incoming;
|
|
32
|
+
try {
|
|
33
|
+
incoming = new URL(request.url, "http://localhost");
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
outgoing.writeHead(400).end("Invalid sign-in callback.");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
30
39
|
if (incoming.pathname !== OPENAI_CALLBACK_PATH) {
|
|
31
40
|
outgoing.writeHead(404).end("Not found.");
|
|
32
41
|
return;
|
|
@@ -43,10 +52,10 @@ export async function openAICallback(state) {
|
|
|
43
52
|
const authError = incoming.searchParams.get("error_description") ?? incoming.searchParams.get("error");
|
|
44
53
|
const authorizationCode = incoming.searchParams.get("code");
|
|
45
54
|
if (authError !== null) {
|
|
46
|
-
rejectCode(new Error(
|
|
55
|
+
rejectCode(new Error(`${ACCOUNT_LABEL} sign-in was rejected · ${leadingText(authError, 300)}`));
|
|
47
56
|
}
|
|
48
57
|
else if (authorizationCode === null || authorizationCode === "") {
|
|
49
|
-
rejectCode(new Error(
|
|
58
|
+
rejectCode(new Error(`${ACCOUNT_LABEL} sign-in returned no authorization code`));
|
|
50
59
|
}
|
|
51
60
|
else {
|
|
52
61
|
resolveCode(authorizationCode);
|
|
@@ -71,7 +80,7 @@ export async function openAICallback(state) {
|
|
|
71
80
|
"referrer-policy": "no-referrer",
|
|
72
81
|
"x-content-type-options": "nosniff",
|
|
73
82
|
});
|
|
74
|
-
response.end(
|
|
83
|
+
response.end(oauthResultPage(success));
|
|
75
84
|
await flushed;
|
|
76
85
|
},
|
|
77
86
|
close: () => closeServer(listening.server),
|
|
@@ -94,7 +103,7 @@ async function firstAvailableServer(handler) {
|
|
|
94
103
|
throw error;
|
|
95
104
|
}
|
|
96
105
|
}
|
|
97
|
-
throw new Error(
|
|
106
|
+
throw new Error(`${ACCOUNT_LABEL} sign-in could not open callback ports 1455 or 1457`);
|
|
98
107
|
}
|
|
99
108
|
function listen(server, port) {
|
|
100
109
|
return new Promise((resolve, reject) => {
|
|
@@ -130,54 +139,6 @@ function closeServer(server) {
|
|
|
130
139
|
server.closeIdleConnections();
|
|
131
140
|
});
|
|
132
141
|
}
|
|
133
|
-
function resultPage(success) {
|
|
134
|
-
const title = success ? "Signed in to Jecode" : "Jecode sign-in failed";
|
|
135
|
-
const status = success ? "Authentication complete" : "Authentication stopped";
|
|
136
|
-
const detail = success
|
|
137
|
-
? "Return to your terminal. Jecode will continue automatically."
|
|
138
|
-
: "Return to your terminal to see what stopped the connection.";
|
|
139
|
-
const state = success ? "success" : "failure";
|
|
140
|
-
return `<!doctype html>
|
|
141
|
-
<html lang="en">
|
|
142
|
-
<head>
|
|
143
|
-
<meta charset="utf-8">
|
|
144
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
145
|
-
<meta name="color-scheme" content="dark">
|
|
146
|
-
<title>${title}</title>
|
|
147
|
-
<style>
|
|
148
|
-
:root{--night:#000;--steel:#669bd2;--steel-soft:#8db4dd;--bright:#ebeff4;--danger:#e87070}
|
|
149
|
-
*{box-sizing:border-box}
|
|
150
|
-
body{margin:0;min-height:100vh;display:grid;place-items:center;background:var(--night);color:var(--steel-soft);font-family:"Segoe UI",system-ui,sans-serif}
|
|
151
|
-
main{width:min(34rem,calc(100vw - 3rem));padding:3rem 1.5rem;text-align:center}
|
|
152
|
-
img{display:block;width:clamp(7.5rem,20vw,10rem);height:auto;margin:0 auto 1.75rem;filter:drop-shadow(0 1.25rem 2rem rgba(102,155,210,.16))}
|
|
153
|
-
.rail{width:min(18rem,70vw);height:1px;margin:0 auto 1.5rem;background:linear-gradient(90deg,transparent,var(--steel),transparent)}
|
|
154
|
-
.status{margin:0 0 .75rem;color:var(--steel);font:600 .72rem/1.2 ui-monospace,"Cascadia Mono",monospace;letter-spacing:.14em;text-transform:uppercase}
|
|
155
|
-
h1{margin:0;color:var(--steel);font-size:clamp(2rem,6vw,3.25rem);font-weight:720;letter-spacing:-.04em;line-height:1.05}
|
|
156
|
-
p:last-of-type{max-width:30rem;margin:1.25rem auto 0;color:var(--steel-soft);font-size:1.05rem;line-height:1.6}
|
|
157
|
-
.failure h1,.failure .status{color:var(--danger)}
|
|
158
|
-
@media (prefers-reduced-motion:no-preference){main{animation:arrive .45s ease-out both}@keyframes arrive{from{opacity:0;transform:translateY(.6rem)}to{opacity:1;transform:none}}}
|
|
159
|
-
</style>
|
|
160
|
-
</head>
|
|
161
|
-
<body>
|
|
162
|
-
<main class="${state}">
|
|
163
|
-
<img src="${mascotDataUri()}" alt="Jeco, the Jecode gecko">
|
|
164
|
-
<div class="rail" aria-hidden="true"></div>
|
|
165
|
-
<p class="status">${status}</p>
|
|
166
|
-
<h1>${title}</h1>
|
|
167
|
-
<p>${detail}</p>
|
|
168
|
-
</main>
|
|
169
|
-
<script>history.replaceState(null,"","/auth/complete")</script>
|
|
170
|
-
</body>
|
|
171
|
-
</html>`;
|
|
172
|
-
}
|
|
173
|
-
let mascot;
|
|
174
|
-
function mascotDataUri() {
|
|
175
|
-
if (mascot === undefined) {
|
|
176
|
-
const file = new URL("../assets/jeco-256.png", import.meta.url);
|
|
177
|
-
mascot = `data:image/png;base64,${readFileSync(file).toString("base64")}`;
|
|
178
|
-
}
|
|
179
|
-
return mascot;
|
|
180
|
-
}
|
|
181
142
|
function sameState(expected, received) {
|
|
182
143
|
if (received === null)
|
|
183
144
|
return false;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
// Validate OAuth token responses and extract the ChatGPT account claims.
|
|
2
|
+
import { providerLabel } from "./provider-label.js";
|
|
3
|
+
const ACCOUNT_LABEL = providerLabel("openai-codex");
|
|
2
4
|
const CLAIMS = "https://api.openai.com/auth";
|
|
3
5
|
export function openAITokenReply(value, previousRefresh) {
|
|
4
6
|
if (!record(value))
|
|
5
|
-
throw new Error(
|
|
7
|
+
throw new Error(`${ACCOUNT_LABEL} sign-in returned an invalid token response`);
|
|
6
8
|
const accessToken = required(value["access_token"], "access token");
|
|
7
9
|
const refreshToken = optional(value["refresh_token"]) ?? previousRefresh;
|
|
8
10
|
const expiresIn = value["expires_in"];
|
|
9
11
|
if (refreshToken === undefined)
|
|
10
|
-
throw new Error(
|
|
12
|
+
throw new Error(`${ACCOUNT_LABEL} sign-in did not return a refresh token`);
|
|
11
13
|
if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0) {
|
|
12
|
-
throw new Error(
|
|
14
|
+
throw new Error(`${ACCOUNT_LABEL} sign-in did not return a valid token lifetime`);
|
|
13
15
|
}
|
|
14
16
|
const idToken = optional(value["id_token"]);
|
|
15
17
|
return {
|
|
@@ -25,7 +27,7 @@ export function openAIAccountFromTokens(token) {
|
|
|
25
27
|
const auth = record(access[CLAIMS]) ? access[CLAIMS] : {};
|
|
26
28
|
const accountId = optional(auth["chatgpt_account_id"]) ?? optional(identity?.["chatgpt_account_id"]);
|
|
27
29
|
if (accountId === undefined)
|
|
28
|
-
throw new Error(
|
|
30
|
+
throw new Error(`${ACCOUNT_LABEL} sign-in did not identify an account`);
|
|
29
31
|
const email = optional(identity?.["email"]);
|
|
30
32
|
const plan = optional(auth["chatgpt_plan_type"]) ?? optional(identity?.["chatgpt_plan_type"]);
|
|
31
33
|
return {
|
|
@@ -40,7 +42,7 @@ export function openAIAccountFromTokens(token) {
|
|
|
40
42
|
function jwt(token) {
|
|
41
43
|
const part = token.split(".")[1];
|
|
42
44
|
if (part === undefined)
|
|
43
|
-
throw new Error(
|
|
45
|
+
throw new Error(`${ACCOUNT_LABEL} sign-in returned an unreadable token`);
|
|
44
46
|
try {
|
|
45
47
|
const value = JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
|
|
46
48
|
if (!record(value))
|
|
@@ -48,13 +50,13 @@ function jwt(token) {
|
|
|
48
50
|
return value;
|
|
49
51
|
}
|
|
50
52
|
catch {
|
|
51
|
-
throw new Error(
|
|
53
|
+
throw new Error(`${ACCOUNT_LABEL} sign-in returned an unreadable token`);
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
function required(value, label) {
|
|
55
57
|
const found = optional(value);
|
|
56
58
|
if (found === undefined)
|
|
57
|
-
throw new Error(
|
|
59
|
+
throw new Error(`${ACCOUNT_LABEL} sign-in returned no ${label}`);
|
|
58
60
|
return found;
|
|
59
61
|
}
|
|
60
62
|
function optional(value) {
|
package/dist/openai-oauth.js
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
// an account on disk.
|
|
6
6
|
import { createHash, randomBytes } from "node:crypto";
|
|
7
7
|
import { oauthRequest } from "./oauth-http.js";
|
|
8
|
+
import { providerLabel } from "./provider-label.js";
|
|
8
9
|
import { OPENAI_CALLBACK_PATH, openAICallback } from "./openai-oauth-callback.js";
|
|
9
10
|
import { openAIAccountFromTokens, openAITokenReply, } from "./openai-oauth-tokens.js";
|
|
11
|
+
const ACCOUNT_LABEL = providerLabel("openai-codex");
|
|
10
12
|
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
11
13
|
const AUTHORITY = "https://auth.openai.com";
|
|
12
14
|
const AUTHORIZE = `${AUTHORITY}/oauth/authorize`;
|
|
@@ -91,7 +93,7 @@ export async function refreshOpenAITokens(account, signal) {
|
|
|
91
93
|
const token = openAITokenReply(response.value, account.refreshToken);
|
|
92
94
|
const refreshed = openAIAccountFromTokens(token);
|
|
93
95
|
if (refreshed.accountId !== account.accountId) {
|
|
94
|
-
throw new Error(
|
|
96
|
+
throw new Error(`${ACCOUNT_LABEL} refresh returned a different account · sign in again`);
|
|
95
97
|
}
|
|
96
98
|
return {
|
|
97
99
|
...refreshed,
|
|
@@ -125,7 +127,7 @@ async function exchange(code, signal) {
|
|
|
125
127
|
async function pollDevice(deviceAuthId, userCode, interval, signal) {
|
|
126
128
|
const deadline = new AbortController();
|
|
127
129
|
const timer = setTimeout(() => {
|
|
128
|
-
deadline.abort(new Error(
|
|
130
|
+
deadline.abort(new Error(`${ACCOUNT_LABEL} device sign-in timed out after 15 minutes`));
|
|
129
131
|
}, LOGIN_LIMIT_MS);
|
|
130
132
|
const combined = signal === undefined
|
|
131
133
|
? deadline.signal
|
|
@@ -149,10 +151,10 @@ async function pollDevice(deviceAuthId, userCode, interval, signal) {
|
|
|
149
151
|
}
|
|
150
152
|
const errorCode = deviceErrorCode(response.value);
|
|
151
153
|
if (errorCode === "access_denied") {
|
|
152
|
-
throw new Error(
|
|
154
|
+
throw new Error(`${ACCOUNT_LABEL} device sign-in was denied`);
|
|
153
155
|
}
|
|
154
156
|
if (errorCode === "expired_token") {
|
|
155
|
-
throw new Error(
|
|
157
|
+
throw new Error(`${ACCOUNT_LABEL} device sign-in code expired`);
|
|
156
158
|
}
|
|
157
159
|
const pending = errorCode === "authorization_pending" ||
|
|
158
160
|
errorCode === "deviceauth_authorization_pending" ||
|
|
@@ -160,7 +162,7 @@ async function pollDevice(deviceAuthId, userCode, interval, signal) {
|
|
|
160
162
|
const slowDown = errorCode === "slow_down" ||
|
|
161
163
|
(response.status === 429 && errorCode === undefined);
|
|
162
164
|
if (!pending && !slowDown) {
|
|
163
|
-
throw new Error(
|
|
165
|
+
throw new Error(`${ACCOUNT_LABEL} device sign-in failed (${response.status})`);
|
|
164
166
|
}
|
|
165
167
|
if (slowDown)
|
|
166
168
|
intervalMs += SLOW_DOWN_INCREMENT_MS;
|
|
@@ -192,7 +194,7 @@ function intervalSeconds(value) {
|
|
|
192
194
|
function required(value, label) {
|
|
193
195
|
const found = optional(value);
|
|
194
196
|
if (found === undefined)
|
|
195
|
-
throw new Error(
|
|
197
|
+
throw new Error(`${ACCOUNT_LABEL} sign-in returned no ${label}`);
|
|
196
198
|
return found;
|
|
197
199
|
}
|
|
198
200
|
function optional(value) {
|
|
@@ -9,17 +9,13 @@ export async function permissionsCommand(session, host) {
|
|
|
9
9
|
}
|
|
10
10
|
let selected = 0;
|
|
11
11
|
while (true) {
|
|
12
|
-
const index = await choose(permissionControlPicker(control,
|
|
12
|
+
const index = await choose(permissionControlPicker(control, selected));
|
|
13
13
|
if (index === undefined)
|
|
14
14
|
return;
|
|
15
15
|
const tool = control.listTools()[index];
|
|
16
16
|
if (tool === undefined)
|
|
17
17
|
return;
|
|
18
18
|
selected = index;
|
|
19
|
-
if (tool.locked) {
|
|
20
|
-
lockedNotice(tool.name, host);
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
19
|
if (tool.remembered > 0) {
|
|
24
20
|
await reviewGrants(tool.name, control, choose, session.palette);
|
|
25
21
|
}
|
|
@@ -29,12 +25,12 @@ export function permissionsPicker(tools, index = 0, adjust) {
|
|
|
29
25
|
return {
|
|
30
26
|
title: [],
|
|
31
27
|
options: tools.map((tool) => {
|
|
32
|
-
const
|
|
28
|
+
const hint = toolHint(tool);
|
|
33
29
|
return {
|
|
34
30
|
label: tool.name,
|
|
35
|
-
...(
|
|
36
|
-
value: tool.
|
|
37
|
-
adjustable:
|
|
31
|
+
...(hint === undefined ? {} : { hint }),
|
|
32
|
+
value: tool.mode,
|
|
33
|
+
adjustable: true,
|
|
38
34
|
};
|
|
39
35
|
}),
|
|
40
36
|
visible: tools.length,
|
|
@@ -42,21 +38,17 @@ export function permissionsPicker(tools, index = 0, adjust) {
|
|
|
42
38
|
index: Math.min(Math.max(0, index), Math.max(0, tools.length - 1)),
|
|
43
39
|
};
|
|
44
40
|
}
|
|
45
|
-
function permissionControlPicker(control,
|
|
41
|
+
export function permissionControlPicker(control, selected) {
|
|
46
42
|
return permissionsPicker(control.listTools(), selected, (index, step) => {
|
|
47
43
|
const tool = control.listTools()[index];
|
|
48
44
|
if (tool === undefined)
|
|
49
|
-
return permissionControlPicker(control,
|
|
50
|
-
if (tool.locked) {
|
|
51
|
-
lockedNotice(tool.name, host);
|
|
52
|
-
return permissionControlPicker(control, host, index);
|
|
53
|
-
}
|
|
45
|
+
return permissionControlPicker(control, selected);
|
|
54
46
|
const modes = modesFor(tool);
|
|
55
47
|
const at = Math.max(0, modes.indexOf(tool.mode));
|
|
56
48
|
const next = modes[(at + step + modes.length) % modes.length];
|
|
57
49
|
if (next !== undefined)
|
|
58
50
|
control.set(tool.name, next);
|
|
59
|
-
return permissionControlPicker(control,
|
|
51
|
+
return permissionControlPicker(control, index);
|
|
60
52
|
});
|
|
61
53
|
}
|
|
62
54
|
async function reviewGrants(tool, control, choose, pal) {
|
|
@@ -90,17 +82,10 @@ async function reviewGrants(tool, control, choose, pal) {
|
|
|
90
82
|
function modesFor(tool) {
|
|
91
83
|
return tool.dangerous ? ["ask", "allow", "deny"] : ["allow", "deny"];
|
|
92
84
|
}
|
|
93
|
-
function
|
|
85
|
+
function toolHint(tool) {
|
|
94
86
|
const parts = [
|
|
95
87
|
tool.dangerous ? undefined : "read only",
|
|
96
88
|
tool.remembered === 0 ? undefined : `${tool.remembered} remembered`,
|
|
97
89
|
].filter((part) => part !== undefined);
|
|
98
90
|
return parts.length === 0 ? undefined : parts.join(" · ");
|
|
99
91
|
}
|
|
100
|
-
function lockedNotice(tool, host) {
|
|
101
|
-
host.emit({
|
|
102
|
-
kind: "notice",
|
|
103
|
-
text: `restart without --auto-approve to change ${tool}`,
|
|
104
|
-
tone: "warn",
|
|
105
|
-
});
|
|
106
|
-
}
|
package/dist/permissions.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
// Session-local tool policies and remembered approval scopes.
|
|
2
2
|
/** One permission control plane for one interactive process. */
|
|
3
|
-
export function sessionPermissions(tools
|
|
3
|
+
export function sessionPermissions(tools) {
|
|
4
4
|
const catalogue = [...tools];
|
|
5
5
|
const byName = new Map(catalogue.map((tool) => [tool.name, tool]));
|
|
6
6
|
const modes = new Map();
|
|
7
7
|
const grants = new Map();
|
|
8
8
|
const configured = (tool) => modes.get(tool.name) ?? defaultMode(tool);
|
|
9
|
-
const effective = (tool) => autoApprove && tool.dangerous ? "allow" : configured(tool);
|
|
10
9
|
const revokeTool = (name) => {
|
|
11
10
|
for (const [key, grant] of grants) {
|
|
12
11
|
if (grant.tools.includes(name))
|
|
@@ -18,14 +17,13 @@ export function sessionPermissions(tools, autoApprove) {
|
|
|
18
17
|
return catalogue.map((tool) => ({
|
|
19
18
|
name: tool.name,
|
|
20
19
|
dangerous: tool.dangerous,
|
|
21
|
-
mode:
|
|
20
|
+
mode: configured(tool),
|
|
22
21
|
remembered: [...grants.values()].filter((grant) => grant.tools.includes(tool.name)).length,
|
|
23
|
-
locked: autoApprove && tool.dangerous,
|
|
24
22
|
}));
|
|
25
23
|
},
|
|
26
24
|
set(name, mode) {
|
|
27
25
|
const tool = byName.get(name);
|
|
28
|
-
if (tool === undefined
|
|
26
|
+
if (tool === undefined)
|
|
29
27
|
return false;
|
|
30
28
|
if (!tool.dangerous && mode === "ask")
|
|
31
29
|
return false;
|
|
@@ -50,13 +48,17 @@ export function sessionPermissions(tools, autoApprove) {
|
|
|
50
48
|
grants.clear();
|
|
51
49
|
},
|
|
52
50
|
availableTools() {
|
|
53
|
-
return catalogue.filter((tool) =>
|
|
51
|
+
return catalogue.filter((tool) => configured(tool) !== "deny");
|
|
52
|
+
},
|
|
53
|
+
allowed(call) {
|
|
54
|
+
const tool = byName.get(call.name);
|
|
55
|
+
return tool !== undefined && configured(tool) !== "deny";
|
|
54
56
|
},
|
|
55
57
|
approved(call) {
|
|
56
58
|
const tool = byName.get(call.name);
|
|
57
59
|
if (tool === undefined)
|
|
58
60
|
return false;
|
|
59
|
-
const mode =
|
|
61
|
+
const mode = configured(tool);
|
|
60
62
|
if (mode === "allow")
|
|
61
63
|
return true;
|
|
62
64
|
if (mode === "deny")
|
|
@@ -65,7 +67,7 @@ export function sessionPermissions(tools, autoApprove) {
|
|
|
65
67
|
},
|
|
66
68
|
remember(call) {
|
|
67
69
|
const tool = byName.get(call.name);
|
|
68
|
-
if (tool === undefined ||
|
|
70
|
+
if (tool === undefined || configured(tool) !== "ask")
|
|
69
71
|
return;
|
|
70
72
|
const scope = scopeFor(call);
|
|
71
73
|
grants.set(scope.key, { key: scope.key, tools: grantTools(call), label: scope.summary });
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
// Provider access and connection management. Runtime selection lives in /models.
|
|
2
|
-
import { saveCommandSettings } from "./command-settings.js";
|
|
3
2
|
import { apiKeyCommand } from "./credential-commands.js";
|
|
4
3
|
import { credentialSource } from "./credentials.js";
|
|
5
|
-
import { ollamaConnectionHint, ollamaConnectionSetting, } from "./ollama-settings-command.js";
|
|
6
4
|
import { openAIAccountHint } from "./openai-account.js";
|
|
7
5
|
import { openAIAccountCommand } from "./openai-account-command.js";
|
|
8
6
|
import { providerLabel, providerRouteLabel } from "./provider-label.js";
|
|
9
7
|
import { PROVIDERS } from "./providers/index.js";
|
|
10
|
-
import { ollamaConnection } from "./providers/ollama.js";
|
|
11
8
|
import { heading } from "./tui/picker.js";
|
|
12
|
-
|
|
13
|
-
/** The single control plane for API keys, OAuth accounts, and Ollama endpoints. */
|
|
9
|
+
/** The single control plane for API keys and OAuth accounts. */
|
|
14
10
|
export async function providersCommand(session, host) {
|
|
15
11
|
const choose = chooser(host);
|
|
16
12
|
if (choose === undefined)
|
|
@@ -81,45 +77,17 @@ function providerGroupHint(group, activeId) {
|
|
|
81
77
|
return active === undefined ? count : `${count} · current ${providerRouteLabel(active)}`;
|
|
82
78
|
}
|
|
83
79
|
export function providerAccessHint(provider) {
|
|
84
|
-
if (provider.id === "ollama") {
|
|
85
|
-
const connection = ollamaConnection();
|
|
86
|
-
if (connection.loopback)
|
|
87
|
-
return `${ollamaConnectionHint()} · no key needed`;
|
|
88
|
-
return `${ollamaConnectionHint()} · API key ${credentialSource(OLLAMA_KEY) ?? "missing"}`;
|
|
89
|
-
}
|
|
90
80
|
if (provider.auth.kind === "oauth")
|
|
91
81
|
return openAIAccountHint();
|
|
92
82
|
return `API key · ${credentialSource(provider.auth.keyVar) ?? "missing"}`;
|
|
93
83
|
}
|
|
94
84
|
async function manageProvider(provider, session, host) {
|
|
95
|
-
if (provider.id === "ollama") {
|
|
96
|
-
await ollamaProviderCommand(session, host);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
85
|
if (provider.auth.kind === "oauth") {
|
|
100
86
|
await openAIAccountCommand(session, host);
|
|
101
87
|
return;
|
|
102
88
|
}
|
|
103
89
|
await apiKeyCommand(provider.auth.keyVar, providerLabel(provider.id), session, host, provider.id);
|
|
104
90
|
}
|
|
105
|
-
async function ollamaProviderCommand(session, host) {
|
|
106
|
-
if (host.choose === undefined)
|
|
107
|
-
return;
|
|
108
|
-
const index = await host.choose({
|
|
109
|
-
title: heading("Ollama", "access and connection", session.palette),
|
|
110
|
-
options: [
|
|
111
|
-
{ label: "connection", hint: ollamaConnectionHint() },
|
|
112
|
-
{ label: "API key", hint: credentialSource(OLLAMA_KEY) ?? "missing" },
|
|
113
|
-
],
|
|
114
|
-
index: 0,
|
|
115
|
-
});
|
|
116
|
-
if (index === 0) {
|
|
117
|
-
await ollamaConnectionSetting(session, host, (patch) => saveCommandSettings(host, patch));
|
|
118
|
-
}
|
|
119
|
-
else if (index === 1) {
|
|
120
|
-
await apiKeyCommand(OLLAMA_KEY, "Ollama", session, host, "ollama");
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
91
|
function chooser(host) {
|
|
124
92
|
if (host.choose === undefined) {
|
|
125
93
|
host.emit({ kind: "notice", text: "that command needs the screen", tone: "warn" });
|
package/dist/provider-errors.js
CHANGED
|
@@ -4,21 +4,12 @@ import { providerLabel } from "./provider-label.js";
|
|
|
4
4
|
import { leadingText } from "./text-boundary.js";
|
|
5
5
|
import { terminalText } from "./ui/terminal-text.js";
|
|
6
6
|
import { providerFailureDetails, } from "./providers/failure.js";
|
|
7
|
-
const CONNECTION_FAILURE = /network error calling|timed out waiting for response headers|response body was idle/i;
|
|
8
7
|
const MAX_MESSAGE_CHARS = 1_000;
|
|
9
8
|
const MAX_REASON_CHARS = 500;
|
|
10
9
|
const HTML = /<!doctype\s|<\/?[a-z][^>]*>/i;
|
|
11
10
|
export function providerFailure(provider, error, labelProvider = false) {
|
|
12
11
|
const details = providerFailureDetails(provider.id, error);
|
|
13
|
-
let failure;
|
|
14
|
-
if (provider.id === "ollama" && details.kind === "network" && CONNECTION_FAILURE.test(error.message)) {
|
|
15
|
-
failure = provider.location?.() === "local"
|
|
16
|
-
? "Ollama is not reachable on this computer · start Ollama or choose cloud in /providers"
|
|
17
|
-
: "Ollama is not reachable · check its connection in /providers";
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
20
|
-
failure = actionableFailure(provider.id, details.kind) ?? rawFailure(error);
|
|
21
|
-
}
|
|
12
|
+
let failure = actionableFailure(provider.id, details.kind) ?? rawFailure(error);
|
|
22
13
|
if (details.requestId !== undefined)
|
|
23
14
|
failure += ` · request ${details.requestId}`;
|
|
24
15
|
const label = providerLabel(provider.id);
|
package/dist/provider-label.js
CHANGED
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
// Stable human-facing names for provider identifiers.
|
|
2
2
|
export function providerLabel(id) {
|
|
3
3
|
switch (id) {
|
|
4
|
-
case "anthropic": return "Anthropic";
|
|
4
|
+
case "anthropic": return "Anthropic API";
|
|
5
5
|
case "openai": return "OpenAI API";
|
|
6
|
-
case "openai-codex": return "
|
|
7
|
-
case "ollama": return "Ollama";
|
|
6
|
+
case "openai-codex": return "OpenAI Account";
|
|
7
|
+
case "ollama": return "Ollama API";
|
|
8
8
|
default: return id === "" ? "Provider" : `${id[0]?.toUpperCase() ?? ""}${id.slice(1)}`;
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
/** Name the actual connection a model request will use. */
|
|
12
12
|
export function providerRouteLabel(provider) {
|
|
13
|
-
if (provider.auth.kind === "oauth")
|
|
14
|
-
return `${providerLabel(provider.id)} account`;
|
|
15
13
|
if (provider.id === "openai")
|
|
16
14
|
return "OpenAI API · billed usage";
|
|
17
|
-
if (provider.id === "anthropic")
|
|
18
|
-
return "Anthropic API";
|
|
19
|
-
if (provider.id === "ollama") {
|
|
20
|
-
return provider.location?.() === "local" ? "Ollama · local" : "Ollama API";
|
|
21
|
-
}
|
|
22
15
|
return providerLabel(provider.id);
|
|
23
16
|
}
|
|
@@ -38,7 +38,7 @@ export function stopNotice(data) {
|
|
|
38
38
|
return `[refused: ${category}] ${why}`;
|
|
39
39
|
}
|
|
40
40
|
if (data.stop_reason === "max_tokens") {
|
|
41
|
-
return "[truncated: hit max_tokens — raise
|
|
41
|
+
return "[truncated: hit max_tokens — raise max output tokens in /settings]";
|
|
42
42
|
}
|
|
43
43
|
return undefined;
|
|
44
44
|
}
|
|
@@ -13,6 +13,7 @@ import { EFFORTS, requireSupportedEffort } from "../effort.js";
|
|
|
13
13
|
import { assembleAnthropic } from "./anthropic-stream.js";
|
|
14
14
|
import { isRetryableGenerationFailure, isRetryableReadFailure, throwProviderError, } from "./failure.js";
|
|
15
15
|
import { fromWireResponse, stopNotice, toWireMessage, toWireTool } from "./anthropic-wire.js";
|
|
16
|
+
import { measureAnthropicInput } from "./input-measurement.js";
|
|
16
17
|
const ENDPOINT = "https://api.anthropic.com/v1/messages";
|
|
17
18
|
const MODELS = "https://api.anthropic.com/v1/models?limit=100";
|
|
18
19
|
const API_VERSION = "2023-06-01";
|
|
@@ -36,7 +37,7 @@ export function anthropicEfforts(model) {
|
|
|
36
37
|
export const anthropic = {
|
|
37
38
|
id: ID,
|
|
38
39
|
// Sonnet is the default because it is the one that can be left running.
|
|
39
|
-
//
|
|
40
|
+
// Other models are selected through the shared model catalogue.
|
|
40
41
|
defaultModel: "claude-sonnet-5",
|
|
41
42
|
auth: { kind: "api-key", keyVar: KEY },
|
|
42
43
|
blocked() {
|
|
@@ -72,7 +73,7 @@ export const anthropic = {
|
|
|
72
73
|
throwProviderError(ID, signal, error);
|
|
73
74
|
}
|
|
74
75
|
},
|
|
75
|
-
|
|
76
|
+
measureInput: measureAnthropicInput,
|
|
76
77
|
async send(req) {
|
|
77
78
|
const key = requireKey();
|
|
78
79
|
const body = {
|
package/dist/providers/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { anthropic } from "./anthropic.js";
|
|
2
2
|
import { openai } from "./openai.js";
|
|
3
3
|
import { openaiCodex } from "./openai-codex.js";
|
|
4
|
-
import {
|
|
4
|
+
import { ollama } from "./ollama.js";
|
|
5
5
|
export const PROVIDERS = [anthropic, openai, openaiCodex, ollama];
|
|
6
6
|
export function providerNames() {
|
|
7
7
|
return PROVIDERS.map((provider) => provider.id);
|
|
@@ -13,7 +13,3 @@ export function selectProvider(id) {
|
|
|
13
13
|
}
|
|
14
14
|
return found;
|
|
15
15
|
}
|
|
16
|
-
/** Apply provider-specific process state after startup precedence is resolved. */
|
|
17
|
-
export function configureProviders(config) {
|
|
18
|
-
configureOllama(config.ollamaHost);
|
|
19
|
-
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Measure the content each adapter actually sends, without exposing wire
|
|
2
|
+
// formats to context policy or counting the normalized and raw copies twice.
|
|
3
|
+
import { estimateSerializedTokensResponsive } from "../context/estimate.js";
|
|
4
|
+
import { toWireItems, toWireTool as responsesTool } from "./openai-wire.js";
|
|
5
|
+
import { toWireMessage, toWireTool as anthropicTool } from "./anthropic-wire.js";
|
|
6
|
+
import { toWireMessages, toWireTool as ollamaTool } from "./ollama-wire.js";
|
|
7
|
+
import { countO200k } from "../context/tokenizer/o200k.js";
|
|
8
|
+
export function measureResponsesInput(request, providerId, signal) {
|
|
9
|
+
return measure({ instructions: request.system, tools: request.tools.map(responsesTool) }, request.messages.map((message) => {
|
|
10
|
+
const items = toWireItems(message, providerId);
|
|
11
|
+
const outputTokens = opaqueReserve(message, providerId);
|
|
12
|
+
if (outputTokens === undefined)
|
|
13
|
+
return { items,
|
|
14
|
+
unmeasuredOpaque: items.some((item) => hasOpaqueField(item, "reasoning", "encrypted_content")) };
|
|
15
|
+
return {
|
|
16
|
+
items: items.map((item) => withoutOpaqueField(item, "reasoning", "encrypted_content")),
|
|
17
|
+
outputTokens,
|
|
18
|
+
};
|
|
19
|
+
}), request.tools.length, signal, responsesTokenCounter(request.model, providerId));
|
|
20
|
+
}
|
|
21
|
+
export function measureAnthropicInput(request, signal) {
|
|
22
|
+
return measure({ system: request.system, tools: request.tools.map(anthropicTool) }, request.messages.map((message) => {
|
|
23
|
+
const wire = toWireMessage(message);
|
|
24
|
+
const outputTokens = opaqueReserve(message, "anthropic");
|
|
25
|
+
if (outputTokens === undefined || !Array.isArray(wire.content))
|
|
26
|
+
return { items: [wire] };
|
|
27
|
+
return {
|
|
28
|
+
items: [{
|
|
29
|
+
...wire,
|
|
30
|
+
content: wire.content.map((item) => withoutOpaqueField(withoutOpaqueField(item, "thinking", "signature"), "redacted_thinking", "data")),
|
|
31
|
+
}],
|
|
32
|
+
outputTokens,
|
|
33
|
+
};
|
|
34
|
+
}), request.tools.length, signal);
|
|
35
|
+
}
|
|
36
|
+
export function measureOllamaInput(request, signal) {
|
|
37
|
+
return measure({ messages: toWireMessages(request.system, []), tools: request.tools.map(ollamaTool) }, request.messages.map((message) => ({ items: toWireMessages("", [message]) })), request.tools.length, signal);
|
|
38
|
+
}
|
|
39
|
+
async function measure(envelope, messages, toolCount, signal, count = estimateSerializedTokensResponsive) {
|
|
40
|
+
let tokens = 64 + toolCount * 16 + await count(envelope, signal);
|
|
41
|
+
for (const message of messages) {
|
|
42
|
+
if (message.items.length === 0)
|
|
43
|
+
continue;
|
|
44
|
+
const visible = await (message.unmeasuredOpaque ? estimateSerializedTokensResponsive : count)(message.items, signal);
|
|
45
|
+
// Reported output already includes hidden reasoning. Reserve it once for
|
|
46
|
+
// this assistant message, never once per opaque item or in addition to it.
|
|
47
|
+
tokens += Math.max(visible, message.outputTokens ?? 0) + message.items.length * 8;
|
|
48
|
+
}
|
|
49
|
+
return tokens;
|
|
50
|
+
}
|
|
51
|
+
/** Account aliases may precede tiktoken's model map: use a reference encoding,
|
|
52
|
+
* never label this a provider-exact count. Legacy/unknown API routes stay conservative. */
|
|
53
|
+
function responsesTokenCounter(model, providerId) {
|
|
54
|
+
if (responsesTokenization(model, providerId) === "heuristic")
|
|
55
|
+
return estimateSerializedTokensResponsive;
|
|
56
|
+
return async (value, signal) => Math.max(1, Math.ceil(await countO200k(JSON.stringify(value), signal) * 1.1));
|
|
57
|
+
}
|
|
58
|
+
export function responsesTokenization(model, providerId) {
|
|
59
|
+
const modern = /^(?:gpt-5|gpt-4\.[15](?:-|$)|gpt-4o(?:-|$)|chatgpt-4o-|o[13](?:-|$)|o4-mini(?:-|$)|ft:gpt-4o)/u.test(model);
|
|
60
|
+
return providerId === "openai-codex" || modern ? "o200k-reference" : "heuristic";
|
|
61
|
+
}
|
|
62
|
+
function opaqueReserve(message, providerId) {
|
|
63
|
+
const tokens = message.usage?.outputTokens;
|
|
64
|
+
return message.role === "assistant" && message.rawFrom === providerId &&
|
|
65
|
+
Array.isArray(message.raw) && typeof tokens === "number" &&
|
|
66
|
+
Number.isSafeInteger(tokens) && tokens > 0 && message.raw.some((item) => (providerId === "anthropic"
|
|
67
|
+
? hasOpaqueField(item, "thinking", "signature") ||
|
|
68
|
+
hasOpaqueField(item, "redacted_thinking", "data")
|
|
69
|
+
: hasOpaqueField(item, "reasoning", "encrypted_content")))
|
|
70
|
+
? tokens
|
|
71
|
+
: undefined;
|
|
72
|
+
}
|
|
73
|
+
function withoutOpaqueField(value, type, field) {
|
|
74
|
+
if (!hasOpaqueField(value, type, field))
|
|
75
|
+
return value;
|
|
76
|
+
const visible = { ...value };
|
|
77
|
+
delete visible[field];
|
|
78
|
+
return visible;
|
|
79
|
+
}
|
|
80
|
+
function hasOpaqueField(value, type, field) {
|
|
81
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
82
|
+
return false;
|
|
83
|
+
const item = value;
|
|
84
|
+
return item["type"] === type && typeof item[field] === "string";
|
|
85
|
+
}
|