@vincemakes/kiso-code 0.40.6 → 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 +4 -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/config.d.ts +9 -1
- package/dist/config.js +19 -2
- package/dist/dispatch.js +37 -3
- package/dist/index.js +14 -5
- package/dist/readonly-shell.js +5 -9
- 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;
|
|
@@ -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/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
|
@@ -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
|
}
|
package/dist/dispatch.js
CHANGED
|
@@ -101,13 +101,31 @@ function effortAxis(p) {
|
|
|
101
101
|
: {}),
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
|
-
/** What signs a profile in, for the listing:
|
|
105
|
-
*
|
|
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
|
+
}
|
|
106
120
|
function signInNote(p) {
|
|
107
121
|
try {
|
|
108
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
|
|
109
125
|
if (auth.type === "oauth")
|
|
110
|
-
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";
|
|
111
129
|
}
|
|
112
130
|
catch {
|
|
113
131
|
// unavailable — the availability mark says so; the env name still names what would sign it in
|
|
@@ -564,6 +582,8 @@ export function dispatch(line, ctx) {
|
|
|
564
582
|
ctx.input.panelAsk(modePickView({
|
|
565
583
|
header: `mode — current: ${current}`,
|
|
566
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) } : {}),
|
|
567
587
|
},
|
|
568
588
|
// DC-12 (design §4): a panel WAITING ON A HUMAN says ❯.
|
|
569
589
|
ctx.isRunning() ? "❯ run paused" : `▸ ${current}`), (v) => resolve(v.action === "picked" ? v.result : null));
|
|
@@ -653,6 +673,9 @@ export function dispatch(line, ctx) {
|
|
|
653
673
|
// "openai/…" hint failed with "no such model profile"
|
|
654
674
|
// on exactly the fresh-install path that shows it.
|
|
655
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) } : {}),
|
|
656
679
|
// the zero-profile copy is TODAY'S, verbatim: the
|
|
657
680
|
// user who sees it is exactly the user who needs
|
|
658
681
|
// the path spelled out
|
|
@@ -788,6 +811,10 @@ export function dispatch(line, ctx) {
|
|
|
788
811
|
// display — a direct provider/model has none.
|
|
789
812
|
profileName: direct === null ? profName : null,
|
|
790
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;
|
|
791
818
|
ctx.session.setModelBinding(binding);
|
|
792
819
|
setLastBinding(binding);
|
|
793
820
|
setAgentModel(profile.model, profile.baseUrl);
|
|
@@ -814,6 +841,13 @@ export function dispatch(line, ctx) {
|
|
|
814
841
|
// model beside the previous model's figure.
|
|
815
842
|
ctx.modelSwitched();
|
|
816
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}`);
|
|
817
851
|
}
|
|
818
852
|
}
|
|
819
853
|
}
|
package/dist/index.js
CHANGED
|
@@ -54,6 +54,7 @@ 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
56
|
import { preferences, usePreferences } from "./preferences.js";
|
|
57
|
+
import { sweepStaleMergeDirs } from "./temp-sweep.js";
|
|
57
58
|
import { settingsLayers } from "./state.js";
|
|
58
59
|
import { providerHost } from "./provider-label.js";
|
|
59
60
|
import { adapterOptionsFor } from "./auth/adapter-options.js";
|
|
@@ -68,6 +69,8 @@ import { armByteTrace } from "./byte-trace.js";
|
|
|
68
69
|
import { tmpdir, homedir } from "node:os";
|
|
69
70
|
import { clipboardImage } from "./clipboard.js";
|
|
70
71
|
import { cardsFromListings } from "./session-cards.js";
|
|
72
|
+
/** 0.40.7: the stale-merge-directory sweep runs once per process. */
|
|
73
|
+
let tempSwept = false;
|
|
71
74
|
// The moved exports stay reachable from this entry — the test imports
|
|
72
75
|
// (project-trust, coding-agent) never change (B4: zero assertion changes).
|
|
73
76
|
export { applyProjectMerges } from "./trust-ui.js";
|
|
@@ -782,11 +785,17 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
782
785
|
// channel and not an exported KISO_SESSIONS_DIR: a variable in
|
|
783
786
|
// process.env would reach every shell child, and a kiso started from a
|
|
784
787
|
// shell tool would then write into this project's folder.
|
|
785
|
-
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() });
|
|
786
789
|
setConfigModels(merged.models ?? {});
|
|
787
790
|
// CW-1 batch 2: the windows endpoints stated by refusing — read before the
|
|
788
791
|
// first window is asked for (the unknown-window notice below).
|
|
789
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
|
+
}
|
|
790
799
|
const resolved = resolveModel(modelFlag, merged);
|
|
791
800
|
// AFTER the model resolves: the window a PROFILE states is about that
|
|
792
801
|
// profile's model, so it cannot be read before we know which profile is
|
|
@@ -1292,7 +1301,7 @@ async function reloadAgent(old, id, input, announce = true) {
|
|
|
1292
1301
|
rmSync(p, { recursive: true, force: true });
|
|
1293
1302
|
}
|
|
1294
1303
|
catch {
|
|
1295
|
-
// best-effort —
|
|
1304
|
+
// best-effort — a survivor is removed by a later startup's sweep (temp-sweep.ts) once this process is gone
|
|
1296
1305
|
}
|
|
1297
1306
|
}
|
|
1298
1307
|
mergedTempPaths.push(...oldTemps);
|
|
@@ -1321,7 +1330,7 @@ async function reloadAgent(old, id, input, announce = true) {
|
|
|
1321
1330
|
rmSync(p, { recursive: true, force: true });
|
|
1322
1331
|
}
|
|
1323
1332
|
catch {
|
|
1324
|
-
// best-effort —
|
|
1333
|
+
// best-effort — a survivor is removed by a later startup's sweep (temp-sweep.ts) once this process is gone
|
|
1325
1334
|
}
|
|
1326
1335
|
}
|
|
1327
1336
|
if (announce)
|
|
@@ -2047,7 +2056,7 @@ async function main() {
|
|
|
2047
2056
|
if (cred.type === "api-key")
|
|
2048
2057
|
lines.push(` ${id.padEnd(10)} api-key ${maskSecret(cred.key)} saved ${new Date(cred.savedAt).toISOString().slice(0, 10)}`);
|
|
2049
2058
|
else
|
|
2050
|
-
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)" : ""}`);
|
|
2051
2060
|
}
|
|
2052
2061
|
const envs = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"].filter((k) => process.env[k] !== undefined);
|
|
2053
2062
|
lines.push(`env vars set: ${envs.length ? envs.join(", ") : "none"} (a stored credential owns its provider; env applies only when nothing is stored)`);
|
|
@@ -2198,7 +2207,7 @@ async function main() {
|
|
|
2198
2207
|
rmSync(p, { recursive: true, force: true });
|
|
2199
2208
|
}
|
|
2200
2209
|
catch {
|
|
2201
|
-
// best-effort —
|
|
2210
|
+
// best-effort — a survivor is removed by a later startup's sweep (temp-sweep.ts) once this process is gone
|
|
2202
2211
|
}
|
|
2203
2212
|
}
|
|
2204
2213
|
}
|
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,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",
|