@vincemakes/kiso-code 0.30.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 +42 -6
- package/dist/config.js +70 -12
- package/dist/dispatch.js +45 -8
- package/dist/index.js +126 -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;
|
|
@@ -82,13 +87,44 @@ export declare function loadUserConfig(): KisoConfig | null;
|
|
|
82
87
|
export declare function loadProjectConfig(cwd: string, trusted: boolean): KisoConfig | null;
|
|
83
88
|
/** Merge: project wins over user (each layer's own keys only). */
|
|
84
89
|
export declare function mergeConfigs(user: KisoConfig | null, project: KisoConfig | null): KisoConfig;
|
|
85
|
-
/**
|
|
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. */
|
|
86
117
|
export declare function profileAvailable(p: ModelProfile): boolean;
|
|
87
|
-
/** 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). */
|
|
88
122
|
export interface ResolvedModel {
|
|
89
123
|
readonly name: string;
|
|
90
124
|
readonly profile: ModelProfile;
|
|
91
|
-
readonly apiKey
|
|
125
|
+
readonly apiKey?: string;
|
|
126
|
+
/** OR-1: the provider whose stored OAuth sign-in this profile uses. */
|
|
127
|
+
readonly oauthProviderId?: string;
|
|
92
128
|
}
|
|
93
129
|
/** "provider/model" direct write → a profile. */
|
|
94
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
|
}
|
|
@@ -177,11 +179,65 @@ export function mergeConfigs(user, project) {
|
|
|
177
179
|
...(u.checks !== undefined || p.checks !== undefined ? { checks: { ...(u.checks ?? {}), ...(p.checks ?? {}) } } : {}),
|
|
178
180
|
};
|
|
179
181
|
}
|
|
180
|
-
/**
|
|
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. */
|
|
181
233
|
export function profileAvailable(p) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
234
|
+
try {
|
|
235
|
+
authForProfile("?", p);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
185
241
|
}
|
|
186
242
|
/** "provider/model" direct write → a profile. */
|
|
187
243
|
export function directWriteProfile(value) {
|
|
@@ -189,11 +245,15 @@ export function directWriteProfile(value) {
|
|
|
189
245
|
if (slash <= 0 || slash === value.length - 1)
|
|
190
246
|
return null;
|
|
191
247
|
const kind = value.slice(0, slash);
|
|
192
|
-
if (kind !== "openai-compat" && kind !== "anthropic")
|
|
248
|
+
if (kind !== "openai-compat" && kind !== "anthropic" && kind !== "openai-responses")
|
|
193
249
|
return null;
|
|
194
250
|
return {
|
|
195
251
|
kind,
|
|
196
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.
|
|
197
257
|
apiKeyEnv: kind === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY",
|
|
198
258
|
};
|
|
199
259
|
}
|
|
@@ -256,12 +316,10 @@ export function resolveModel(modelFlag, merged) {
|
|
|
256
316
|
return null;
|
|
257
317
|
}
|
|
258
318
|
function resolveProfile(name, p) {
|
|
259
|
-
if (!profileAvailable(p)) {
|
|
260
|
-
throw new ConfigError(`model ${name}: unavailable — the env var ${p.apiKeyEnv} is not set (configs never store keys, only the env-var name)`);
|
|
261
|
-
}
|
|
262
319
|
// PH-1c (finding PH-F19): a keyless profile hands the adapter a
|
|
263
320
|
// placeholder — the SDKs require SOME string; the endpoint ignores it.
|
|
264
|
-
|
|
321
|
+
const auth = authForProfile(name, p);
|
|
322
|
+
return auth.type === "oauth" ? { name, profile: p, oauthProviderId: auth.providerId } : { name, profile: p, apiKey: auth.apiKey };
|
|
265
323
|
}
|
|
266
324
|
/** Mode: env (KISO_MODE) beats config.mode; the --mode flag is applied by
|
|
267
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";
|
|
@@ -607,7 +608,7 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
607
608
|
setCurrentFaux(false);
|
|
608
609
|
setCurrentModelName(resolved.name);
|
|
609
610
|
}
|
|
610
|
-
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
|
|
611
612
|
// W21: the extensions array is built ONCE per agent and shared with
|
|
612
613
|
// the runtime by reference — the don't-ask-again writer pushes the
|
|
613
614
|
// generated extension into it so a first-time rule joins the chain
|
|
@@ -647,13 +648,24 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
647
648
|
// field survives for the callers that want a bound (subagents, the
|
|
648
649
|
// SDK, `kiso run`); the interactive front door does not set one.
|
|
649
650
|
// Modes: the five tiers join at the CHAIN HEAD, before the user/
|
|
650
|
-
// project extensions (the deny>ask
|
|
651
|
+
// project extensions (the deny>allow>ask composition keeps a user
|
|
651
652
|
// deny winning over any mode tier — bypass included).
|
|
652
653
|
extensions,
|
|
653
654
|
...(resolved !== null
|
|
654
655
|
? {
|
|
655
656
|
provider: resolved.profile.kind,
|
|
656
|
-
|
|
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 } : {}),
|
|
657
669
|
...(resolved.profile.baseUrl !== undefined ? { baseUrl: resolved.profile.baseUrl } : {}),
|
|
658
670
|
...(resolved.profile.promptCaching !== undefined ? { promptCaching: resolved.profile.promptCaching } : {}),
|
|
659
671
|
}
|
|
@@ -789,7 +801,7 @@ async function chatLoop(agent, firstId, input, autoCompact) {
|
|
|
789
801
|
// XP-1 §2.1: the switched-to session's OWN truth repaints the row —
|
|
790
802
|
// the global display state never outlives the session it described
|
|
791
803
|
// (pre-XP the row kept the previous session's /model selection).
|
|
792
|
-
setAgentModel(session.model);
|
|
804
|
+
setAgentModel(session.model, session.baseUrl);
|
|
793
805
|
setCurrentModelName(session.model);
|
|
794
806
|
paintBootStatus(session);
|
|
795
807
|
const nav = {
|
|
@@ -803,6 +815,51 @@ async function chatLoop(agent, firstId, input, autoCompact) {
|
|
|
803
815
|
id = end.id;
|
|
804
816
|
}
|
|
805
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
|
+
}
|
|
806
863
|
async function main() {
|
|
807
864
|
// E group (the graceful-exit gate ③, R-G 0.1.48): a terminal closing
|
|
808
865
|
// turns the in-flight stdout/stderr writes into EIO, and node's
|
|
@@ -1095,6 +1152,67 @@ async function main() {
|
|
|
1095
1152
|
}
|
|
1096
1153
|
break;
|
|
1097
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
|
+
}
|
|
1098
1216
|
case "help": {
|
|
1099
1217
|
// PH-1b (finding PH-F21): the CLI must be able to describe its
|
|
1100
1218
|
// own configuration — the old help listed five commands and
|
|
@@ -1108,6 +1226,10 @@ async function main() {
|
|
|
1108
1226
|
" kiso resume pick a session to continue (TTY picker)\n" +
|
|
1109
1227
|
" kiso resume <id> [prompt] continue a session (one-shot)\n" +
|
|
1110
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" +
|
|
1111
1233
|
" kiso help this help\n\n" +
|
|
1112
1234
|
"flags (any position):\n" +
|
|
1113
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
|
*
|
package/dist/trust-ui.js
CHANGED
|
@@ -104,12 +104,27 @@ opts) {
|
|
|
104
104
|
}
|
|
105
105
|
settled = true;
|
|
106
106
|
pendingAsk = null;
|
|
107
|
-
|
|
108
|
-
resolve(yes ? { action: "allow", reason: "" } : { action: "deny", reason: "" });
|
|
107
|
+
resolve(parseFallbackAnswer(answer));
|
|
109
108
|
});
|
|
110
109
|
}
|
|
111
110
|
});
|
|
112
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* The dock-less fallback line → a verdict. `y…` allows. `n <reason>` / `no
|
|
114
|
+
* <reason>` DENIES WITH THE REASON — the words become the tool result and
|
|
115
|
+
* the run continues with the model seeing them, the same verdict the panel's
|
|
116
|
+
* "No + words" produces (chat.ts, the deny case). A bare `n`/`no`, or any
|
|
117
|
+
* other line, is the bare denial that aborts the run — unchanged. (LH-1 ruling
|
|
118
|
+
* 2026-09-08: the default refusal across arms is refuse-with-a-reason-and-
|
|
119
|
+
* continue, and the surface a piped or 0-row leg drives is this one.)
|
|
120
|
+
*/
|
|
121
|
+
export function parseFallbackAnswer(answer) {
|
|
122
|
+
const line = answer.trim();
|
|
123
|
+
if (line.toLowerCase().startsWith("y"))
|
|
124
|
+
return { action: "allow", reason: "" };
|
|
125
|
+
const m = /^(?:n|no)\b[\s:,-]*(.*)$/is.exec(line);
|
|
126
|
+
return { action: "deny", reason: (m?.[1] ?? "").trim() };
|
|
127
|
+
}
|
|
113
128
|
/**
|
|
114
129
|
* KC3.5 — the AskUI bridge: the panel the ask extension asks through.
|
|
115
130
|
*
|