@vincemakes/kiso-code 0.40.5 → 0.40.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/dist/auth/credentials.d.ts +16 -0
- package/dist/auth/credentials.js +25 -0
- package/dist/auth/oauth/chatgpt.d.ts +1 -1
- package/dist/auth/oauth/chatgpt.js +6 -1
- package/dist/auth/oauth/index.d.ts +8 -0
- package/dist/auth/oauth/index.js +8 -1
- package/dist/auth/refresh.d.ts +1 -1
- package/dist/auth/refresh.js +20 -1
- package/dist/chat.d.ts +3 -4
- package/dist/chat.js +4 -7
- package/dist/config.d.ts +9 -1
- package/dist/config.js +43 -4
- package/dist/dispatch.js +82 -27
- package/dist/index.js +55 -9
- package/dist/preferences.d.ts +14 -0
- package/dist/preferences.js +64 -0
- package/dist/readonly-shell.js +5 -9
- package/dist/settings.d.ts +37 -0
- package/dist/settings.js +79 -0
- package/dist/state.d.ts +10 -0
- package/dist/state.js +4 -0
- package/dist/temp-sweep.d.ts +12 -0
- package/dist/temp-sweep.js +76 -0
- package/dist/trust-ui.js +4 -3
- package/package.json +15 -15
|
@@ -10,6 +10,10 @@ export type Credential = {
|
|
|
10
10
|
readonly expires: number;
|
|
11
11
|
readonly accountId?: string;
|
|
12
12
|
readonly savedAt: number;
|
|
13
|
+
/** 0.40.7: when the token endpoint REFUSED this credential's
|
|
14
|
+
* refresh token. Set only by that refusal (never by a network
|
|
15
|
+
* failure); a new login replaces the whole entry and so clears it. */
|
|
16
|
+
readonly refreshRejectedAt?: number;
|
|
13
17
|
};
|
|
14
18
|
export interface AuthFile {
|
|
15
19
|
readonly version: 1;
|
|
@@ -49,6 +53,18 @@ export declare function effectiveBaseUrl(kind: string, baseUrl?: string): string
|
|
|
49
53
|
/** The provider identity a profile's credential is stored under, or null
|
|
50
54
|
* for an origin nobody recognizes (custom: env var only). */
|
|
51
55
|
export declare function providerIdOf(kind: string, baseUrl?: string): string | null;
|
|
56
|
+
/**
|
|
57
|
+
* 0.40.6 — the store id for a GATEWAY's own key: `endpoint:<origin>`.
|
|
58
|
+
*
|
|
59
|
+
* A vendor's credential is stored under the vendor and only ever reaches
|
|
60
|
+
* the vendor's origin (R1 above). A gateway has no vendor identity, so its
|
|
61
|
+
* key is stored under the one thing that names where it may go — its
|
|
62
|
+
* ORIGIN (scheme, host, port). `kiso login --endpoint <url>` writes it; a
|
|
63
|
+
* profile whose endpoint has exactly that origin reads it; no other origin
|
|
64
|
+
* ever does, a local forwarder in front of the gateway included. Null for
|
|
65
|
+
* anything that is not an http(s) URL — never a guess.
|
|
66
|
+
*/
|
|
67
|
+
export declare function endpointCredentialId(url: string | undefined): string | null;
|
|
52
68
|
export declare function authPath(): string;
|
|
53
69
|
export declare function readAuthFile(path?: string): AuthFile;
|
|
54
70
|
export declare function getCredential(providerId: string, path?: string): Credential | undefined;
|
package/dist/auth/credentials.js
CHANGED
|
@@ -133,6 +133,31 @@ function originOf(baseUrl) {
|
|
|
133
133
|
return null;
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* 0.40.6 — the store id for a GATEWAY's own key: `endpoint:<origin>`.
|
|
138
|
+
*
|
|
139
|
+
* A vendor's credential is stored under the vendor and only ever reaches
|
|
140
|
+
* the vendor's origin (R1 above). A gateway has no vendor identity, so its
|
|
141
|
+
* key is stored under the one thing that names where it may go — its
|
|
142
|
+
* ORIGIN (scheme, host, port). `kiso login --endpoint <url>` writes it; a
|
|
143
|
+
* profile whose endpoint has exactly that origin reads it; no other origin
|
|
144
|
+
* ever does, a local forwarder in front of the gateway included. Null for
|
|
145
|
+
* anything that is not an http(s) URL — never a guess.
|
|
146
|
+
*/
|
|
147
|
+
export function endpointCredentialId(url) {
|
|
148
|
+
if (url === undefined)
|
|
149
|
+
return null;
|
|
150
|
+
let u;
|
|
151
|
+
try {
|
|
152
|
+
u = new URL(url);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
if (u.protocol !== "https:" && u.protocol !== "http:")
|
|
158
|
+
return null;
|
|
159
|
+
return `endpoint:${u.origin}`;
|
|
160
|
+
}
|
|
136
161
|
export function authPath() {
|
|
137
162
|
return join(kisoHome(), "auth.json");
|
|
138
163
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* endpoint — the Responses adapter is the second half of this step.
|
|
18
18
|
*/
|
|
19
19
|
import { createServer } from "node:http";
|
|
20
|
+
import { RefreshRejectedError } from "./index.js";
|
|
20
21
|
import { generatePkce, randomState } from "./pkce.js";
|
|
21
22
|
export const CHATGPT = {
|
|
22
23
|
providerId: "chatgpt",
|
|
@@ -86,7 +87,11 @@ export function accountIdOf(accessToken) {
|
|
|
86
87
|
async function readTokenResponse(response, operation) {
|
|
87
88
|
if (!response.ok) {
|
|
88
89
|
const text = await response.text().catch(() => "");
|
|
89
|
-
|
|
90
|
+
const message = `ChatGPT sign-in: token ${operation} failed (${response.status})${text ? `: ${text.slice(0, 200)}` : ""}`;
|
|
91
|
+
// the endpoint's own refusal of the refresh token — the sign-in is over
|
|
92
|
+
if (operation === "refresh" && (response.status === 400 || response.status === 401))
|
|
93
|
+
throw new RefreshRejectedError(message);
|
|
94
|
+
throw new Error(message);
|
|
90
95
|
}
|
|
91
96
|
const json = (await response.json());
|
|
92
97
|
if (typeof json.access_token !== "string" || typeof json.refresh_token !== "string" || typeof json.expires_in !== "number") {
|
|
@@ -8,6 +8,14 @@ import type { Credential } from "../credentials.js";
|
|
|
8
8
|
export type OAuthCredential = Extract<Credential, {
|
|
9
9
|
type: "oauth";
|
|
10
10
|
}>;
|
|
11
|
+
/**
|
|
12
|
+
* 0.40.7 — a refresh the token endpoint REFUSED (400 / 401: the refresh
|
|
13
|
+
* token was revoked or has expired), as distinct from one that never got an
|
|
14
|
+
* answer. Only this marks a sign-in dead (`refreshRejectedAt`); a network
|
|
15
|
+
* failure leaves the credential as it was, to be tried again.
|
|
16
|
+
*/
|
|
17
|
+
export declare class RefreshRejectedError extends Error {
|
|
18
|
+
}
|
|
11
19
|
export interface LoginInteraction {
|
|
12
20
|
/** Print a line for the person signing in (the URL to open, the paste prompt). */
|
|
13
21
|
readonly notify: (line: string) => void;
|
package/dist/auth/oauth/index.js
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 0.40.7 — a refresh the token endpoint REFUSED (400 / 401: the refresh
|
|
3
|
+
* token was revoked or has expired), as distinct from one that never got an
|
|
4
|
+
* answer. Only this marks a sign-in dead (`refreshRejectedAt`); a network
|
|
5
|
+
* failure leaves the credential as it was, to be tried again.
|
|
6
|
+
*/
|
|
7
|
+
export class RefreshRejectedError extends Error {
|
|
8
|
+
}
|
package/dist/auth/refresh.d.ts
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
* never a silent fall back to an API key or an env var.
|
|
8
8
|
*/
|
|
9
9
|
import { type Credential } from "./credentials.js";
|
|
10
|
-
import type
|
|
10
|
+
import { type OAuthFlow } from "./oauth/index.js";
|
|
11
11
|
export declare const FRESH_WINDOW_MS: number;
|
|
12
12
|
export declare function ensureFresh(providerId: string, flow: OAuthFlow, path?: string, now?: number, tokenUrl?: string): Promise<Credential>;
|
package/dist/auth/refresh.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* never a silent fall back to an API key or an env var.
|
|
8
8
|
*/
|
|
9
9
|
import { AuthError, modifyAuthFile, readAuthFile } from "./credentials.js";
|
|
10
|
+
import { RefreshRejectedError } from "./oauth/index.js";
|
|
10
11
|
export const FRESH_WINDOW_MS = 5 * 60_000;
|
|
11
12
|
export async function ensureFresh(providerId, flow, path, now = Date.now(), tokenUrl) {
|
|
12
13
|
const current = readAuthFile(path).credentials[providerId];
|
|
@@ -14,6 +15,10 @@ export async function ensureFresh(providerId, flow, path, now = Date.now(), toke
|
|
|
14
15
|
throw new AuthError(`not signed in to ${providerId}: run \`kiso login ${providerId}\``);
|
|
15
16
|
if (current.type !== "oauth" || current.expires - now > FRESH_WINDOW_MS)
|
|
16
17
|
return current;
|
|
18
|
+
// 0.40.7: a refresh token the endpoint already refused is not sent again —
|
|
19
|
+
// the answer would be the same refusal, one round trip later
|
|
20
|
+
if (current.refreshRejectedAt !== undefined)
|
|
21
|
+
throw new AuthError(`signed out of ${providerId} (the sign-in was refused when kiso tried to renew it) — run \`kiso login ${providerId}\``);
|
|
17
22
|
let refreshed;
|
|
18
23
|
let error;
|
|
19
24
|
// the double check: another process may have refreshed while we waited for the lock
|
|
@@ -26,8 +31,22 @@ export async function ensureFresh(providerId, flow, path, now = Date.now(), toke
|
|
|
26
31
|
catch (err) {
|
|
27
32
|
error = err instanceof Error ? err : new Error(String(err));
|
|
28
33
|
}
|
|
29
|
-
if (refreshed === undefined)
|
|
34
|
+
if (refreshed === undefined) {
|
|
35
|
+
// 0.40.7: the endpoint's REFUSAL is remembered on the credential, so
|
|
36
|
+
// /model can say the sign-in is over before a turn finds out. The
|
|
37
|
+
// same generation rule as the write-back below (R2a): a logout or a
|
|
38
|
+
// new login while the refresh was on the wire is left alone.
|
|
39
|
+
if (error instanceof RefreshRejectedError) {
|
|
40
|
+
const began = current;
|
|
41
|
+
modifyAuthFile((file) => {
|
|
42
|
+
const latest = file.credentials[providerId];
|
|
43
|
+
if (latest === undefined || latest.type !== "oauth" || latest.refresh !== began.refresh || latest.expires !== began.expires)
|
|
44
|
+
return file;
|
|
45
|
+
return { version: 1, credentials: { ...file.credentials, [providerId]: { ...latest, refreshRejectedAt: now } } };
|
|
46
|
+
}, path);
|
|
47
|
+
}
|
|
30
48
|
throw new AuthError(`signed out of ${providerId} (the token could not be refreshed: ${error?.message ?? "unknown"}) — run \`kiso login ${providerId}\``);
|
|
49
|
+
}
|
|
31
50
|
const written = refreshed;
|
|
32
51
|
// R2a — THE WRITE-BACK MAY NOT RESURRECT WHAT THE HUMAN REMOVED.
|
|
33
52
|
//
|
package/dist/chat.d.ts
CHANGED
|
@@ -87,10 +87,9 @@ export declare function statedContextWindow(of?: {
|
|
|
87
87
|
/** CW-1: a window as the displays write it — `1M`, `1.05M`, `272K`,
|
|
88
88
|
* `1,048,576` (a figure that is not round is shown whole). */
|
|
89
89
|
export declare function windowLabel(tokens: number): string;
|
|
90
|
-
/** CW-1: the source, said
|
|
91
|
-
* form
|
|
92
|
-
|
|
93
|
-
export declare function windowSourceNote(w: StatedWindow | null, form: "short" | "long"): string;
|
|
90
|
+
/** CW-1: the window and its source, said — /status's line (0.40.6: the
|
|
91
|
+
* short form that rode each /model row is gone with its caller). */
|
|
92
|
+
export declare function windowSourceNote(w: StatedWindow | null): string;
|
|
94
93
|
export declare function contextWindowTokens(of?: {
|
|
95
94
|
readonly model: string;
|
|
96
95
|
readonly baseUrl?: string;
|
package/dist/chat.js
CHANGED
|
@@ -122,15 +122,12 @@ export function windowLabel(tokens) {
|
|
|
122
122
|
return `${tokens / 1000}K`;
|
|
123
123
|
return tokens.toLocaleString("en-US");
|
|
124
124
|
}
|
|
125
|
-
/** CW-1: the source, said
|
|
126
|
-
* form
|
|
127
|
-
|
|
128
|
-
export function windowSourceNote(w, form) {
|
|
125
|
+
/** CW-1: the window and its source, said — /status's line (0.40.6: the
|
|
126
|
+
* short form that rode each /model row is gone with its caller). */
|
|
127
|
+
export function windowSourceNote(w) {
|
|
129
128
|
if (w === null)
|
|
130
|
-
return
|
|
129
|
+
return `window unknown — compaction assumes ${windowLabel(DEFAULT_CONTEXT_WINDOW)}; set contextWindow on the profile to state it`;
|
|
131
130
|
const size = windowLabel(w.tokens);
|
|
132
|
-
if (form === "short")
|
|
133
|
-
return w.source === "model" ? `ctx ${size} inferred` : `ctx ${size}`;
|
|
134
131
|
switch (w.source) {
|
|
135
132
|
case "set":
|
|
136
133
|
return `window ${size}, as you set it`;
|
package/dist/config.d.ts
CHANGED
|
@@ -118,6 +118,13 @@ export interface KisoConfig {
|
|
|
118
118
|
* in the child's worktree. A model never supplies a command; it names
|
|
119
119
|
* one of these. `{ "test": "npm test", "lint": "npm run lint" }`. */
|
|
120
120
|
readonly checks?: Readonly<Record<string, string>>;
|
|
121
|
+
/** CS-1 (0.40.7): the evaluator scripts a delegated task may name —
|
|
122
|
+
* absolute paths, `["/abs/path/to/evaluate.sh"]`. The PARENT runs the
|
|
123
|
+
* one a task names with the child's worktree as its argument. A path
|
|
124
|
+
* the user did not list is refused: the model chooses the task, so an
|
|
125
|
+
* unlisted path could be an interpreter running code the child wrote.
|
|
126
|
+
* Same layers as `checks`. */
|
|
127
|
+
readonly evaluators?: readonly string[];
|
|
121
128
|
}
|
|
122
129
|
/** The resolved, merged config — project wins over user, both validated. */
|
|
123
130
|
export interface ResolvedConfig {
|
|
@@ -145,7 +152,8 @@ export type ProfileAuth = {
|
|
|
145
152
|
readonly source: "store" | "env" | "none";
|
|
146
153
|
} | {
|
|
147
154
|
readonly type: "oauth";
|
|
148
|
-
readonly providerId: string;
|
|
155
|
+
readonly providerId: string; /** the access token's expiry (epoch ms) — past it, the next use renews */
|
|
156
|
+
readonly expires?: number;
|
|
149
157
|
};
|
|
150
158
|
/** Where a profile's sign-in comes from, or why it has none. The sign-in
|
|
151
159
|
* plan's resolve rule: a stored credential OWNS the provider (an
|
package/dist/config.js
CHANGED
|
@@ -32,7 +32,7 @@ import { readFileSync, statSync } from "node:fs";
|
|
|
32
32
|
import { homedir } from "node:os";
|
|
33
33
|
import { join } from "node:path";
|
|
34
34
|
import { kisoHome } from "./state.js";
|
|
35
|
-
import { AuthError, getCredential, providerIdOf } from "./auth/credentials.js";
|
|
35
|
+
import { AuthError, effectiveBaseUrl, endpointCredentialId, getCredential, providerIdOf } from "./auth/credentials.js";
|
|
36
36
|
import { BUILTIN_MANIFESTS } from "@vincemakes/kiso-runtime/internal";
|
|
37
37
|
/** Header names a profile may not set: credentials (a key never lives in the
|
|
38
38
|
* config file) and the framing the adapter owns. */
|
|
@@ -130,6 +130,15 @@ export function parseConfig(text, source) {
|
|
|
130
130
|
}
|
|
131
131
|
out.checks = obj.checks;
|
|
132
132
|
}
|
|
133
|
+
if (obj.evaluators !== undefined) {
|
|
134
|
+
if (!Array.isArray(obj.evaluators))
|
|
135
|
+
fail("evaluators", "expected a list of absolute paths to evaluator scripts");
|
|
136
|
+
for (const path of obj.evaluators) {
|
|
137
|
+
if (typeof path !== "string" || !path.startsWith("/"))
|
|
138
|
+
fail("evaluators", `expected an absolute path, got ${JSON.stringify(path)}`);
|
|
139
|
+
}
|
|
140
|
+
out.evaluators = obj.evaluators;
|
|
141
|
+
}
|
|
133
142
|
if (obj.model !== undefined) {
|
|
134
143
|
if (typeof obj.model !== "string" || obj.model === "")
|
|
135
144
|
fail("model", "expected a profile name or provider/model string");
|
|
@@ -253,6 +262,8 @@ export function mergeConfigs(user, project) {
|
|
|
253
262
|
...(p.projectTrust !== undefined ? { projectTrust: p.projectTrust } : {}),
|
|
254
263
|
// DT-1a: checks merge per name — a (trusted) project's check wins over the user's
|
|
255
264
|
...(u.checks !== undefined || p.checks !== undefined ? { checks: { ...(u.checks ?? {}), ...(p.checks ?? {}) } } : {}),
|
|
265
|
+
// CS-1: the evaluator lists join — either layer's script may be named
|
|
266
|
+
...(u.evaluators !== undefined || p.evaluators !== undefined ? { evaluators: [...(u.evaluators ?? []), ...(p.evaluators ?? [])] } : {}),
|
|
256
267
|
};
|
|
257
268
|
}
|
|
258
269
|
/** Where a profile's sign-in comes from, or why it has none. The sign-in
|
|
@@ -280,8 +291,14 @@ export function authForProfile(name, p) {
|
|
|
280
291
|
if (stored !== undefined) {
|
|
281
292
|
if (stored.type === "api-key")
|
|
282
293
|
return { type: "api-key", apiKey: stored.key, source: "store" };
|
|
283
|
-
if (p.kind === "openai-responses")
|
|
284
|
-
|
|
294
|
+
if (p.kind === "openai-responses") {
|
|
295
|
+
// 0.40.7: a sign-in whose renewal the endpoint REFUSED is over —
|
|
296
|
+
// unavailable here, where /model reads it, not first at a turn
|
|
297
|
+
if (stored.refreshRejectedAt !== undefined) {
|
|
298
|
+
throw new ConfigError(`model ${name}: unavailable — the ${providerId} sign-in was refused when kiso tried to renew it: run \`kiso login ${providerId}\``);
|
|
299
|
+
}
|
|
300
|
+
return { type: "oauth", providerId, expires: stored.expires };
|
|
301
|
+
}
|
|
285
302
|
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)"}`);
|
|
286
303
|
}
|
|
287
304
|
}
|
|
@@ -294,12 +311,34 @@ export function authForProfile(name, p) {
|
|
|
294
311
|
if (providerId !== null && !manifestTakesKey(providerId)) {
|
|
295
312
|
throw new ConfigError(`model ${name}: unavailable — not signed in: run \`kiso login ${providerId}\` (this provider takes no API key)`);
|
|
296
313
|
}
|
|
314
|
+
// 0.40.6: a GATEWAY's own key, stored for exactly this profile's origin
|
|
315
|
+
// (`kiso login --endpoint <url>`). The rule a vendor credential follows:
|
|
316
|
+
// stored first, the env var only when nothing is stored — and it goes to
|
|
317
|
+
// that origin alone (credentials.ts endpointCredentialId).
|
|
318
|
+
const endpointId = providerId === null ? endpointCredentialId(effectiveBaseUrl(p.kind, p.baseUrl)) : null;
|
|
319
|
+
if (endpointId !== null) {
|
|
320
|
+
let stored;
|
|
321
|
+
try {
|
|
322
|
+
stored = getCredential(endpointId);
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
if (err instanceof AuthError)
|
|
326
|
+
throw new ConfigError(`model ${name}: ${err.message}`);
|
|
327
|
+
throw err;
|
|
328
|
+
}
|
|
329
|
+
if (stored?.type === "api-key")
|
|
330
|
+
return { type: "api-key", apiKey: stored.key, source: "store" };
|
|
331
|
+
}
|
|
297
332
|
if (p.apiKeyEnv === undefined)
|
|
298
333
|
return { type: "api-key", apiKey: "none", source: "none" };
|
|
299
334
|
const fromEnv = process.env[p.apiKeyEnv];
|
|
300
335
|
if (fromEnv !== undefined)
|
|
301
336
|
return { type: "api-key", apiKey: fromEnv, source: "env" };
|
|
302
|
-
const hint = providerId !== null
|
|
337
|
+
const hint = providerId !== null
|
|
338
|
+
? `run \`kiso login ${providerId}\` or set the env var ${p.apiKeyEnv}`
|
|
339
|
+
: endpointId !== null
|
|
340
|
+
? `set the env var ${p.apiKeyEnv}, or run \`kiso login --endpoint ${endpointId.slice("endpoint:".length)}\``
|
|
341
|
+
: `set the env var ${p.apiKeyEnv}`;
|
|
303
342
|
throw new ConfigError(`model ${name}: unavailable — no credential: ${hint} (configs never store keys, only the env-var name)`);
|
|
304
343
|
}
|
|
305
344
|
/** OR-2: does the provider's manifest admit an API key (or no auth at all)?
|
package/dist/dispatch.js
CHANGED
|
@@ -13,6 +13,10 @@ import { agentBaseUrl, currentProfileName, setCurrentProfileName, currentModelNa
|
|
|
13
13
|
import { adapterOptionsFor } from "./auth/adapter-options.js";
|
|
14
14
|
import { profileProviderLabel, providerLabel } from "./provider-label.js";
|
|
15
15
|
import { installedVersion, versionStatusLine } from "./stale-version.js";
|
|
16
|
+
import { preferences, setPreference } from "./preferences.js";
|
|
17
|
+
import { settingsRows } from "./settings.js";
|
|
18
|
+
import { floorOn, settingsLayers } from "./state.js";
|
|
19
|
+
import { currentGround } from "@vincemakes/kiso-tui-cells/render";
|
|
16
20
|
import { queuedSwitchLines } from "./state.js";
|
|
17
21
|
import { contextWindowTokens, microcompactThresholdFor, startStatusSpinner, statedContextWindow, windowSourceNote } from "./chat.js";
|
|
18
22
|
import { authForProfile, directWriteProfile, profileAvailable, resolveContextWindow, unavailableReason } from "./config.js";
|
|
@@ -97,27 +101,37 @@ function effortAxis(p) {
|
|
|
97
101
|
: {}),
|
|
98
102
|
};
|
|
99
103
|
}
|
|
100
|
-
/** What signs a profile in, for the listing:
|
|
101
|
-
*
|
|
104
|
+
/** What signs a profile in, for the listing: `stored key` when a key in
|
|
105
|
+
* auth.json pays (a vendor's, or a gateway's `kiso login --endpoint`),
|
|
106
|
+
* `oauth` for a subscription sign-in, else the env var's name, or `no key`
|
|
107
|
+
* for an unauthenticated endpoint.
|
|
108
|
+
*
|
|
109
|
+
* 0406-F1: this named the env var whenever a profile had one, so a row
|
|
110
|
+
* paid by the stored key read `DEEPSEEK_API_KEY (available)` with that
|
|
111
|
+
* variable unset — and after 0.40.6's endpoint login every gateway row
|
|
112
|
+
* would have named a variable nobody exports any more. */
|
|
113
|
+
/** MP-1 (0.40.7): who a profile's requests are billed to — the endpoint
|
|
114
|
+
* and the credential that pays there (`@api.deepseek.com · stored key`). */
|
|
115
|
+
function payeeOf(p) {
|
|
116
|
+
const host = profileProviderLabel(p.kind, p.baseUrl, p.upstream);
|
|
117
|
+
const who = signInNote(p).replace(/, expired — renews on use$/, "");
|
|
118
|
+
return host === "" ? who : `${host} · ${who}`;
|
|
119
|
+
}
|
|
102
120
|
function signInNote(p) {
|
|
103
121
|
try {
|
|
104
122
|
const auth = authForProfile("?", p);
|
|
123
|
+
// 0.40.7: an access token past its expiry is not a dead sign-in — the
|
|
124
|
+
// next use renews it — but the row says so rather than nothing
|
|
105
125
|
if (auth.type === "oauth")
|
|
106
|
-
return "oauth";
|
|
126
|
+
return auth.expires !== undefined && auth.expires < Date.now() ? "oauth, expired — renews on use" : "oauth";
|
|
127
|
+
if (auth.source === "store")
|
|
128
|
+
return "stored key";
|
|
107
129
|
}
|
|
108
130
|
catch {
|
|
109
131
|
// unavailable — the availability mark says so; the env name still names what would sign it in
|
|
110
132
|
}
|
|
111
133
|
return p.apiKeyEnv ?? "no key";
|
|
112
134
|
}
|
|
113
|
-
/** CW-1 (owner, 2026-09-23): a profile's window on its /model row — the
|
|
114
|
-
* profile's own stated figure first, then the registry's chain for its
|
|
115
|
-
* model, endpoint and upstream; `inferred` marks the one no row states
|
|
116
|
-
* for that endpoint. */
|
|
117
|
-
function windowNoteOf(p) {
|
|
118
|
-
const w = statedContextWindow({ model: p.model, ...(p.baseUrl !== undefined ? { baseUrl: p.baseUrl } : {}), ...(p.upstream !== undefined ? { upstream: p.upstream } : {}) }, { configured: resolveContextWindow(mergedConfig, p) });
|
|
119
|
-
return windowSourceNote(w, "short");
|
|
120
|
-
}
|
|
121
135
|
/** The ONE dispatcher — slash commands, exit, and turns. The recovery
|
|
122
136
|
* replay routes through it too — a queued "/last" must never become a
|
|
123
137
|
* user turn (v2c: the rl lives in main, so lines arrive earlier and the
|
|
@@ -383,18 +397,15 @@ export function dispatch(line, ctx) {
|
|
|
383
397
|
return;
|
|
384
398
|
}
|
|
385
399
|
if (trimmed === "\x14think") {
|
|
386
|
-
// §2.3 — ctrl+t
|
|
387
|
-
//
|
|
388
|
-
// session is reprinted, so the blocks already on
|
|
389
|
-
// switch
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
// and the pipe's byte-identity gate is this row's gate too.
|
|
394
|
-
//
|
|
395
|
-
// The live `thinking…` placeholder is NOT touched: it belongs to
|
|
396
|
-
// the live region, and this switch is about committed blocks.
|
|
400
|
+
// §2.3 / 0.40.6 — ctrl+t hides thinking (one italic line per block,
|
|
401
|
+
// the open one included) and shows it again. DC-50's mechanism: one
|
|
402
|
+
// boolean, then the session is reprinted, so the blocks already on
|
|
403
|
+
// screen obey the switch. The choice is REMEMBERED (preferences.json)
|
|
404
|
+
// — the owner, 2026-09-23, after the reference implementation's
|
|
405
|
+
// persisted toggle; shown stays the default. The pipe keeps its own
|
|
406
|
+
// one-line fold whatever the choice.
|
|
397
407
|
body.toggleThinking();
|
|
408
|
+
setPreference("thinking", body.thinkingHidden() ? "hidden" : "shown");
|
|
398
409
|
ctx.input.prompt();
|
|
399
410
|
return;
|
|
400
411
|
}
|
|
@@ -487,6 +498,31 @@ export function dispatch(line, ctx) {
|
|
|
487
498
|
});
|
|
488
499
|
return;
|
|
489
500
|
}
|
|
501
|
+
if (trimmed === "/settings") {
|
|
502
|
+
// 0.40.6 — what kiso runs with, each value's layer, and how to change
|
|
503
|
+
// it (settings.ts). Read-only: the config file stays the human's.
|
|
504
|
+
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
505
|
+
const w = statedContextWindow();
|
|
506
|
+
const profile = currentProfileName;
|
|
507
|
+
bodyLog(settingsRows({
|
|
508
|
+
user: settingsLayers.user,
|
|
509
|
+
project: settingsLayers.project,
|
|
510
|
+
env: process.env,
|
|
511
|
+
...(settingsLayers.modeFlag !== undefined ? { modeFlag: settingsLayers.modeFlag } : {}),
|
|
512
|
+
...(settingsLayers.modelFlag !== undefined ? { modelFlag: settingsLayers.modelFlag } : {}),
|
|
513
|
+
mode: getMode(),
|
|
514
|
+
model: { label: `${agentModel}${providerLabel(agentBaseUrl, upstreamOf(agentBaseUrl))}${profile === null ? "" : ` · profile ${profile}`}`, profile, switched: settingsLayers.modelSwitched },
|
|
515
|
+
ground: currentGround(),
|
|
516
|
+
floorOn,
|
|
517
|
+
window: windowSourceNote(w).replace(/^window /, ""),
|
|
518
|
+
thinkingHidden: body.thinkingHidden(),
|
|
519
|
+
thinkingRemembered: preferences().thinking !== undefined,
|
|
520
|
+
version: versionStatusLine(installedVersion(), VERSION).replace(/^version /, ""),
|
|
521
|
+
}).join("\n\n"), "words");
|
|
522
|
+
ctx.input.prompt();
|
|
523
|
+
});
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
490
526
|
if (trimmed === "/status") {
|
|
491
527
|
// B area: session id, durable event count, and the ~ context
|
|
492
528
|
// estimate — all read straight from the live session, nothing
|
|
@@ -499,7 +535,7 @@ export function dispatch(line, ctx) {
|
|
|
499
535
|
// CW-1: the percentage names its denominator and who stated it — a
|
|
500
536
|
// window inferred from the model reads differently from one the
|
|
501
537
|
// registry states for this endpoint.
|
|
502
|
-
bodyLog(`ctx ${ctxPct} · ${windowSourceNote(statedContextWindow()
|
|
538
|
+
bodyLog(`ctx ${ctxPct} · ${windowSourceNote(statedContextWindow())}`);
|
|
503
539
|
// The owner, 2026-09-21: /status is where "what am I actually
|
|
504
540
|
// running on" is answered, so the identity is spelled out HERE —
|
|
505
541
|
// the model, the host the request goes to, the profile it came
|
|
@@ -546,6 +582,8 @@ export function dispatch(line, ctx) {
|
|
|
546
582
|
ctx.input.panelAsk(modePickView({
|
|
547
583
|
header: `mode — current: ${current}`,
|
|
548
584
|
options: OFFERED_MODES.map((name) => ({ label: name, note: [MODE_NOTE[name], ...(name === current ? ["current"] : [])].join(" · ") })),
|
|
585
|
+
// MP-1 (0.40.7): the cursor opens on the tier in force
|
|
586
|
+
...(OFFERED_MODES.indexOf(current) >= 0 ? { initial: OFFERED_MODES.indexOf(current) } : {}),
|
|
549
587
|
},
|
|
550
588
|
// DC-12 (design §4): a panel WAITING ON A HUMAN says ❯.
|
|
551
589
|
ctx.isRunning() ? "❯ run paused" : `▸ ${current}`), (v) => resolve(v.action === "picked" ? v.result : null));
|
|
@@ -622,9 +660,11 @@ export function dispatch(line, ctx) {
|
|
|
622
660
|
// profile key is what `/model <name>` takes, not what a chooser
|
|
623
661
|
// reading a list needs. What remains is what the chooser CANNOT
|
|
624
662
|
// infer: availability, and which one is live.
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
|
|
663
|
+
// 0.40.6 (the owner, 2026-09-23: "who told you to write ctx"):
|
|
664
|
+
// the window rode here in 0.40.3–0.40.5 and cut `unavailable`
|
|
665
|
+
// to `unavailabl…` on the owner's rows. It lives on /status and
|
|
666
|
+
// /settings, where there is room to say it and its source.
|
|
667
|
+
const marks = [...(profileAvailable(profile) ? [] : ["unavailable"]), ...(name === currentProfileName ? ["current"] : [])];
|
|
628
668
|
const host = profileProviderLabel(profile.kind, profile.baseUrl, profile.upstream);
|
|
629
669
|
return { label: `${profile.kind}/${profile.model}${host === "" ? "" : ` ${host}`}`, note: marks.join(" · "), ...effortAxis(profile) };
|
|
630
670
|
}),
|
|
@@ -633,6 +673,9 @@ export function dispatch(line, ctx) {
|
|
|
633
673
|
// "openai/…" hint failed with "no such model profile"
|
|
634
674
|
// on exactly the fresh-install path that shows it.
|
|
635
675
|
typeHint: names.length === 0 ? "type provider/model directly (e.g. openai-compat/deepseek-reasoner)" : "type provider/model directly",
|
|
676
|
+
// MP-1 (0.40.7): the cursor opens on the session's own
|
|
677
|
+
// profile — opening on row 0 plus one Enter changed who pays
|
|
678
|
+
...(currentProfileName !== null && names.indexOf(currentProfileName) >= 0 ? { initial: names.indexOf(currentProfileName) } : {}),
|
|
636
679
|
// the zero-profile copy is TODAY'S, verbatim: the
|
|
637
680
|
// user who sees it is exactly the user who needs
|
|
638
681
|
// the path spelled out
|
|
@@ -676,7 +719,7 @@ export function dispatch(line, ctx) {
|
|
|
676
719
|
for (const name of names) {
|
|
677
720
|
const p = configModels[name];
|
|
678
721
|
const hostOf = profileProviderLabel(p.kind, p.baseUrl, p.upstream);
|
|
679
|
-
bodyLog(` ${name} → ${p.kind}/${p.model}${hostOf === "" ? "" : ` ${hostOf}`} · ${signInNote(p)} ${profileAvailable(p) ? "(available)" : "(unavailable)"} · ${effortNote(p)}
|
|
722
|
+
bodyLog(` ${name} → ${p.kind}/${p.model}${hostOf === "" ? "" : ` ${hostOf}`} · ${signInNote(p)} ${profileAvailable(p) ? "(available)" : "(unavailable)"} · ${effortNote(p)}`);
|
|
680
723
|
}
|
|
681
724
|
}
|
|
682
725
|
bodyLog("switch with /model <profile-name|provider/model>");
|
|
@@ -768,11 +811,16 @@ export function dispatch(line, ctx) {
|
|
|
768
811
|
// display — a direct provider/model has none.
|
|
769
812
|
profileName: direct === null ? profName : null,
|
|
770
813
|
};
|
|
814
|
+
// MP-1 (0.40.7): who pays BEFORE the switch, read while the
|
|
815
|
+
// session still names the outgoing profile
|
|
816
|
+
const prevProfile = currentProfileName !== null ? configModels[currentProfileName] : undefined;
|
|
817
|
+
const payeeBefore = prevProfile !== undefined ? payeeOf(prevProfile) : null;
|
|
771
818
|
ctx.session.setModelBinding(binding);
|
|
772
819
|
setLastBinding(binding);
|
|
773
820
|
setAgentModel(profile.model, profile.baseUrl);
|
|
774
821
|
setCurrentModelName(arg);
|
|
775
822
|
setCurrentProfileName(direct === null ? null : profName);
|
|
823
|
+
settingsLayers.modelSwitched = true;
|
|
776
824
|
// §2.5: the ONE source a reload reads for the model — a
|
|
777
825
|
// switch made here must survive the rebuild.
|
|
778
826
|
setModelChoice(arg);
|
|
@@ -793,6 +841,13 @@ export function dispatch(line, ctx) {
|
|
|
793
841
|
// model beside the previous model's figure.
|
|
794
842
|
ctx.modelSwitched();
|
|
795
843
|
body.notice(`model → ${profName} (${profile.model}${providerLabel(profile.baseUrl, profile.upstream) === "" ? "" : ` ${providerLabel(profile.baseUrl, profile.upstream)}`}${effortTok !== undefined ? ` · ${effortTok}` : ""}) — takes effect on the next turn`);
|
|
844
|
+
// MP-1 (0.40.7): when the switch changes WHO PAYS — another
|
|
845
|
+
// endpoint, or another credential for it — say so, once,
|
|
846
|
+
// under the switch line. A switch between two models on the
|
|
847
|
+
// same account says nothing more.
|
|
848
|
+
const payeeAfter = payeeOf(profile);
|
|
849
|
+
if (payeeBefore !== null && payeeBefore !== payeeAfter)
|
|
850
|
+
body.notice(`paid by ${payeeAfter} from the next turn — was ${payeeBefore}`);
|
|
796
851
|
}
|
|
797
852
|
}
|
|
798
853
|
}
|
package/dist/index.js
CHANGED
|
@@ -53,6 +53,9 @@ import { isFirstRun, scaffoldFirstRun } from "./first-run.js";
|
|
|
53
53
|
import { fauxSkip, readFauxScript } from "./faux-glue.js";
|
|
54
54
|
import { chat, compactionDiscardedNotice, contextWindowTokens, displayCtxRatio, knownContextWindow, microcompactThresholdFor, statusModelLabel, unknownWindowNotice, windowLearnedNotice } from "./chat.js";
|
|
55
55
|
import { recordLearnedWindow, useLearnedWindows } from "./learned-windows.js";
|
|
56
|
+
import { preferences, usePreferences } from "./preferences.js";
|
|
57
|
+
import { sweepStaleMergeDirs } from "./temp-sweep.js";
|
|
58
|
+
import { settingsLayers } from "./state.js";
|
|
56
59
|
import { providerHost } from "./provider-label.js";
|
|
57
60
|
import { adapterOptionsFor } from "./auth/adapter-options.js";
|
|
58
61
|
import { loadProjectConfig, loadUserConfig, mergeConfigs, resolveAutoCompact, resolveContextWindow, resolveModel } from "./config.js";
|
|
@@ -66,6 +69,8 @@ import { armByteTrace } from "./byte-trace.js";
|
|
|
66
69
|
import { tmpdir, homedir } from "node:os";
|
|
67
70
|
import { clipboardImage } from "./clipboard.js";
|
|
68
71
|
import { cardsFromListings } from "./session-cards.js";
|
|
72
|
+
/** 0.40.7: the stale-merge-directory sweep runs once per process. */
|
|
73
|
+
let tempSwept = false;
|
|
69
74
|
// The moved exports stay reachable from this entry — the test imports
|
|
70
75
|
// (project-trust, coding-agent) never change (B4: zero assertion changes).
|
|
71
76
|
export { applyProjectMerges } from "./trust-ui.js";
|
|
@@ -759,6 +764,10 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
759
764
|
const userCfg = loadUserConfig();
|
|
760
765
|
const projectCfg = loadProjectConfig(process.cwd(), project !== null);
|
|
761
766
|
const merged = mergeConfigs(userCfg, projectCfg);
|
|
767
|
+
// 0.40.6: /settings names each value's layer from these
|
|
768
|
+
settingsLayers.user = userCfg;
|
|
769
|
+
settingsLayers.project = projectCfg;
|
|
770
|
+
settingsLayers.modelFlag = modelFlag;
|
|
762
771
|
const secretEnvNames = secretEnvNamesOf(merged.models ?? {});
|
|
763
772
|
const builtIn = await builtInLayer(user, proj, input !== undefined && process.stdin.isTTY ? askUi(input) : undefined, secretEnvNames);
|
|
764
773
|
setExtensionLists(builtIn, user, proj, [...builtIn, ...user, ...proj]);
|
|
@@ -776,11 +785,17 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
776
785
|
// channel and not an exported KISO_SESSIONS_DIR: a variable in
|
|
777
786
|
// process.env would reach every shell child, and a kiso started from a
|
|
778
787
|
// shell tool would then write into this project's folder.
|
|
779
|
-
process.env.KISO_DELEGATION_CONFIG_JSON = JSON.stringify({ checks: merged.checks ?? {}, profiles: Object.keys(merged.models ?? {}), sessionsDir: sessionsDir() });
|
|
788
|
+
process.env.KISO_DELEGATION_CONFIG_JSON = JSON.stringify({ checks: merged.checks ?? {}, evaluators: merged.evaluators ?? [], profiles: Object.keys(merged.models ?? {}), sessionsDir: sessionsDir() });
|
|
780
789
|
setConfigModels(merged.models ?? {});
|
|
781
790
|
// CW-1 batch 2: the windows endpoints stated by refusing — read before the
|
|
782
791
|
// first window is asked for (the unknown-window notice below).
|
|
783
792
|
useLearnedWindows();
|
|
793
|
+
// 0.40.7: the merge directories a killed kiso left in the temp dir —
|
|
794
|
+
// once per process, never this process's own (temp-sweep.ts)
|
|
795
|
+
if (!tempSwept) {
|
|
796
|
+
tempSwept = true;
|
|
797
|
+
sweepStaleMergeDirs();
|
|
798
|
+
}
|
|
784
799
|
const resolved = resolveModel(modelFlag, merged);
|
|
785
800
|
// AFTER the model resolves: the window a PROFILE states is about that
|
|
786
801
|
// profile's model, so it cannot be read before we know which profile is
|
|
@@ -1286,7 +1301,7 @@ async function reloadAgent(old, id, input, announce = true) {
|
|
|
1286
1301
|
rmSync(p, { recursive: true, force: true });
|
|
1287
1302
|
}
|
|
1288
1303
|
catch {
|
|
1289
|
-
// best-effort —
|
|
1304
|
+
// best-effort — a survivor is removed by a later startup's sweep (temp-sweep.ts) once this process is gone
|
|
1290
1305
|
}
|
|
1291
1306
|
}
|
|
1292
1307
|
mergedTempPaths.push(...oldTemps);
|
|
@@ -1315,7 +1330,7 @@ async function reloadAgent(old, id, input, announce = true) {
|
|
|
1315
1330
|
rmSync(p, { recursive: true, force: true });
|
|
1316
1331
|
}
|
|
1317
1332
|
catch {
|
|
1318
|
-
// best-effort —
|
|
1333
|
+
// best-effort — a survivor is removed by a later startup's sweep (temp-sweep.ts) once this process is gone
|
|
1319
1334
|
}
|
|
1320
1335
|
}
|
|
1321
1336
|
if (announce)
|
|
@@ -1643,6 +1658,7 @@ async function main() {
|
|
|
1643
1658
|
process.exit(2);
|
|
1644
1659
|
}
|
|
1645
1660
|
setMode(m);
|
|
1661
|
+
settingsLayers.modeFlag = m;
|
|
1646
1662
|
args.splice(modeFlag, 2);
|
|
1647
1663
|
}
|
|
1648
1664
|
else {
|
|
@@ -1790,6 +1806,10 @@ async function main() {
|
|
|
1790
1806
|
editCol: () => dock.editCol(),
|
|
1791
1807
|
onDock: () => dock.redraw(), // v2d-B: the freeze scrolls the dock up — re-pin it
|
|
1792
1808
|
}));
|
|
1809
|
+
// 0.40.6: the choices kiso remembers (preferences.json) — today ctrl+t's
|
|
1810
|
+
// thinking display, restored before the first block can be drawn.
|
|
1811
|
+
usePreferences();
|
|
1812
|
+
body.setThinkingHidden(preferences().thinking === "hidden");
|
|
1793
1813
|
// REL-0152-D11: pasting an image sends no bytes, so an empty paste is
|
|
1794
1814
|
// the signal to go and look at the clipboard. What comes back is a
|
|
1795
1815
|
// PATH, which the turn's attachment scan then picks up exactly as it
|
|
@@ -2001,8 +2021,33 @@ async function main() {
|
|
|
2001
2021
|
// `login <provider>` reads the key from a hidden prompt on a TTY
|
|
2002
2022
|
// or from stdin otherwise (never from an argument — shell history);
|
|
2003
2023
|
// `logout <provider>` deletes; `auth` lists, keys masked.
|
|
2004
|
-
const { KNOWN_PROVIDERS, OAUTH_PROVIDERS, authPath, deleteCredential, maskSecret, readAuthFile, setCredential } = await import("./auth/credentials.js");
|
|
2024
|
+
const { KNOWN_PROVIDERS, OAUTH_PROVIDERS, authPath, deleteCredential, endpointCredentialId, maskSecret, providerIdOf, readAuthFile, setCredential } = await import("./auth/credentials.js");
|
|
2005
2025
|
const provider = arg;
|
|
2026
|
+
if (provider === "--endpoint" && command !== "auth") {
|
|
2027
|
+
// 0.40.6: a gateway's own key, stored for its ORIGIN and sent
|
|
2028
|
+
// there alone (credentials.ts endpointCredentialId) — so a
|
|
2029
|
+
// gateway profile starts from plain `kiso`, no env var on the
|
|
2030
|
+
// command line. The key still never comes from an argument.
|
|
2031
|
+
const url = args[2];
|
|
2032
|
+
const id = endpointCredentialId(url);
|
|
2033
|
+
if (id === null)
|
|
2034
|
+
throw new CliUsageError(`kiso ${command} --endpoint <url> — an http(s) URL, e.g. https://gateway.example/v1`);
|
|
2035
|
+
const origin = id.slice("endpoint:".length);
|
|
2036
|
+
const vendor = ["openai-compat", "anthropic", "openai-responses"].map((k) => providerIdOf(k, url)).find((v) => v !== null);
|
|
2037
|
+
if (vendor !== undefined && vendor !== null)
|
|
2038
|
+
throw new CliUsageError(`${origin} is ${vendor}'s own endpoint — run \`kiso ${command} ${vendor}\``);
|
|
2039
|
+
if (command === "logout") {
|
|
2040
|
+
const had = deleteCredential(id);
|
|
2041
|
+
process.stdout.write(had ? `removed the key for ${origin}\n` : `nothing stored for ${origin}\n`);
|
|
2042
|
+
break;
|
|
2043
|
+
}
|
|
2044
|
+
const key = (await readSecret(`API key for ${origin}: `)).trim();
|
|
2045
|
+
if (key === "")
|
|
2046
|
+
throw new CliUsageError(`kiso login --endpoint ${origin}: no key given`);
|
|
2047
|
+
setCredential(id, { type: "api-key", key, savedAt: Date.now() });
|
|
2048
|
+
process.stdout.write(`stored an API key for ${origin} (${maskSecret(key)}) in ${authPath()} — sent to that origin only; every profile whose baseUrl is on it uses this key before its env var\n`);
|
|
2049
|
+
break;
|
|
2050
|
+
}
|
|
2006
2051
|
if (command === "auth") {
|
|
2007
2052
|
const file = readAuthFile();
|
|
2008
2053
|
const rows = Object.entries(file.credentials);
|
|
@@ -2011,7 +2056,7 @@ async function main() {
|
|
|
2011
2056
|
if (cred.type === "api-key")
|
|
2012
2057
|
lines.push(` ${id.padEnd(10)} api-key ${maskSecret(cred.key)} saved ${new Date(cred.savedAt).toISOString().slice(0, 10)}`);
|
|
2013
2058
|
else
|
|
2014
|
-
lines.push(` ${id.padEnd(10)} oauth ${cred.accountId ?? ""} expires ${new Date(cred.expires).toISOString()}${cred.expires < Date.now() ? " (expired)" : ""}`);
|
|
2059
|
+
lines.push(` ${id.padEnd(10)} oauth ${cred.accountId ?? ""} expires ${new Date(cred.expires).toISOString()}${cred.refreshRejectedAt !== undefined ? ` (renewal refused — run kiso login ${id})` : cred.expires < Date.now() ? " (expired — renews on use)" : ""}`);
|
|
2015
2060
|
}
|
|
2016
2061
|
const envs = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"].filter((k) => process.env[k] !== undefined);
|
|
2017
2062
|
lines.push(`env vars set: ${envs.length ? envs.join(", ") : "none"} (a stored credential owns its provider; env applies only when nothing is stored)`);
|
|
@@ -2019,7 +2064,7 @@ async function main() {
|
|
|
2019
2064
|
break;
|
|
2020
2065
|
}
|
|
2021
2066
|
if (provider === undefined || !KNOWN_PROVIDERS.includes(provider)) {
|
|
2022
|
-
throw new CliUsageError(`kiso ${command} <provider> — one of: ${KNOWN_PROVIDERS.join(", ")}`);
|
|
2067
|
+
throw new CliUsageError(`kiso ${command} <provider> — one of: ${KNOWN_PROVIDERS.join(", ")}; or kiso ${command} --endpoint <url> for a gateway`);
|
|
2023
2068
|
}
|
|
2024
2069
|
if (command === "logout") {
|
|
2025
2070
|
const had = deleteCredential(provider);
|
|
@@ -2096,7 +2141,8 @@ async function main() {
|
|
|
2096
2141
|
" kiso sessions [--all|--current] list durable sessions (a terminal shows this workspace's by default)\n" +
|
|
2097
2142
|
" kiso login <provider> anthropic|openai|deepseek|zai: store an API key (hidden prompt, or stdin when piped);\n" +
|
|
2098
2143
|
" chatgpt: sign in with a ChatGPT subscription (browser; unofficial third-party flow)\n" +
|
|
2099
|
-
" kiso
|
|
2144
|
+
" kiso login --endpoint <url> a gateway: store its API key for that URL's origin only\n" +
|
|
2145
|
+
" kiso logout <provider> remove the stored credential (or --endpoint <url>)\n" +
|
|
2100
2146
|
" kiso auth list stored credentials (keys masked)\n" +
|
|
2101
2147
|
" kiso update install the latest release (npm i -g @vincemakes/kiso-code@latest)\n" +
|
|
2102
2148
|
" kiso help this help\n\n" +
|
|
@@ -2109,7 +2155,7 @@ async function main() {
|
|
|
2109
2155
|
" OPENAI_API_KEY OpenAI-compatible (OPENAI_MODEL, default gpt-4o;\n" +
|
|
2110
2156
|
" OPENAI_BASE_URL for DeepSeek/compat endpoints) — checked first\n" +
|
|
2111
2157
|
" ANTHROPIC_API_KEY Anthropic (ANTHROPIC_MODEL, default claude-sonnet-5)\n" +
|
|
2112
|
-
" ~/.kiso/config.json named model profiles (keys stay in env vars; see the README)\n");
|
|
2158
|
+
" ~/.kiso/config.json named model profiles (keys stay in env vars or kiso login; see the README)\n");
|
|
2113
2159
|
break;
|
|
2114
2160
|
}
|
|
2115
2161
|
case undefined:
|
|
@@ -2161,7 +2207,7 @@ async function main() {
|
|
|
2161
2207
|
rmSync(p, { recursive: true, force: true });
|
|
2162
2208
|
}
|
|
2163
2209
|
catch {
|
|
2164
|
-
// best-effort —
|
|
2210
|
+
// best-effort — a survivor is removed by a later startup's sweep (temp-sweep.ts) once this process is gone
|
|
2165
2211
|
}
|
|
2166
2212
|
}
|
|
2167
2213
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface Preferences {
|
|
2
|
+
/** ctrl+t: the thinking blocks as one italic line each. Absent = shown. */
|
|
3
|
+
readonly thinking?: "shown" | "hidden";
|
|
4
|
+
}
|
|
5
|
+
export declare function preferencesPath(home?: string): string;
|
|
6
|
+
/** Startup: read the file, and keep choices made from here on in it. */
|
|
7
|
+
export declare function usePreferences(path?: string): void;
|
|
8
|
+
/** The remembered choices; empty before startup names the file. */
|
|
9
|
+
export declare function preferences(): Preferences;
|
|
10
|
+
/** Remember a choice. Returns false when no file was named. The file is
|
|
11
|
+
* re-read before the write, so another process's choice is kept. */
|
|
12
|
+
export declare function setPreference<K extends keyof Preferences>(key: K, value: NonNullable<Preferences[K]>): boolean;
|
|
13
|
+
/** Tests: back to the state before startup. */
|
|
14
|
+
export declare function resetPreferences(): void;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.40.6 — the choices kiso remembers for you, in
|
|
3
|
+
* `<KISO_HOME>/preferences.json`.
|
|
4
|
+
*
|
|
5
|
+
* `config.json` is the human's file: kiso reads it and never writes it. A
|
|
6
|
+
* choice made with a key — ctrl+t's thinking display, today the only one —
|
|
7
|
+
* has to survive a restart, so it lands here instead: kiso-owned, private
|
|
8
|
+
* (0600, tmp + rename), and read back at start-up. A missing or unreadable
|
|
9
|
+
* file is an empty one; an unknown value is ignored rather than guessed.
|
|
10
|
+
*
|
|
11
|
+
* Nothing is read until the CLI's startup names the file
|
|
12
|
+
* (`usePreferences`) — the same rule as learned-windows.ts, so in-process
|
|
13
|
+
* tests never read a home directory.
|
|
14
|
+
*/
|
|
15
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { kisoHome } from "./state.js";
|
|
18
|
+
let prefs = null;
|
|
19
|
+
let prefsPath = null;
|
|
20
|
+
export function preferencesPath(home = kisoHome()) {
|
|
21
|
+
return join(home, "preferences.json");
|
|
22
|
+
}
|
|
23
|
+
function read(path) {
|
|
24
|
+
try {
|
|
25
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
26
|
+
return raw.thinking === "shown" || raw.thinking === "hidden" ? { thinking: raw.thinking } : {};
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Startup: read the file, and keep choices made from here on in it. */
|
|
33
|
+
export function usePreferences(path = preferencesPath()) {
|
|
34
|
+
prefsPath = path;
|
|
35
|
+
prefs = read(path);
|
|
36
|
+
}
|
|
37
|
+
/** The remembered choices; empty before startup names the file. */
|
|
38
|
+
export function preferences() {
|
|
39
|
+
return prefs ?? {};
|
|
40
|
+
}
|
|
41
|
+
/** Remember a choice. Returns false when no file was named. The file is
|
|
42
|
+
* re-read before the write, so another process's choice is kept. */
|
|
43
|
+
export function setPreference(key, value) {
|
|
44
|
+
const path = prefsPath;
|
|
45
|
+
if (path === null)
|
|
46
|
+
return false;
|
|
47
|
+
const next = { ...read(path), [key]: value };
|
|
48
|
+
prefs = next;
|
|
49
|
+
try {
|
|
50
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
51
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
52
|
+
writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
53
|
+
renameSync(tmp, path);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// an unwritable home: the choice still holds for this process
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
/** Tests: back to the state before startup. */
|
|
61
|
+
export function resetPreferences() {
|
|
62
|
+
prefs = null;
|
|
63
|
+
prefsPath = null;
|
|
64
|
+
}
|
package/dist/readonly-shell.js
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
import { statSync } from "node:fs";
|
|
34
34
|
import { homedir } from "node:os";
|
|
35
35
|
import { basename, isAbsolute, join, relative } from "node:path";
|
|
36
|
-
import { isCredentialName } from "@vincemakes/kiso-tools-node";
|
|
36
|
+
import { isCredentialName, isCredentialPath } from "@vincemakes/kiso-tools-node";
|
|
37
37
|
import { getMode } from "./mode.js";
|
|
38
38
|
import { HOME_SUBTREES, parseShell, realCase, resolveShellPath } from "./shell-words.js";
|
|
39
39
|
// ── the predicates ──────────────────────────────────────────────────────
|
|
@@ -55,15 +55,11 @@ function scope(ctx, word) {
|
|
|
55
55
|
}
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
|
-
/** B4
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* finding RO-F4, 0.41.0.) */
|
|
62
|
-
const SHELL_CREDENTIAL_NAMES = new Set([".npmrc", ".pypirc", ".git-credentials", ".htpasswd"]);
|
|
63
|
-
const SHELL_CREDENTIAL_PATHS = ["/.aws/credentials", "/.config/gh/hosts.yml", "/.docker/config.json", "/.kube/config"];
|
|
58
|
+
/** B4, and since RO-F4 (0.40.7) the search corpus's own set: the shell and
|
|
59
|
+
* the file tools ask ONE rule — names (`isCredentialName`) and the
|
|
60
|
+
* directory-scoped paths (`isCredentialPath`) — on the canonical path. */
|
|
64
61
|
function shellCredential(canonical) {
|
|
65
|
-
|
|
66
|
-
return SHELL_CREDENTIAL_NAMES.has(basename(folded)) || SHELL_CREDENTIAL_PATHS.some((s) => folded.endsWith(s));
|
|
62
|
+
return isCredentialName(basename(canonical).toLowerCase()) || isCredentialPath(canonical);
|
|
67
63
|
}
|
|
68
64
|
function content(ctx, word) {
|
|
69
65
|
const s = scope(ctx, word);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.40.6 — `/settings`: what kiso is running with, where each value came
|
|
3
|
+
* from, and how to change it.
|
|
4
|
+
*
|
|
5
|
+
* The owner, 2026-09-23, asked whether kiso has a help for what can be
|
|
6
|
+
* configured. `/help` lists the commands and `?` the keys; the settings
|
|
7
|
+
* lived only in docs/configuration.md and in files the user had to open.
|
|
8
|
+
*
|
|
9
|
+
* READ-ONLY by design: the config file stays the human's. The layers are
|
|
10
|
+
* the documented precedence — flag > env > project config > user config >
|
|
11
|
+
* default — and a value that no layer explains was set in this session
|
|
12
|
+
* (`/mode`, `/model`). Pure: every input is passed in, so each row is
|
|
13
|
+
* testable without a session.
|
|
14
|
+
*/
|
|
15
|
+
import type { KisoConfig } from "./config.js";
|
|
16
|
+
export interface SettingsInput {
|
|
17
|
+
readonly user: KisoConfig | null;
|
|
18
|
+
readonly project: KisoConfig | null;
|
|
19
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
20
|
+
/** the --mode / --model values on the command line, when given */
|
|
21
|
+
readonly modeFlag?: string;
|
|
22
|
+
readonly modelFlag?: string;
|
|
23
|
+
/** what is live now */
|
|
24
|
+
readonly mode: string;
|
|
25
|
+
readonly model: {
|
|
26
|
+
readonly label: string;
|
|
27
|
+
readonly profile: string | null;
|
|
28
|
+
readonly switched: boolean;
|
|
29
|
+
};
|
|
30
|
+
readonly ground: string;
|
|
31
|
+
readonly floorOn: boolean;
|
|
32
|
+
readonly window: string;
|
|
33
|
+
readonly thinkingHidden: boolean;
|
|
34
|
+
readonly thinkingRemembered: boolean;
|
|
35
|
+
readonly version: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function settingsRows(i: SettingsInput): string[];
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
function modeRow(i) {
|
|
2
|
+
const change = "/mode or shift+tab; \"mode\" in ~/.kiso/config.json";
|
|
3
|
+
const from = i.modeFlag === i.mode
|
|
4
|
+
? "--mode"
|
|
5
|
+
: i.env.KISO_MODE === i.mode
|
|
6
|
+
? "env KISO_MODE"
|
|
7
|
+
: i.project?.mode === i.mode
|
|
8
|
+
? "project config"
|
|
9
|
+
: i.user?.mode === i.mode
|
|
10
|
+
? "user config"
|
|
11
|
+
: i.mode === "default"
|
|
12
|
+
? "default"
|
|
13
|
+
: "set in this session";
|
|
14
|
+
return { name: "mode", value: i.mode, from, change };
|
|
15
|
+
}
|
|
16
|
+
function modelRow(i) {
|
|
17
|
+
const change = "/model; \"model\" in ~/.kiso/config.json";
|
|
18
|
+
const p = i.model.profile;
|
|
19
|
+
const from = i.model.switched
|
|
20
|
+
? "set in this session"
|
|
21
|
+
: i.modelFlag !== undefined
|
|
22
|
+
? "--model"
|
|
23
|
+
: p !== null && i.project?.model === p
|
|
24
|
+
? "project config"
|
|
25
|
+
: p !== null && i.user?.model === p
|
|
26
|
+
? "user config"
|
|
27
|
+
: i.env.OPENAI_API_KEY !== undefined && p === null
|
|
28
|
+
? "env OPENAI_API_KEY"
|
|
29
|
+
: "default";
|
|
30
|
+
return { name: "model", value: i.model.label, from, change };
|
|
31
|
+
}
|
|
32
|
+
function themeRow(i) {
|
|
33
|
+
const change = "\"theme\": \"dark\" | \"light\" in ~/.kiso/config.json; KISO_THEME for one run";
|
|
34
|
+
if (i.env.KISO_THEME !== undefined)
|
|
35
|
+
return { name: "theme", value: i.env.KISO_THEME, from: "env KISO_THEME", change };
|
|
36
|
+
if (i.user?.theme !== undefined)
|
|
37
|
+
return { name: "theme", value: i.user.theme, from: "user config", change };
|
|
38
|
+
return { name: "theme", value: i.ground, from: "the terminal (detected)", change };
|
|
39
|
+
}
|
|
40
|
+
function layered(i, key) {
|
|
41
|
+
if (i.project?.[key] !== undefined)
|
|
42
|
+
return { value: i.project[key], from: "project config" };
|
|
43
|
+
if (i.user?.[key] !== undefined)
|
|
44
|
+
return { value: i.user[key], from: "user config" };
|
|
45
|
+
return { value: undefined, from: "default" };
|
|
46
|
+
}
|
|
47
|
+
export function settingsRows(i) {
|
|
48
|
+
const trust = layered(i, "projectTrust");
|
|
49
|
+
const auto = i.env.KISO_AUTO_COMPACT !== undefined ? { value: i.env.KISO_AUTO_COMPACT, from: "env KISO_AUTO_COMPACT" } : layered(i, "autoCompact");
|
|
50
|
+
const rows = [
|
|
51
|
+
modelRow(i),
|
|
52
|
+
modeRow(i),
|
|
53
|
+
themeRow(i),
|
|
54
|
+
{
|
|
55
|
+
name: "floor",
|
|
56
|
+
value: i.floorOn ? "on — irrecoverable deletes are refused in every mode" : "off",
|
|
57
|
+
from: i.user?.floor !== undefined ? "user config" : "default",
|
|
58
|
+
change: "\"floor\": \"off\" in ~/.kiso/config.json (user config only)",
|
|
59
|
+
},
|
|
60
|
+
{ name: "window", value: i.window, from: "the window chain (see /status)", change: "\"contextWindow\" on the profile; KISO_CONTEXT_WINDOW for one run" },
|
|
61
|
+
{ name: "compaction", value: "in-run: past half the window at a phase end, past 80% at once", from: "built in", change: "/compact on demand" },
|
|
62
|
+
{
|
|
63
|
+
name: "auto-compact",
|
|
64
|
+
value: auto.value === undefined ? "off" : typeof auto.value === "object" ? `at ${auto.value.thresholdRatio} of the window, at a run's start` : String(auto.value),
|
|
65
|
+
from: auto.from,
|
|
66
|
+
change: "\"autoCompact\" in config; KISO_AUTO_COMPACT",
|
|
67
|
+
},
|
|
68
|
+
{ name: "project trust", value: trust.value ?? "ask", from: trust.from, change: "\"projectTrust\": \"ask\" | \"never\" in config" },
|
|
69
|
+
{
|
|
70
|
+
name: "thinking",
|
|
71
|
+
value: i.thinkingHidden ? "hidden — one line per block" : "shown",
|
|
72
|
+
from: i.thinkingRemembered ? "ctrl+t (remembered)" : "default",
|
|
73
|
+
change: "ctrl+t",
|
|
74
|
+
},
|
|
75
|
+
{ name: "version", value: i.version, from: "running", change: "kiso update" },
|
|
76
|
+
];
|
|
77
|
+
const width = Math.max(...rows.map((r) => r.name.length)) + 2;
|
|
78
|
+
return rows.map((r) => `${r.name.padEnd(width)}${r.value}\n${" ".repeat(width)}from ${r.from} · change: ${r.change}`);
|
|
79
|
+
}
|
package/dist/state.d.ts
CHANGED
|
@@ -295,6 +295,16 @@ export declare const queuedSwitchLines: string[];
|
|
|
295
295
|
/** E1: the extensions loaded by makeAgent — their names feed the banner. */
|
|
296
296
|
/** 0.40.0: whether the catastrophe floor is on (floor.ts) — the user
|
|
297
297
|
* config's `floor`, read where the chain is assembled. */
|
|
298
|
+
/** 0.40.6: what /settings needs to name each value's layer — the two config
|
|
299
|
+
* files as read, the command-line flags as given, and whether /model
|
|
300
|
+
* changed the binding in this session. */
|
|
301
|
+
export declare const settingsLayers: {
|
|
302
|
+
user: import("./config.js").KisoConfig | null;
|
|
303
|
+
project: import("./config.js").KisoConfig | null;
|
|
304
|
+
modeFlag: string | undefined;
|
|
305
|
+
modelFlag: string | undefined;
|
|
306
|
+
modelSwitched: boolean;
|
|
307
|
+
};
|
|
298
308
|
export declare let floorOn: boolean;
|
|
299
309
|
export declare function setFloorOn(value: boolean): void;
|
|
300
310
|
/** 0.40.0: the calls a saved allow never carries (protected-writes.ts) —
|
package/dist/state.js
CHANGED
|
@@ -397,6 +397,10 @@ export const queuedSwitchLines = [];
|
|
|
397
397
|
/** E1: the extensions loaded by makeAgent — their names feed the banner. */
|
|
398
398
|
/** 0.40.0: whether the catastrophe floor is on (floor.ts) — the user
|
|
399
399
|
* config's `floor`, read where the chain is assembled. */
|
|
400
|
+
/** 0.40.6: what /settings needs to name each value's layer — the two config
|
|
401
|
+
* files as read, the command-line flags as given, and whether /model
|
|
402
|
+
* changed the binding in this session. */
|
|
403
|
+
export const settingsLayers = { user: null, project: null, modeFlag: undefined, modelFlag: undefined, modelSwitched: false };
|
|
400
404
|
export let floorOn = true;
|
|
401
405
|
export function setFloorOn(value) {
|
|
402
406
|
floorOn = value;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const LEGACY_MAX_AGE_MS: number;
|
|
2
|
+
/** One startup never spends long here, whatever the temp directory holds. */
|
|
3
|
+
export declare const SWEEP_LIMIT = 200;
|
|
4
|
+
/** The mkdtemp prefix for a merge directory: this process's pid in the name. */
|
|
5
|
+
export declare function mergeDirPrefix(kind: "mcp" | "skills"): string;
|
|
6
|
+
/** Is a process with this pid running? EPERM means it is, and belongs to
|
|
7
|
+
* someone else; only ESRCH says it is gone. */
|
|
8
|
+
export declare function pidAlive(pid: number): boolean;
|
|
9
|
+
/** Remove the merge directories whose owner is gone (and legacy ones a week
|
|
10
|
+
* old). Returns the names removed. Every failure is skipped — a sweep is
|
|
11
|
+
* housekeeping, never a reason for a session not to start. */
|
|
12
|
+
export declare function sweepStaleMergeDirs(dir?: string, now?: number, alive?: (pid: number) => boolean): string[];
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.40.7 — the merge directories a killed kiso left behind.
|
|
3
|
+
*
|
|
4
|
+
* A project with its own `.kiso/mcp.json` or skills gets a merged copy in
|
|
5
|
+
* the temp directory (`kiso-mcp-*`, `kiso-skills-*`, trust-ui.ts). A clean
|
|
6
|
+
* exit removes it; `kill -9` does not, and macOS's temp cleaner removes old
|
|
7
|
+
* FILES but leaves directories — so every killed session left one behind
|
|
8
|
+
* for good (the owner's machine once held 449,956 of the test suite's).
|
|
9
|
+
*
|
|
10
|
+
* The directory's name now carries its owner's pid
|
|
11
|
+
* (`kiso-mcp-<pid>-XXXXXX`). At startup kiso removes the ones whose owner
|
|
12
|
+
* is gone; a live session's directory is never touched. Names from before
|
|
13
|
+
* this version carry no pid: those are removed only when untouched for a
|
|
14
|
+
* week, which no live session's merge can be (it is read at every reload).
|
|
15
|
+
* Only directories this user owns are considered; nothing else in the
|
|
16
|
+
* temp directory is read.
|
|
17
|
+
*/
|
|
18
|
+
import { lstatSync, readdirSync, rmSync } from "node:fs";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
export const LEGACY_MAX_AGE_MS = 7 * 24 * 3600_000;
|
|
22
|
+
/** One startup never spends long here, whatever the temp directory holds. */
|
|
23
|
+
export const SWEEP_LIMIT = 200;
|
|
24
|
+
const OWNED = /^kiso-(?:mcp|skills)-(\d+)-[A-Za-z0-9]{6}$/;
|
|
25
|
+
const LEGACY = /^kiso-(?:mcp|skills)-[A-Za-z0-9]{6}$/;
|
|
26
|
+
/** The mkdtemp prefix for a merge directory: this process's pid in the name. */
|
|
27
|
+
export function mergeDirPrefix(kind) {
|
|
28
|
+
return join(tmpdir(), `kiso-${kind}-${process.pid}-`);
|
|
29
|
+
}
|
|
30
|
+
/** Is a process with this pid running? EPERM means it is, and belongs to
|
|
31
|
+
* someone else; only ESRCH says it is gone. */
|
|
32
|
+
export function pidAlive(pid) {
|
|
33
|
+
try {
|
|
34
|
+
process.kill(pid, 0);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
return err.code === "EPERM";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Remove the merge directories whose owner is gone (and legacy ones a week
|
|
42
|
+
* old). Returns the names removed. Every failure is skipped — a sweep is
|
|
43
|
+
* housekeeping, never a reason for a session not to start. */
|
|
44
|
+
export function sweepStaleMergeDirs(dir = tmpdir(), now = Date.now(), alive = pidAlive) {
|
|
45
|
+
let names;
|
|
46
|
+
try {
|
|
47
|
+
names = readdirSync(dir);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
53
|
+
const removed = [];
|
|
54
|
+
for (const name of names) {
|
|
55
|
+
if (removed.length >= SWEEP_LIMIT)
|
|
56
|
+
break;
|
|
57
|
+
const owned = OWNED.exec(name);
|
|
58
|
+
if (owned === null && !LEGACY.test(name))
|
|
59
|
+
continue;
|
|
60
|
+
const full = join(dir, name);
|
|
61
|
+
try {
|
|
62
|
+
const st = lstatSync(full);
|
|
63
|
+
if (!st.isDirectory() || (uid !== undefined && st.uid !== uid))
|
|
64
|
+
continue;
|
|
65
|
+
const stale = owned !== null ? Number(owned[1]) !== process.pid && !alive(Number(owned[1])) : now - st.mtimeMs > LEGACY_MAX_AGE_MS;
|
|
66
|
+
if (!stale)
|
|
67
|
+
continue;
|
|
68
|
+
rmSync(full, { recursive: true, force: true });
|
|
69
|
+
removed.push(name);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// gone already, or not ours to remove
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return removed;
|
|
76
|
+
}
|
package/dist/trust-ui.js
CHANGED
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
* decisions. All bodies moved verbatim from index.ts.
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, symlinkSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { homedir
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { pathToFileURL } from "node:url";
|
|
12
12
|
import { askDeclineAll, askView, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView, unansweredAskView } from "@vincemakes/kiso-tui";
|
|
13
13
|
import { projectArtifacts, recordTrust, trustFor } from "@vincemakes/kiso-runtime";
|
|
14
14
|
import { bodyLog, currentAgentExtensions, dock, extensionsDir, kisoHome, mergedTempPaths, neverInherited } from "./state.js";
|
|
15
15
|
import { guardSavedAllow } from "./protected-writes.js";
|
|
16
|
+
import { mergeDirPrefix } from "./temp-sweep.js";
|
|
16
17
|
import { loadUserConfig, resolveProjectTrustPolicy } from "./config.js";
|
|
17
18
|
import { getMode } from "./mode.js";
|
|
18
19
|
/**
|
|
@@ -415,7 +416,7 @@ function applyMcpMerge(root) {
|
|
|
415
416
|
// the paths the previous set owned deleted the file the new set had just
|
|
416
417
|
// been merged into. One directory per merge, and the directory is what is
|
|
417
418
|
// recorded, so the cleanup removes exactly what that merge created.
|
|
418
|
-
const dir = mkdtempSync(
|
|
419
|
+
const dir = mkdtempSync(mergeDirPrefix("mcp")); // 0.40.7: the owner's pid in the name (temp-sweep.ts)
|
|
419
420
|
const temp = join(dir, "mcp.json");
|
|
420
421
|
writeFileSync(temp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
421
422
|
process.env.KISO_MCP_CONFIG = temp;
|
|
@@ -442,7 +443,7 @@ function applySkillsMerge(root) {
|
|
|
442
443
|
originalUserSkillsDir ??= process.env.KISO_SKILLS_DIR ?? join(kisoHome(), "skills");
|
|
443
444
|
const userDir = originalUserSkillsDir;
|
|
444
445
|
const projectDir = join(root, "skills");
|
|
445
|
-
const merged = mkdtempSync(
|
|
446
|
+
const merged = mkdtempSync(mergeDirPrefix("skills"));
|
|
446
447
|
mergedTempPaths.push(merged);
|
|
447
448
|
for (const dir of readdirSyncSafe(projectDir)) {
|
|
448
449
|
symlinkSync(join(projectDir, dir), join(merged, dir)); // project wins on collision
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-code",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.7",
|
|
4
4
|
"description": "kiso-code \u2014 the coding agent on kiso, the durable runtime for AI agents: kiso chat / kiso resume / kiso sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,20 +18,20 @@
|
|
|
18
18
|
"test": "vitest run"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@vincemakes/kiso-ask-ext": "0.40.
|
|
22
|
-
"@vincemakes/kiso-core": "0.40.
|
|
23
|
-
"@vincemakes/kiso-evals": "0.40.
|
|
24
|
-
"@vincemakes/kiso-mcp-ext": "0.40.
|
|
25
|
-
"@vincemakes/kiso-provider-anthropic": "0.40.
|
|
26
|
-
"@vincemakes/kiso-provider-openai": "0.40.
|
|
27
|
-
"@vincemakes/kiso-provider-openai-responses": "0.40.
|
|
28
|
-
"@vincemakes/kiso-runtime": "0.40.
|
|
29
|
-
"@vincemakes/kiso-skills-ext": "0.40.
|
|
30
|
-
"@vincemakes/kiso-subagent-ext": "0.40.
|
|
31
|
-
"@vincemakes/kiso-task-ext": "0.40.
|
|
32
|
-
"@vincemakes/kiso-tools-node": "0.40.
|
|
33
|
-
"@vincemakes/kiso-tui": "0.40.
|
|
34
|
-
"@vincemakes/kiso-tui-cells": "0.40.
|
|
21
|
+
"@vincemakes/kiso-ask-ext": "0.40.7",
|
|
22
|
+
"@vincemakes/kiso-core": "0.40.7",
|
|
23
|
+
"@vincemakes/kiso-evals": "0.40.7",
|
|
24
|
+
"@vincemakes/kiso-mcp-ext": "0.40.7",
|
|
25
|
+
"@vincemakes/kiso-provider-anthropic": "0.40.7",
|
|
26
|
+
"@vincemakes/kiso-provider-openai": "0.40.7",
|
|
27
|
+
"@vincemakes/kiso-provider-openai-responses": "0.40.7",
|
|
28
|
+
"@vincemakes/kiso-runtime": "0.40.7",
|
|
29
|
+
"@vincemakes/kiso-skills-ext": "0.40.7",
|
|
30
|
+
"@vincemakes/kiso-subagent-ext": "0.40.7",
|
|
31
|
+
"@vincemakes/kiso-task-ext": "0.40.7",
|
|
32
|
+
"@vincemakes/kiso-tools-node": "0.40.7",
|
|
33
|
+
"@vincemakes/kiso-tui": "0.40.7",
|
|
34
|
+
"@vincemakes/kiso-tui-cells": "0.40.7"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^26.1.2",
|