@vincemakes/kiso-code 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/credentials.d.ts +39 -0
- package/dist/auth/credentials.js +170 -0
- package/dist/auth/oauth/chatgpt.d.ts +40 -0
- package/dist/auth/oauth/chatgpt.js +197 -0
- package/dist/auth/oauth/index.d.ts +32 -0
- package/dist/auth/oauth/index.js +1 -0
- package/dist/auth/oauth/pkce.d.ts +5 -0
- package/dist/auth/oauth/pkce.js +12 -0
- package/dist/auth/refresh.d.ts +12 -0
- package/dist/auth/refresh.js +40 -0
- package/dist/auth/token.d.ts +17 -0
- package/dist/auth/token.js +26 -0
- package/dist/chat.d.ts +1 -1
- package/dist/chat.js +12 -6
- package/dist/config.d.ts +47 -6
- package/dist/config.js +83 -12
- package/dist/dispatch.js +45 -8
- package/dist/index.js +130 -4
- package/dist/mode.d.ts +2 -2
- package/dist/mode.js +2 -2
- package/dist/state.d.ts +7 -2
- package/dist/state.js +5 -1
- package/dist/trust-ui.d.ts +13 -0
- package/dist/trust-ui.js +17 -2
- package/package.json +15 -14
package/dist/config.d.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Schema v1:
|
|
10
10
|
* model?: string — a profile NAME (models.<name>) or "provider/model"
|
|
11
|
-
* direct write (provider: "openai-compat" |
|
|
12
|
-
*
|
|
11
|
+
* direct write (provider: "openai-compat" |
|
|
12
|
+
* "anthropic" | "openai-responses")
|
|
13
|
+
* models?: { [name]: { kind: "openai-compat"|"anthropic"|"openai-responses",
|
|
13
14
|
* baseUrl?: string, model: string, apiKeyEnv: string } }
|
|
14
15
|
* mode?: "manual"|"default"|"accept-edits"|"plan"|"bypass"
|
|
15
16
|
* contextWindow?: number — tokens
|
|
@@ -27,7 +28,11 @@
|
|
|
27
28
|
* ignored (forward compatibility).
|
|
28
29
|
*/
|
|
29
30
|
import type { Mode } from "./mode.js";
|
|
30
|
-
|
|
31
|
+
/** OR-1: `openai-responses` is a DIALECT, not a vendor — the same kind
|
|
32
|
+
* drives the first-party Responses API and the ChatGPT subscription
|
|
33
|
+
* backend, and which one it is follows the profile's baseUrl (and so
|
|
34
|
+
* the credential it resolves to). */
|
|
35
|
+
export type ProfileKind = "openai-compat" | "anthropic" | "openai-responses";
|
|
31
36
|
export interface ModelProfile {
|
|
32
37
|
readonly kind: ProfileKind;
|
|
33
38
|
readonly baseUrl?: string;
|
|
@@ -59,6 +64,11 @@ export interface KisoConfig {
|
|
|
59
64
|
* project file is a LOUD error rather than a silent win. `KISO_THEME`
|
|
60
65
|
* still outranks it — the environment is the more local answer. */
|
|
61
66
|
readonly theme?: "dark" | "light";
|
|
67
|
+
/** DT-1a: named acceptance checks a delegated task may reference —
|
|
68
|
+
* user-authored (or trust-gated project) commands, run by the PARENT
|
|
69
|
+
* in the child's worktree. A model never supplies a command; it names
|
|
70
|
+
* one of these. `{ "test": "npm test", "lint": "npm run lint" }`. */
|
|
71
|
+
readonly checks?: Readonly<Record<string, string>>;
|
|
62
72
|
}
|
|
63
73
|
/** The resolved, merged config — project wins over user, both validated. */
|
|
64
74
|
export interface ResolvedConfig {
|
|
@@ -77,13 +87,44 @@ export declare function loadUserConfig(): KisoConfig | null;
|
|
|
77
87
|
export declare function loadProjectConfig(cwd: string, trusted: boolean): KisoConfig | null;
|
|
78
88
|
/** Merge: project wins over user (each layer's own keys only). */
|
|
79
89
|
export declare function mergeConfigs(user: KisoConfig | null, project: KisoConfig | null): KisoConfig;
|
|
80
|
-
/**
|
|
90
|
+
/** What a profile's sign-in actually IS. The two shapes are not
|
|
91
|
+
* interchangeable: an API key is a string an adapter holds, an OAuth
|
|
92
|
+
* sign-in is a token that expires and must be re-resolved per request. */
|
|
93
|
+
export type ProfileAuth = {
|
|
94
|
+
readonly type: "api-key";
|
|
95
|
+
readonly apiKey: string;
|
|
96
|
+
readonly source: "store" | "env" | "none";
|
|
97
|
+
} | {
|
|
98
|
+
readonly type: "oauth";
|
|
99
|
+
readonly providerId: string;
|
|
100
|
+
};
|
|
101
|
+
/** Where a profile's sign-in comes from, or why it has none. The sign-in
|
|
102
|
+
* plan's resolve rule: a stored credential OWNS the provider (an
|
|
103
|
+
* unusable stored one is an error, never a silent fall back to env);
|
|
104
|
+
* with nothing stored, the profile's env var applies; a keyless profile
|
|
105
|
+
* (no apiKeyEnv — PH-1c, finding PH-F19) is an unauthenticated endpoint.
|
|
106
|
+
*
|
|
107
|
+
* OR-1: a stored OAuth credential is USABLE only by an adapter that can
|
|
108
|
+
* drive one, which today is the Responses adapter's ChatGPT target. For
|
|
109
|
+
* every other kind it stays the same loud refusal it has always been —
|
|
110
|
+
* the caller is told which sign-in it has and which one it needs. */
|
|
111
|
+
export declare function authForProfile(name: string, p: ModelProfile): ProfileAuth;
|
|
112
|
+
/** The reason a profile is unavailable, for the surfaces that print it. */
|
|
113
|
+
export declare function unavailableReason(name: string, p: ModelProfile): string;
|
|
114
|
+
/** A profile is available when a sign-in resolves for it — of EITHER
|
|
115
|
+
* shape: an OAuth-only profile is available, and calling the key-only
|
|
116
|
+
* resolver here would have declared it broken. */
|
|
81
117
|
export declare function profileAvailable(p: ModelProfile): boolean;
|
|
82
|
-
/** The resolved runtime model — what the adapter is built from.
|
|
118
|
+
/** The resolved runtime model — what the adapter is built from. Exactly
|
|
119
|
+
* one of the two sign-in fields is set: an OAuth profile has no key to
|
|
120
|
+
* hand over, and the caller must build the adapter from the token thunk
|
|
121
|
+
* instead (the compiler enforces the branch — `apiKey` is optional). */
|
|
83
122
|
export interface ResolvedModel {
|
|
84
123
|
readonly name: string;
|
|
85
124
|
readonly profile: ModelProfile;
|
|
86
|
-
readonly apiKey
|
|
125
|
+
readonly apiKey?: string;
|
|
126
|
+
/** OR-1: the provider whose stored OAuth sign-in this profile uses. */
|
|
127
|
+
readonly oauthProviderId?: string;
|
|
87
128
|
}
|
|
88
129
|
/** "provider/model" direct write → a profile. */
|
|
89
130
|
export declare function directWriteProfile(value: string): ModelProfile | null;
|
package/dist/config.js
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Schema v1:
|
|
10
10
|
* model?: string — a profile NAME (models.<name>) or "provider/model"
|
|
11
|
-
* direct write (provider: "openai-compat" |
|
|
12
|
-
*
|
|
11
|
+
* direct write (provider: "openai-compat" |
|
|
12
|
+
* "anthropic" | "openai-responses")
|
|
13
|
+
* models?: { [name]: { kind: "openai-compat"|"anthropic"|"openai-responses",
|
|
13
14
|
* baseUrl?: string, model: string, apiKeyEnv: string } }
|
|
14
15
|
* mode?: "manual"|"default"|"accept-edits"|"plan"|"bypass"
|
|
15
16
|
* contextWindow?: number — tokens
|
|
@@ -29,7 +30,8 @@
|
|
|
29
30
|
import { readFileSync } from "node:fs";
|
|
30
31
|
import { join } from "node:path";
|
|
31
32
|
import { kisoHome } from "./state.js";
|
|
32
|
-
|
|
33
|
+
import { AuthError, getCredential, providerIdOf } from "./auth/credentials.js";
|
|
34
|
+
const KINDS = ["openai-compat", "anthropic", "openai-responses"];
|
|
33
35
|
const MODES_LIST = ["manual", "default", "accept-edits", "plan", "bypass"];
|
|
34
36
|
export class ConfigError extends Error {
|
|
35
37
|
}
|
|
@@ -63,6 +65,17 @@ export function parseConfig(text, source) {
|
|
|
63
65
|
fail("theme", "belongs in the USER config — a terminal is a property of the person at it, not of the project");
|
|
64
66
|
out.theme = obj.theme;
|
|
65
67
|
}
|
|
68
|
+
if (obj.checks !== undefined) {
|
|
69
|
+
if (obj.checks === null || typeof obj.checks !== "object" || Array.isArray(obj.checks))
|
|
70
|
+
fail("checks", "expected an object of name → command");
|
|
71
|
+
for (const [name, cmd] of Object.entries(obj.checks)) {
|
|
72
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name))
|
|
73
|
+
fail(`checks.${name}`, "a check name is letters, digits, _ or -");
|
|
74
|
+
if (typeof cmd !== "string" || cmd.trim() === "")
|
|
75
|
+
fail(`checks.${name}`, "expected a non-empty command string");
|
|
76
|
+
}
|
|
77
|
+
out.checks = obj.checks;
|
|
78
|
+
}
|
|
66
79
|
if (obj.model !== undefined) {
|
|
67
80
|
if (typeof obj.model !== "string" || obj.model === "")
|
|
68
81
|
fail("model", "expected a profile name or provider/model string");
|
|
@@ -162,13 +175,69 @@ export function mergeConfigs(user, project) {
|
|
|
162
175
|
...(p.autoCompact !== undefined ? { autoCompact: p.autoCompact } : {}),
|
|
163
176
|
...(u.projectTrust !== undefined ? { projectTrust: u.projectTrust } : {}),
|
|
164
177
|
...(p.projectTrust !== undefined ? { projectTrust: p.projectTrust } : {}),
|
|
178
|
+
// DT-1a: checks merge per name — a (trusted) project's check wins over the user's
|
|
179
|
+
...(u.checks !== undefined || p.checks !== undefined ? { checks: { ...(u.checks ?? {}), ...(p.checks ?? {}) } } : {}),
|
|
165
180
|
};
|
|
166
181
|
}
|
|
167
|
-
/**
|
|
182
|
+
/** Where a profile's sign-in comes from, or why it has none. The sign-in
|
|
183
|
+
* plan's resolve rule: a stored credential OWNS the provider (an
|
|
184
|
+
* unusable stored one is an error, never a silent fall back to env);
|
|
185
|
+
* with nothing stored, the profile's env var applies; a keyless profile
|
|
186
|
+
* (no apiKeyEnv — PH-1c, finding PH-F19) is an unauthenticated endpoint.
|
|
187
|
+
*
|
|
188
|
+
* OR-1: a stored OAuth credential is USABLE only by an adapter that can
|
|
189
|
+
* drive one, which today is the Responses adapter's ChatGPT target. For
|
|
190
|
+
* every other kind it stays the same loud refusal it has always been —
|
|
191
|
+
* the caller is told which sign-in it has and which one it needs. */
|
|
192
|
+
export function authForProfile(name, p) {
|
|
193
|
+
const providerId = providerIdOf(p.kind, p.baseUrl);
|
|
194
|
+
if (providerId !== null) {
|
|
195
|
+
let stored;
|
|
196
|
+
try {
|
|
197
|
+
stored = getCredential(providerId);
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
if (err instanceof AuthError)
|
|
201
|
+
throw new ConfigError(`model ${name}: ${err.message}`);
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
if (stored !== undefined) {
|
|
205
|
+
if (stored.type === "api-key")
|
|
206
|
+
return { type: "api-key", apiKey: stored.key, source: "store" };
|
|
207
|
+
if (p.kind === "openai-responses")
|
|
208
|
+
return { type: "oauth", providerId };
|
|
209
|
+
throw new ConfigError(`model ${name}: signed in to ${providerId} with OAuth, but this profile's adapter needs an API key — run \`kiso login ${providerId}\` with a key, or \`kiso logout ${providerId}\` to use the env var ${p.apiKeyEnv ?? "(none configured)"}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (p.apiKeyEnv === undefined)
|
|
213
|
+
return { type: "api-key", apiKey: "none", source: "none" };
|
|
214
|
+
const fromEnv = process.env[p.apiKeyEnv];
|
|
215
|
+
if (fromEnv !== undefined)
|
|
216
|
+
return { type: "api-key", apiKey: fromEnv, source: "env" };
|
|
217
|
+
const hint = providerId !== null ? `run \`kiso login ${providerId}\` or set the env var ${p.apiKeyEnv}` : `set the env var ${p.apiKeyEnv}`;
|
|
218
|
+
throw new ConfigError(`model ${name}: unavailable — no credential: ${hint} (configs never store keys, only the env-var name)`);
|
|
219
|
+
}
|
|
220
|
+
/** The reason a profile is unavailable, for the surfaces that print it. */
|
|
221
|
+
export function unavailableReason(name, p) {
|
|
222
|
+
try {
|
|
223
|
+
authForProfile(name, p);
|
|
224
|
+
return "";
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
return err instanceof Error ? err.message : String(err);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/** A profile is available when a sign-in resolves for it — of EITHER
|
|
231
|
+
* shape: an OAuth-only profile is available, and calling the key-only
|
|
232
|
+
* resolver here would have declared it broken. */
|
|
168
233
|
export function profileAvailable(p) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
234
|
+
try {
|
|
235
|
+
authForProfile("?", p);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
172
241
|
}
|
|
173
242
|
/** "provider/model" direct write → a profile. */
|
|
174
243
|
export function directWriteProfile(value) {
|
|
@@ -176,11 +245,15 @@ export function directWriteProfile(value) {
|
|
|
176
245
|
if (slash <= 0 || slash === value.length - 1)
|
|
177
246
|
return null;
|
|
178
247
|
const kind = value.slice(0, slash);
|
|
179
|
-
if (kind !== "openai-compat" && kind !== "anthropic")
|
|
248
|
+
if (kind !== "openai-compat" && kind !== "anthropic" && kind !== "openai-responses")
|
|
180
249
|
return null;
|
|
181
250
|
return {
|
|
182
251
|
kind,
|
|
183
252
|
model: value.slice(slash + 1),
|
|
253
|
+
// OR-1: a direct-write `openai-responses/…` names no baseUrl, so it
|
|
254
|
+
// is the FIRST-PARTY target and its env var is OpenAI's. The
|
|
255
|
+
// ChatGPT backend needs a baseUrl and a stored sign-in, which is a
|
|
256
|
+
// configured profile's job, not a one-line direct write.
|
|
184
257
|
apiKeyEnv: kind === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY",
|
|
185
258
|
};
|
|
186
259
|
}
|
|
@@ -243,12 +316,10 @@ export function resolveModel(modelFlag, merged) {
|
|
|
243
316
|
return null;
|
|
244
317
|
}
|
|
245
318
|
function resolveProfile(name, p) {
|
|
246
|
-
if (!profileAvailable(p)) {
|
|
247
|
-
throw new ConfigError(`model ${name}: unavailable — the env var ${p.apiKeyEnv} is not set (configs never store keys, only the env-var name)`);
|
|
248
|
-
}
|
|
249
319
|
// PH-1c (finding PH-F19): a keyless profile hands the adapter a
|
|
250
320
|
// placeholder — the SDKs require SOME string; the endpoint ignores it.
|
|
251
|
-
|
|
321
|
+
const auth = authForProfile(name, p);
|
|
322
|
+
return auth.type === "oauth" ? { name, profile: p, oauthProviderId: auth.providerId } : { name, profile: p, apiKey: auth.apiKey };
|
|
252
323
|
}
|
|
253
324
|
/** Mode: env (KISO_MODE) beats config.mode; the --mode flag is applied by
|
|
254
325
|
* main before this runs (flags are the top of the chain). */
|
package/dist/dispatch.js
CHANGED
|
@@ -5,11 +5,37 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { contextRows, contextUnavailableRows, displayVerb, escapeTerminal, helpRows, kUnit, modePickView, modelPickView, palette } from "@vincemakes/kiso-tui";
|
|
7
7
|
import { newSessionId } from "./session-id.js";
|
|
8
|
-
import { buildAdapter, resolveContinuationScope, resolveReasoning } from "@vincemakes/kiso-runtime/internal";
|
|
8
|
+
import { buildAdapter, lookupModelMetadata, resolveContinuationScope, resolveReasoning } from "@vincemakes/kiso-runtime/internal";
|
|
9
9
|
import { MODES, MODE_NOTE, getMode, setMode } from "./mode.js";
|
|
10
10
|
import { clipboardWrite, lastAnswer } from "./clipboard.js";
|
|
11
11
|
import { agentModel, body, bodyLog, configModels, dock, readContextLedger, sessionsDir, setAgentModel, setCurrentModelName, setLastBinding } from "./state.js";
|
|
12
|
-
import { directWriteProfile, profileAvailable } from "./config.js";
|
|
12
|
+
import { authForProfile, directWriteProfile, profileAvailable, unavailableReason } from "./config.js";
|
|
13
|
+
/** The picker's CLI half (owner 2026-09-08): a profile's LEGAL effort levels
|
|
14
|
+
* and its default, from the registry, shown wherever the profile is listed
|
|
15
|
+
* — kiso never silently drops a level, and now it shows the ones it takes.
|
|
16
|
+
* A model the registry does not know shows `unknown` (nothing is guessed);
|
|
17
|
+
* it must not read `none`, which is a REAL level since the Responses rows. */
|
|
18
|
+
function effortNote(p) {
|
|
19
|
+
const reasoning = lookupModelMetadata(p.model, p.baseUrl)?.capabilities.reasoning ?? null;
|
|
20
|
+
const effort = reasoning?.effort ?? null;
|
|
21
|
+
if (effort === null)
|
|
22
|
+
return "effort: unknown";
|
|
23
|
+
return `effort: ${effort.levels.map((l) => (l === effort.default ? `[${l}]` : l)).join(" · ")}`;
|
|
24
|
+
}
|
|
25
|
+
/** What signs a profile in, for the listing: the env var's name, `oauth`
|
|
26
|
+
* for a subscription sign-in, or `no key` for an unauthenticated endpoint. */
|
|
27
|
+
function signInNote(p) {
|
|
28
|
+
try {
|
|
29
|
+
const auth = authForProfile("?", p);
|
|
30
|
+
if (auth.type === "oauth")
|
|
31
|
+
return "oauth";
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// unavailable — the availability mark says so; the env name still names what would sign it in
|
|
35
|
+
}
|
|
36
|
+
return p.apiKeyEnv ?? "no key";
|
|
37
|
+
}
|
|
38
|
+
import { oauthTokenThunk } from "./auth/token.js";
|
|
13
39
|
/** The ONE dispatcher — slash commands, exit, and turns. The recovery
|
|
14
40
|
* replay routes through it too — a queued "/last" must never become a
|
|
15
41
|
* user turn (v2c: the rl lives in main, so lines arrive earlier and the
|
|
@@ -295,7 +321,7 @@ export function dispatch(line, ctx) {
|
|
|
295
321
|
header: `model — current: ${agentModel}`,
|
|
296
322
|
options: names.map((name) => {
|
|
297
323
|
const profile = configModels[name];
|
|
298
|
-
const marks = [`profile: ${name}`, ...(profileAvailable(profile) ? [] : ["unavailable"]), ...(profile.model === agentModel ? ["current"] : [])];
|
|
324
|
+
const marks = [`profile: ${name}`, ...(profileAvailable(profile) ? [] : ["unavailable"]), ...(profile.model === agentModel ? ["current"] : []), effortNote(profile)];
|
|
299
325
|
return { label: `${profile.kind}/${profile.model}`, note: marks.join(" · ") };
|
|
300
326
|
}),
|
|
301
327
|
// PH-1a (finding PH-F4): the example must be a syntax
|
|
@@ -329,7 +355,7 @@ export function dispatch(line, ctx) {
|
|
|
329
355
|
else {
|
|
330
356
|
for (const name of names) {
|
|
331
357
|
const p = configModels[name];
|
|
332
|
-
bodyLog(` ${name} → ${p.kind}/${p.model} · ${p
|
|
358
|
+
bodyLog(` ${name} → ${p.kind}/${p.model} · ${signInNote(p)} ${profileAvailable(p) ? "(available)" : "(unavailable)"} · ${effortNote(p)}`);
|
|
333
359
|
}
|
|
334
360
|
}
|
|
335
361
|
bodyLog("switch with /model <profile-name|provider/model>");
|
|
@@ -347,13 +373,19 @@ export function dispatch(line, ctx) {
|
|
|
347
373
|
bodyLog(`no such model profile: ${profName}`);
|
|
348
374
|
}
|
|
349
375
|
else if (!profileAvailable(profile)) {
|
|
350
|
-
bodyLog(
|
|
376
|
+
bodyLog(unavailableReason(arg, profile));
|
|
351
377
|
}
|
|
352
378
|
else {
|
|
379
|
+
const auth = authForProfile(profName, profile);
|
|
353
380
|
const adapter = await buildAdapter(profile.kind, {
|
|
354
381
|
// PH-1c (PH-F19): a keyless profile = an unauthenticated
|
|
355
382
|
// endpoint — the placeholder satisfies the SDK's ctor.
|
|
356
|
-
|
|
383
|
+
// OR-1: an OAuth profile passes a token thunk instead
|
|
384
|
+
// — there is no key, and the adapter re-resolves the
|
|
385
|
+
// token per request.
|
|
386
|
+
...(auth.type === "oauth"
|
|
387
|
+
? { oauth: oauthTokenThunk(auth.providerId), ...(ctx.session.id !== undefined ? { promptCacheKey: ctx.session.id } : {}) }
|
|
388
|
+
: { apiKey: auth.apiKey }),
|
|
357
389
|
...(profile.baseUrl !== undefined ? { baseUrl: profile.baseUrl } : {}),
|
|
358
390
|
...(profile.promptCaching !== undefined ? { promptCaching: profile.promptCaching } : {}),
|
|
359
391
|
});
|
|
@@ -370,7 +402,9 @@ export function dispatch(line, ctx) {
|
|
|
370
402
|
let refused = null;
|
|
371
403
|
if (effortTok !== undefined) {
|
|
372
404
|
const candidate = { thinking: "default", effort: effortTok };
|
|
373
|
-
|
|
405
|
+
// OR-1: the endpoint narrows the row — the same model id answers
|
|
406
|
+
// differently at the first-party API and at the subscription backend
|
|
407
|
+
const resolved = resolveReasoning(profile.model, candidate, profile.baseUrl);
|
|
374
408
|
if (resolved.ok)
|
|
375
409
|
reasoning = candidate;
|
|
376
410
|
else
|
|
@@ -386,12 +420,15 @@ export function dispatch(line, ctx) {
|
|
|
386
420
|
adapter,
|
|
387
421
|
model: profile.model,
|
|
388
422
|
provider: profile.kind,
|
|
423
|
+
// OR-1: the endpoint is the fourth passenger — the cost
|
|
424
|
+
// path and the window lookup key on (model, endpoint).
|
|
425
|
+
...(profile.baseUrl !== undefined ? { baseUrl: profile.baseUrl } : {}),
|
|
389
426
|
...(scope !== undefined ? { scope } : {}),
|
|
390
427
|
...(reasoning !== undefined ? { reasoning } : {}),
|
|
391
428
|
};
|
|
392
429
|
ctx.session.setModelBinding(binding);
|
|
393
430
|
setLastBinding(binding);
|
|
394
|
-
setAgentModel(profile.model);
|
|
431
|
+
setAgentModel(profile.model, profile.baseUrl);
|
|
395
432
|
setCurrentModelName(arg);
|
|
396
433
|
body.notice(`model → ${profName} (${profile.model}${effortTok !== undefined ? ` · ${effortTok}` : ""}) — takes effect on the next turn`);
|
|
397
434
|
}
|
package/dist/index.js
CHANGED
|
@@ -41,6 +41,7 @@ import { isFirstRun, scaffoldFirstRun } from "./first-run.js";
|
|
|
41
41
|
import { fauxSkip, readFauxScript } from "./faux-glue.js";
|
|
42
42
|
import { chat, contextWindowTokens, displayCtxRatio } from "./chat.js";
|
|
43
43
|
import { loadProjectConfig, loadUserConfig, mergeConfigs, resolveAutoCompact, resolveContextWindow, resolveModel } from "./config.js";
|
|
44
|
+
import { oauthTokenThunk } from "./auth/token.js";
|
|
44
45
|
import { checkForUpdate } from "./update-check.js";
|
|
45
46
|
import { resume } from "./resume.js";
|
|
46
47
|
import { resumeTail } from "./resume-tail.js";
|
|
@@ -590,6 +591,10 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
590
591
|
const projectCfg = loadProjectConfig(process.cwd(), project !== null);
|
|
591
592
|
const merged = mergeConfigs(userCfg, projectCfg);
|
|
592
593
|
setMergedConfig(merged);
|
|
594
|
+
// DT-1a: what a delegated task may NAME — the configured checks and the
|
|
595
|
+
// model profiles — handed to the (in-process) subagent extension through
|
|
596
|
+
// the environment. A model never supplies a command; it names a check.
|
|
597
|
+
process.env.KISO_DELEGATION_CONFIG_JSON = JSON.stringify({ checks: merged.checks ?? {}, profiles: Object.keys(merged.models ?? {}) });
|
|
593
598
|
setConfigModels(merged.models ?? {});
|
|
594
599
|
setConfiguredWindow(resolveContextWindow(merged));
|
|
595
600
|
const resolved = resolveModel(modelFlag, merged);
|
|
@@ -603,7 +608,7 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
603
608
|
setCurrentFaux(false);
|
|
604
609
|
setCurrentModelName(resolved.name);
|
|
605
610
|
}
|
|
606
|
-
setAgentModel(model); // v2b: the status bar shows it
|
|
611
|
+
setAgentModel(model, resolved?.profile.baseUrl); // v2b: the status bar shows it; OR-1: the endpoint rides along
|
|
607
612
|
// W21: the extensions array is built ONCE per agent and shared with
|
|
608
613
|
// the runtime by reference — the don't-ask-again writer pushes the
|
|
609
614
|
// generated extension into it so a first-time rule joins the chain
|
|
@@ -643,13 +648,24 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
643
648
|
// field survives for the callers that want a bound (subagents, the
|
|
644
649
|
// SDK, `kiso run`); the interactive front door does not set one.
|
|
645
650
|
// Modes: the five tiers join at the CHAIN HEAD, before the user/
|
|
646
|
-
// project extensions (the deny>ask
|
|
651
|
+
// project extensions (the deny>allow>ask composition keeps a user
|
|
647
652
|
// deny winning over any mode tier — bypass included).
|
|
648
653
|
extensions,
|
|
649
654
|
...(resolved !== null
|
|
650
655
|
? {
|
|
651
656
|
provider: resolved.profile.kind,
|
|
652
|
-
|
|
657
|
+
// OR-1: exactly one sign-in shape reaches the adapter. An
|
|
658
|
+
// OAuth profile has no key to pass — it passes the thunk
|
|
659
|
+
// the adapter re-resolves per request instead.
|
|
660
|
+
...(resolved.oauthProviderId !== undefined
|
|
661
|
+
? { oauth: oauthTokenThunk(resolved.oauthProviderId) }
|
|
662
|
+
: { apiKey: resolved.apiKey ?? "none" }),
|
|
663
|
+
// OR-1: the ChatGPT backend's cache lane is the SESSION —
|
|
664
|
+
// one conversation's requests share a key, different
|
|
665
|
+
// conversations never do. The one entry point that hands
|
|
666
|
+
// no session id is `kiso sessions`, a read-only listing
|
|
667
|
+
// that streams nothing, so its absence costs no cache.
|
|
668
|
+
...(sessionId !== undefined ? { promptCacheKey: sessionId } : {}),
|
|
653
669
|
...(resolved.profile.baseUrl !== undefined ? { baseUrl: resolved.profile.baseUrl } : {}),
|
|
654
670
|
...(resolved.profile.promptCaching !== undefined ? { promptCaching: resolved.profile.promptCaching } : {}),
|
|
655
671
|
}
|
|
@@ -785,7 +801,7 @@ async function chatLoop(agent, firstId, input, autoCompact) {
|
|
|
785
801
|
// XP-1 §2.1: the switched-to session's OWN truth repaints the row —
|
|
786
802
|
// the global display state never outlives the session it described
|
|
787
803
|
// (pre-XP the row kept the previous session's /model selection).
|
|
788
|
-
setAgentModel(session.model);
|
|
804
|
+
setAgentModel(session.model, session.baseUrl);
|
|
789
805
|
setCurrentModelName(session.model);
|
|
790
806
|
paintBootStatus(session);
|
|
791
807
|
const nav = {
|
|
@@ -799,6 +815,51 @@ async function chatLoop(agent, firstId, input, autoCompact) {
|
|
|
799
815
|
id = end.id;
|
|
800
816
|
}
|
|
801
817
|
}
|
|
818
|
+
/** A secret from the terminal: hidden input on a TTY, the whole of stdin
|
|
819
|
+
* otherwise (a pipe, a heredoc) — never an argument, which shell history
|
|
820
|
+
* would keep. */
|
|
821
|
+
async function readSecret(prompt) {
|
|
822
|
+
if (process.stdin.isTTY !== true) {
|
|
823
|
+
return await new Promise((resolve) => {
|
|
824
|
+
let buf = "";
|
|
825
|
+
process.stdin.setEncoding("utf8");
|
|
826
|
+
process.stdin.on("data", (d) => {
|
|
827
|
+
buf += d;
|
|
828
|
+
});
|
|
829
|
+
process.stdin.on("end", () => resolve(buf.split("\n")[0] ?? ""));
|
|
830
|
+
process.stdin.resume();
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
process.stdout.write(prompt);
|
|
834
|
+
return await new Promise((resolve) => {
|
|
835
|
+
let buf = "";
|
|
836
|
+
process.stdin.setRawMode(true);
|
|
837
|
+
process.stdin.resume();
|
|
838
|
+
process.stdin.setEncoding("utf8");
|
|
839
|
+
const onData = (chunk) => {
|
|
840
|
+
for (const ch of chunk) {
|
|
841
|
+
if (ch === "\r" || ch === "\n") {
|
|
842
|
+
process.stdin.setRawMode(false);
|
|
843
|
+
process.stdin.pause();
|
|
844
|
+
process.stdin.off("data", onData);
|
|
845
|
+
process.stdout.write("\n");
|
|
846
|
+
resolve(buf);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (ch === "\u0003") {
|
|
850
|
+
process.stdin.setRawMode(false);
|
|
851
|
+
process.stdout.write("\n");
|
|
852
|
+
process.exit(130);
|
|
853
|
+
}
|
|
854
|
+
if (ch === "\u007f" || ch === "\b")
|
|
855
|
+
buf = buf.slice(0, -1);
|
|
856
|
+
else
|
|
857
|
+
buf += ch;
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
process.stdin.on("data", onData);
|
|
861
|
+
});
|
|
862
|
+
}
|
|
802
863
|
async function main() {
|
|
803
864
|
// E group (the graceful-exit gate ③, R-G 0.1.48): a terminal closing
|
|
804
865
|
// turns the in-flight stdout/stderr writes into EIO, and node's
|
|
@@ -1091,6 +1152,67 @@ async function main() {
|
|
|
1091
1152
|
}
|
|
1092
1153
|
break;
|
|
1093
1154
|
}
|
|
1155
|
+
case "login":
|
|
1156
|
+
case "logout":
|
|
1157
|
+
case "auth": {
|
|
1158
|
+
// the sign-in plan (2026-09-08), step 1: the credential store.
|
|
1159
|
+
// `login <provider>` reads the key from a hidden prompt on a TTY
|
|
1160
|
+
// or from stdin otherwise (never from an argument — shell history);
|
|
1161
|
+
// `logout <provider>` deletes; `auth` lists, keys masked.
|
|
1162
|
+
const { KNOWN_PROVIDERS, OAUTH_PROVIDERS, authPath, deleteCredential, maskSecret, readAuthFile, setCredential } = await import("./auth/credentials.js");
|
|
1163
|
+
const provider = arg;
|
|
1164
|
+
if (command === "auth") {
|
|
1165
|
+
const file = readAuthFile();
|
|
1166
|
+
const rows = Object.entries(file.credentials);
|
|
1167
|
+
const lines = [`credentials: ${authPath()}${rows.length === 0 ? " (none stored)" : ""}`];
|
|
1168
|
+
for (const [id, cred] of rows) {
|
|
1169
|
+
if (cred.type === "api-key")
|
|
1170
|
+
lines.push(` ${id.padEnd(10)} api-key ${maskSecret(cred.key)} saved ${new Date(cred.savedAt).toISOString().slice(0, 10)}`);
|
|
1171
|
+
else
|
|
1172
|
+
lines.push(` ${id.padEnd(10)} oauth ${cred.accountId ?? ""} expires ${new Date(cred.expires).toISOString()}${cred.expires < Date.now() ? " (expired)" : ""}`);
|
|
1173
|
+
}
|
|
1174
|
+
const envs = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"].filter((k) => process.env[k] !== undefined);
|
|
1175
|
+
lines.push(`env vars set: ${envs.length ? envs.join(", ") : "none"} (a stored credential owns its provider; env applies only when nothing is stored)`);
|
|
1176
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
if (provider === undefined || !KNOWN_PROVIDERS.includes(provider)) {
|
|
1180
|
+
throw new CliUsageError(`kiso ${command} <provider> — one of: ${KNOWN_PROVIDERS.join(", ")}`);
|
|
1181
|
+
}
|
|
1182
|
+
if (command === "logout") {
|
|
1183
|
+
const had = deleteCredential(provider);
|
|
1184
|
+
process.stdout.write(had ? `signed out of ${provider}\n` : `nothing stored for ${provider}\n`);
|
|
1185
|
+
break;
|
|
1186
|
+
}
|
|
1187
|
+
if (OAUTH_PROVIDERS.includes(provider)) {
|
|
1188
|
+
// the subscription sign-in (plan step 2): the browser flow with a
|
|
1189
|
+
// paste path; the callback host/port can be moved by env for tests
|
|
1190
|
+
const { chatgptFlow } = await import("./auth/oauth/chatgpt.js");
|
|
1191
|
+
const { createInterface } = await import("node:readline");
|
|
1192
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1193
|
+
const cbPort = process.env.KISO_OAUTH_CALLBACK_PORT !== undefined ? Number(process.env.KISO_OAUTH_CALLBACK_PORT) : undefined;
|
|
1194
|
+
try {
|
|
1195
|
+
const cred = await chatgptFlow.login({
|
|
1196
|
+
notify: (line) => process.stdout.write(`${line}\n`),
|
|
1197
|
+
prompt: (q) => new Promise((resolve) => rl.question(q, resolve)),
|
|
1198
|
+
...(cbPort !== undefined ? { callback: { host: process.env.KISO_OAUTH_CALLBACK_HOST ?? "127.0.0.1", port: cbPort } } : {}),
|
|
1199
|
+
...(process.env.KISO_OAUTH_TOKEN_URL !== undefined ? { tokenUrl: process.env.KISO_OAUTH_TOKEN_URL } : {}),
|
|
1200
|
+
});
|
|
1201
|
+
setCredential(provider, cred);
|
|
1202
|
+
process.stdout.write(`signed in to ${provider} (${chatgptFlow.label}) — account ${cred.accountId ?? "?"}, expires ${new Date(cred.expires).toISOString()} — stored in ${authPath()}\n`);
|
|
1203
|
+
}
|
|
1204
|
+
finally {
|
|
1205
|
+
rl.close();
|
|
1206
|
+
}
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1209
|
+
const key = (await readSecret(`API key for ${provider}: `)).trim();
|
|
1210
|
+
if (key === "")
|
|
1211
|
+
throw new CliUsageError(`kiso login ${provider}: no key given`);
|
|
1212
|
+
setCredential(provider, { type: "api-key", key, savedAt: Date.now() });
|
|
1213
|
+
process.stdout.write(`signed in to ${provider} with an API key (${maskSecret(key)}) — stored in ${authPath()}\n`);
|
|
1214
|
+
break;
|
|
1215
|
+
}
|
|
1094
1216
|
case "help": {
|
|
1095
1217
|
// PH-1b (finding PH-F21): the CLI must be able to describe its
|
|
1096
1218
|
// own configuration — the old help listed five commands and
|
|
@@ -1104,6 +1226,10 @@ async function main() {
|
|
|
1104
1226
|
" kiso resume pick a session to continue (TTY picker)\n" +
|
|
1105
1227
|
" kiso resume <id> [prompt] continue a session (one-shot)\n" +
|
|
1106
1228
|
" kiso sessions list durable sessions\n" +
|
|
1229
|
+
" kiso login <provider> anthropic|openai|deepseek|zai: store an API key (hidden prompt, or stdin when piped);\n" +
|
|
1230
|
+
" chatgpt: sign in with a ChatGPT subscription (browser; unofficial third-party flow)\n" +
|
|
1231
|
+
" kiso logout <provider> remove the stored credential\n" +
|
|
1232
|
+
" kiso auth list stored credentials (keys masked)\n" +
|
|
1107
1233
|
" kiso help this help\n\n" +
|
|
1108
1234
|
"flags (any position):\n" +
|
|
1109
1235
|
" --model <profile|provider/model> pick the model (also /model in-session)\n" +
|
package/dist/mode.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* switches take effect immediately. The extension NAME rides the runtime's decidedBy
|
|
7
7
|
* field: an automated denial records decidedBy: "mode:<name>" — the
|
|
8
8
|
* audit sell. User-level extensions stay on the chain AFTER the mode
|
|
9
|
-
* tiers; a user deny always wins (the chain's deny>ask
|
|
9
|
+
* tiers; a user deny always wins (the chain's deny>allow>ask
|
|
10
10
|
* monotonicity — bypass cannot override an extension deny).
|
|
11
11
|
*/
|
|
12
12
|
import type { KisoExtension } from "@vincemakes/kiso-runtime";
|
|
@@ -29,7 +29,7 @@ export declare function modeFromEnv(): Mode | undefined;
|
|
|
29
29
|
* extension name). The CURRENT tier is first: an all-allow chain records
|
|
30
30
|
* decidedBy = the FIRST SPEAKER, so an auto-allow under the startup mode
|
|
31
31
|
* names that mode honestly. Order never affects verdicts — the chain is
|
|
32
|
-
* deny>ask
|
|
32
|
+
* deny>allow>ask over the SPEAKING verdicts (abstain = no opinion), so a
|
|
33
33
|
* user extension's deny wins over any mode tier, bypass included (the
|
|
34
34
|
* monotonicity e2e pins it). */
|
|
35
35
|
export declare function modeExtensions(): readonly KisoExtension[];
|
package/dist/mode.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* switches take effect immediately. The extension NAME rides the runtime's decidedBy
|
|
7
7
|
* field: an automated denial records decidedBy: "mode:<name>" — the
|
|
8
8
|
* audit sell. User-level extensions stay on the chain AFTER the mode
|
|
9
|
-
* tiers; a user deny always wins (the chain's deny>ask
|
|
9
|
+
* tiers; a user deny always wins (the chain's deny>allow>ask
|
|
10
10
|
* monotonicity — bypass cannot override an extension deny).
|
|
11
11
|
*/
|
|
12
12
|
export const MODES = ["manual", "default", "accept-edits", "plan", "bypass"];
|
|
@@ -78,7 +78,7 @@ function tierVerdict(tier, call) {
|
|
|
78
78
|
* extension name). The CURRENT tier is first: an all-allow chain records
|
|
79
79
|
* decidedBy = the FIRST SPEAKER, so an auto-allow under the startup mode
|
|
80
80
|
* names that mode honestly. Order never affects verdicts — the chain is
|
|
81
|
-
* deny>ask
|
|
81
|
+
* deny>allow>ask over the SPEAKING verdicts (abstain = no opinion), so a
|
|
82
82
|
* user extension's deny wins over any mode tier, bypass included (the
|
|
83
83
|
* monotonicity e2e pins it). */
|
|
84
84
|
export function modeExtensions() {
|
package/dist/state.d.ts
CHANGED
|
@@ -156,7 +156,10 @@ export declare function setSessionStore(value: {
|
|
|
156
156
|
}): void;
|
|
157
157
|
/** The model name for the status bar — set by makeAgent. */
|
|
158
158
|
export declare let agentModel: string;
|
|
159
|
-
|
|
159
|
+
/** OR-1: the live model's ENDPOINT, set in the same call as the model so
|
|
160
|
+
* the two can never drift: the window lookup keys on (model, endpoint). */
|
|
161
|
+
export declare let agentBaseUrl: string | undefined;
|
|
162
|
+
export declare function setAgentModel(value: string, baseUrl?: string): void;
|
|
160
163
|
/** merge round B: whether the agent runs on the faux provider (no real key) —
|
|
161
164
|
* set inside makeAgent, read by main for chat/resume's exhaustion check. */
|
|
162
165
|
export declare let currentFaux: boolean;
|
|
@@ -213,7 +216,9 @@ export declare const CANCELLED: unique symbol;
|
|
|
213
216
|
export interface LastBinding {
|
|
214
217
|
readonly adapter: import("@vincemakes/kiso-core").Adapter;
|
|
215
218
|
readonly model: string;
|
|
216
|
-
readonly provider?: "anthropic" | "openai-compat";
|
|
219
|
+
readonly provider?: "anthropic" | "openai-compat" | "openai-responses";
|
|
220
|
+
/** OR-1: the endpoint inherits with the adapter (the cost path keys on it). */
|
|
221
|
+
readonly baseUrl?: string;
|
|
217
222
|
readonly scope?: import("@vincemakes/kiso-core").ContinuationScope;
|
|
218
223
|
readonly reasoning?: import("@vincemakes/kiso-runtime/internal").ReasoningSetting;
|
|
219
224
|
}
|
package/dist/state.js
CHANGED
|
@@ -213,8 +213,12 @@ export function setSessionStore(value) {
|
|
|
213
213
|
}
|
|
214
214
|
/** The model name for the status bar — set by makeAgent. */
|
|
215
215
|
export let agentModel = "faux";
|
|
216
|
-
|
|
216
|
+
/** OR-1: the live model's ENDPOINT, set in the same call as the model so
|
|
217
|
+
* the two can never drift: the window lookup keys on (model, endpoint). */
|
|
218
|
+
export let agentBaseUrl;
|
|
219
|
+
export function setAgentModel(value, baseUrl) {
|
|
217
220
|
agentModel = value;
|
|
221
|
+
agentBaseUrl = baseUrl;
|
|
218
222
|
}
|
|
219
223
|
/** merge round B: whether the agent runs on the faux provider (no real key) —
|
|
220
224
|
* set inside makeAgent, read by main for chat/resume's exhaustion check. */
|
package/dist/trust-ui.d.ts
CHANGED
|
@@ -37,6 +37,19 @@ export declare let pendingAsk: (() => void) | null;
|
|
|
37
37
|
export declare function askPanel(input: LineInput, view: PanelView, opts?: {
|
|
38
38
|
safer?: () => Promise<SaferAnswer>;
|
|
39
39
|
}): Promise<PanelVerdict>;
|
|
40
|
+
/**
|
|
41
|
+
* The dock-less fallback line → a verdict. `y…` allows. `n <reason>` / `no
|
|
42
|
+
* <reason>` DENIES WITH THE REASON — the words become the tool result and
|
|
43
|
+
* the run continues with the model seeing them, the same verdict the panel's
|
|
44
|
+
* "No + words" produces (chat.ts, the deny case). A bare `n`/`no`, or any
|
|
45
|
+
* other line, is the bare denial that aborts the run — unchanged. (LH-1 ruling
|
|
46
|
+
* 2026-09-08: the default refusal across arms is refuse-with-a-reason-and-
|
|
47
|
+
* continue, and the surface a piped or 0-row leg drives is this one.)
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseFallbackAnswer(answer: string): {
|
|
50
|
+
action: "allow" | "deny";
|
|
51
|
+
reason: string;
|
|
52
|
+
};
|
|
40
53
|
/**
|
|
41
54
|
* KC3.5 — the AskUI bridge: the panel the ask extension asks through.
|
|
42
55
|
*
|