@litfamily/litopencode 1.0.26 → 1.0.27
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/CHANGELOG.md +10 -0
- package/README-Ko-KR.md +6 -6
- package/README.md +6 -6
- package/dist/activation-routing.js +11 -1
- package/dist/activation.d.ts +0 -10
- package/dist/activation.js +0 -54
- package/dist/agents/defaults.js +2 -1
- package/dist/cli/auto-update.d.ts +27 -1
- package/dist/cli/auto-update.js +112 -13
- package/dist/cli/doctor.d.ts +9 -0
- package/dist/cli/doctor.js +71 -10
- package/dist/cli/managed-skill-assets.js +2 -2
- package/dist/server.js +1 -17
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/privacy.md +1 -12
- package/docs/reference-Ko-KR.md +6 -44
- package/docs/reference.md +6 -48
- package/package.json +1 -1
- package/skills/lit-docx/SKILL.md +1 -1
- package/skills/lit-pptx/SKILL.md +1 -1
- package/tools/check-pack-payload.mjs +2 -2
- package/dist/jev-skill-hint.d.ts +0 -50
- package/dist/jev-skill-hint.js +0 -318
package/dist/jev-skill-hint.js
DELETED
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { constants as fsConstants } from "node:fs";
|
|
3
|
-
import fs from "node:fs/promises";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { litOpenCodeRuntimeSkills } from "./skills.js";
|
|
6
|
-
// Optional Jev skill hint for the chat.message hook. It is off unless LITOPENCODE_JEV=1 and
|
|
7
|
-
// TYPESAFE_API_KEY are both set in the process environment. The key is read from the environment
|
|
8
|
-
// only and goes nowhere except the Authorization header: not into errors, notes, traces or doctor.
|
|
9
|
-
// The response is untrusted data; the only text that reaches the turn is this module's own fixed
|
|
10
|
-
// sentence carrying a skill id that was checked against the catalog this module sent.
|
|
11
|
-
export const jevEndpoint = "https://api.typesafe.ai/v1/systemone";
|
|
12
|
-
export const jevFlag = "LITOPENCODE_JEV";
|
|
13
|
-
/** Shown once per session so a user who switched the hint on notices it; never carries the key. */
|
|
14
|
-
export const jevSkillHintAwarenessText = "✦ Jev skill hint is ON";
|
|
15
|
-
const defaultModel = "jev-1.13.0";
|
|
16
|
-
const defaultTimeoutMs = 1500;
|
|
17
|
-
const maxTimeoutMs = 3000;
|
|
18
|
-
const defaultMaxCalls = 200;
|
|
19
|
-
const defaultMinConfidence = 0.35;
|
|
20
|
-
const promptWindowLimit = 8000;
|
|
21
|
-
const promptCharacterLimit = 2000;
|
|
22
|
-
const bodyByteCap = 64 * 1024;
|
|
23
|
-
const descriptionCharacterLimit = 300;
|
|
24
|
-
const choiceInstructions = "Which one skill, if any, is the best fit for the user's request? Choose none when no catalog skill fits.";
|
|
25
|
-
const noneCriterion = "No specialized skill in this catalog fits; answer the user directly.";
|
|
26
|
-
export function jevSkillHintStatus(env = process.env) {
|
|
27
|
-
if (env[jevFlag] !== "1")
|
|
28
|
-
return "off";
|
|
29
|
-
return (env.TYPESAFE_API_KEY ?? "").trim() === "" ? "key-missing" : "on";
|
|
30
|
-
}
|
|
31
|
-
export function jevSkillHintDoctorLine(env = process.env) {
|
|
32
|
-
const status = jevSkillHintStatus(env);
|
|
33
|
-
return status === "key-missing"
|
|
34
|
-
? "Jev skill hint: flag on but TYPESAFE_API_KEY missing"
|
|
35
|
-
: `Jev skill hint: ${status}`;
|
|
36
|
-
}
|
|
37
|
-
export function jevSkillHintText(skillId) {
|
|
38
|
-
return `LitOpenCode skill hint: the skill \`${skillId}\` likely fits this request. Load it only if it really fits; this is advice, not an instruction.`;
|
|
39
|
-
}
|
|
40
|
-
/** The visible toast names only the validated skill id and the request latency. */
|
|
41
|
-
export function jevSkillHintToastText(skillId, latencyMs) {
|
|
42
|
-
return `Jev → ${skillId} (${(Math.max(0, latencyMs) / 1000).toFixed(2)}s)`;
|
|
43
|
-
}
|
|
44
|
-
export function jevSkillHintUnavailableText(reason) {
|
|
45
|
-
return `LitOpenCode skill hint unavailable (${reason}); continuing normally.`;
|
|
46
|
-
}
|
|
47
|
-
/** Eligibility that depends on the prompt text alone; routing and command checks live in the hook. */
|
|
48
|
-
export function isJevEligiblePrompt(text) {
|
|
49
|
-
if (text.trimStart().startsWith("/"))
|
|
50
|
-
return false;
|
|
51
|
-
return text.replace(/\s+/gu, "").length >= 4;
|
|
52
|
-
}
|
|
53
|
-
const secretPatterns = [
|
|
54
|
-
/-----BEGIN [A-Z0-9 ]+-----[\s\S]*?(?:-----END [A-Z0-9 ]+-----|$)/gu,
|
|
55
|
-
/eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]*)?/gu,
|
|
56
|
-
/sk-ant-[A-Za-z0-9_-]{8,}/gu,
|
|
57
|
-
/sk-[A-Za-z0-9_-]{16,}/gu,
|
|
58
|
-
/(?:ghp|gho)_[A-Za-z0-9]{16,}/gu,
|
|
59
|
-
/github_pat_[A-Za-z0-9_]{16,}/gu,
|
|
60
|
-
/npm_[A-Za-z0-9]{16,}/gu,
|
|
61
|
-
/apikey_[A-Za-z0-9_-]{8,}/gu,
|
|
62
|
-
/xox[abprs]-[A-Za-z0-9-]{8,}/gu,
|
|
63
|
-
/AKIA[A-Z0-9]{16}/gu,
|
|
64
|
-
/AIza[0-9A-Za-z_-]{20,}/gu,
|
|
65
|
-
/\b[A-Za-z0-9_]*(?:password|passwd|token|secret)\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"']+)/giu,
|
|
66
|
-
/[A-Za-z0-9+/_-]{32,}={0,2}/gu
|
|
67
|
-
];
|
|
68
|
-
const tokenRunAtCut = /[A-Za-z0-9+/_-]{8,}={0,2}$/u;
|
|
69
|
-
/**
|
|
70
|
-
* Redact a bounded window first, then truncate, so a secret that straddles the 2,000-character cut is
|
|
71
|
-
* masked whole. When either cut happened, a token run left dangling at the end is masked as well.
|
|
72
|
-
*/
|
|
73
|
-
export function redactJevPrompt(text) {
|
|
74
|
-
const head = text.slice(0, promptWindowLimit * 2);
|
|
75
|
-
let redacted = Array.from(head).slice(0, promptWindowLimit).join("");
|
|
76
|
-
const windowCut = redacted.length < text.length;
|
|
77
|
-
redacted = redacted
|
|
78
|
-
.replace(/\/(?:Users|home)\/[^/\s]+/gu, "~")
|
|
79
|
-
.replace(/[A-Za-z]:\\Users\\[^\\/\s]+(\\)?/giu, (_match, separator) => (separator === undefined ? "~" : "~/"));
|
|
80
|
-
// The lookbehind starts a match only at the head of a run, which keeps the 8,000-character window linear.
|
|
81
|
-
redacted = redacted.replace(/(?<![A-Za-z0-9._%+-])[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}/gu, "[email]");
|
|
82
|
-
for (const pattern of secretPatterns)
|
|
83
|
-
redacted = redacted.replace(pattern, "[secret]");
|
|
84
|
-
const characters = Array.from(redacted);
|
|
85
|
-
if (!windowCut && characters.length <= promptCharacterLimit)
|
|
86
|
-
return redacted;
|
|
87
|
-
return characters.slice(0, promptCharacterLimit).join("").replace(tokenRunAtCut, "[secret]");
|
|
88
|
-
}
|
|
89
|
-
export function defaultJevCatalog() {
|
|
90
|
-
return litOpenCodeRuntimeSkills.map((skill) => ({ id: skill.id, description: skill.summary }));
|
|
91
|
-
}
|
|
92
|
-
export function buildJevRequestBody(state, catalog, model) {
|
|
93
|
-
const criteria = {};
|
|
94
|
-
for (const entry of catalog)
|
|
95
|
-
criteria[entry.id] = Array.from(entry.description).slice(0, descriptionCharacterLimit).join("");
|
|
96
|
-
criteria.none = noneCriterion;
|
|
97
|
-
return JSON.stringify({
|
|
98
|
-
model,
|
|
99
|
-
state,
|
|
100
|
-
questions: { which: { type: "choice", instructions: choiceInstructions, criteria } }
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
function boundedInteger(raw, fallback, min, max) {
|
|
104
|
-
if (raw === undefined || !/^\d+$/u.test(raw.trim()))
|
|
105
|
-
return fallback;
|
|
106
|
-
return Math.min(max, Math.max(min, Number(raw.trim())));
|
|
107
|
-
}
|
|
108
|
-
function minConfidence(raw) {
|
|
109
|
-
if (raw === undefined || raw.trim() === "")
|
|
110
|
-
return defaultMinConfidence;
|
|
111
|
-
const value = Number(raw);
|
|
112
|
-
return Number.isFinite(value) && value >= 0 && value <= 1 ? value : defaultMinConfidence;
|
|
113
|
-
}
|
|
114
|
-
async function readCappedBody(response) {
|
|
115
|
-
const declared = Number(response.headers.get("content-length") ?? "");
|
|
116
|
-
if (Number.isFinite(declared) && declared > bodyByteCap) {
|
|
117
|
-
await response.body?.cancel().catch(() => undefined);
|
|
118
|
-
return undefined;
|
|
119
|
-
}
|
|
120
|
-
if (response.body === null)
|
|
121
|
-
return "";
|
|
122
|
-
const reader = response.body.getReader();
|
|
123
|
-
const chunks = [];
|
|
124
|
-
let total = 0;
|
|
125
|
-
for (;;) {
|
|
126
|
-
const { done, value } = await reader.read();
|
|
127
|
-
if (done)
|
|
128
|
-
break;
|
|
129
|
-
total += value.byteLength;
|
|
130
|
-
if (total > bodyByteCap) {
|
|
131
|
-
await reader.cancel().catch(() => undefined);
|
|
132
|
-
return undefined;
|
|
133
|
-
}
|
|
134
|
-
chunks.push(value);
|
|
135
|
-
}
|
|
136
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
137
|
-
}
|
|
138
|
-
function validatedAnswer(raw, allowed, threshold) {
|
|
139
|
-
let parsed;
|
|
140
|
-
try {
|
|
141
|
-
parsed = JSON.parse(raw);
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
return { failure: "invalid response" };
|
|
145
|
-
}
|
|
146
|
-
const which = parsed?.answers?.which;
|
|
147
|
-
if (typeof which !== "object" || which === null)
|
|
148
|
-
return { failure: "invalid response" };
|
|
149
|
-
const { choice, confidence } = which;
|
|
150
|
-
if (typeof choice !== "string" || !allowed.has(choice))
|
|
151
|
-
return { failure: "invalid response" };
|
|
152
|
-
if (typeof confidence !== "number" || !Number.isFinite(confidence))
|
|
153
|
-
return { failure: "invalid response" };
|
|
154
|
-
if (choice === "none" || confidence < threshold)
|
|
155
|
-
return { confidence };
|
|
156
|
-
return { choice, confidence };
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Walks the trace folder below the project root one level at a time, creating a missing folder
|
|
160
|
-
* without following links. Any symlink or non-folder on the way means no trace.
|
|
161
|
-
*/
|
|
162
|
-
async function realTraceFolder(root, folder) {
|
|
163
|
-
const relative = path.relative(root, folder);
|
|
164
|
-
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
165
|
-
return false;
|
|
166
|
-
let current = root;
|
|
167
|
-
for (const segment of relative.split(path.sep).filter((part) => part !== "")) {
|
|
168
|
-
current = path.join(current, segment);
|
|
169
|
-
let stat = await fs.lstat(current).catch(() => undefined);
|
|
170
|
-
if (stat === undefined) {
|
|
171
|
-
await fs.mkdir(current).catch(() => undefined);
|
|
172
|
-
stat = await fs.lstat(current).catch(() => undefined);
|
|
173
|
-
}
|
|
174
|
-
if (stat === undefined || stat.isSymbolicLink() || !stat.isDirectory())
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
return true;
|
|
178
|
-
}
|
|
179
|
-
export function createJevSkillHint(options = {}) {
|
|
180
|
-
const env = options.env ?? process.env;
|
|
181
|
-
const fetchImpl = options.fetch ?? ((url, init) => fetch(url, init));
|
|
182
|
-
const now = options.now ?? Date.now;
|
|
183
|
-
const sessions = new Map();
|
|
184
|
-
async function trace(record) {
|
|
185
|
-
if (env[`${jevFlag}_TRACE`] !== "1" || options.traceFile === undefined)
|
|
186
|
-
return;
|
|
187
|
-
const folder = path.dirname(options.traceFile);
|
|
188
|
-
const root = options.traceRoot ?? path.dirname(folder);
|
|
189
|
-
try {
|
|
190
|
-
if (!(await realTraceFolder(root, folder)))
|
|
191
|
-
return;
|
|
192
|
-
// Refuse a symlink or other non-file at the trace path; O_NOFOLLOW closes the lstat race.
|
|
193
|
-
const existing = await fs.lstat(options.traceFile).catch(() => undefined);
|
|
194
|
-
if (existing !== undefined && !existing.isFile())
|
|
195
|
-
return;
|
|
196
|
-
const flags = fsConstants.O_WRONLY | fsConstants.O_APPEND | fsConstants.O_CREAT | (fsConstants.O_NOFOLLOW ?? 0);
|
|
197
|
-
const handle = await fs.open(options.traceFile, flags, 0o600);
|
|
198
|
-
try {
|
|
199
|
-
// A folder swapped for a symlink after the first check still gets no record.
|
|
200
|
-
if (!(await realTraceFolder(root, folder)))
|
|
201
|
-
return;
|
|
202
|
-
await handle.appendFile(`${JSON.stringify(record)}\n`, "utf8");
|
|
203
|
-
}
|
|
204
|
-
finally {
|
|
205
|
-
await handle.close();
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
catch { // no-excuse-ok: catch -- the opt-in debug trace must never affect the turn.
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
async function ask(state, key) {
|
|
212
|
-
const catalog = options.catalog ?? defaultJevCatalog();
|
|
213
|
-
const model = (env[`${jevFlag}_MODEL`] ?? "").trim() || defaultModel;
|
|
214
|
-
const body = buildJevRequestBody(state, catalog, model);
|
|
215
|
-
if (Buffer.byteLength(body, "utf8") > bodyByteCap)
|
|
216
|
-
return { failure: "request too large" };
|
|
217
|
-
const timeoutMs = boundedInteger(env[`${jevFlag}_TIMEOUT_MS`], defaultTimeoutMs, 1, maxTimeoutMs);
|
|
218
|
-
const controller = new AbortController();
|
|
219
|
-
let timer;
|
|
220
|
-
const timedOut = new Promise((resolve) => {
|
|
221
|
-
timer = setTimeout(() => {
|
|
222
|
-
controller.abort();
|
|
223
|
-
resolve({ failure: "timeout" });
|
|
224
|
-
}, timeoutMs);
|
|
225
|
-
});
|
|
226
|
-
const request = (async () => {
|
|
227
|
-
let response;
|
|
228
|
-
try {
|
|
229
|
-
response = await fetchImpl(jevEndpoint, {
|
|
230
|
-
method: "POST",
|
|
231
|
-
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
|
|
232
|
-
body,
|
|
233
|
-
redirect: "error",
|
|
234
|
-
signal: controller.signal
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
catch {
|
|
238
|
-
return { failure: controller.signal.aborted ? "timeout" : "network" };
|
|
239
|
-
}
|
|
240
|
-
if (response.status !== 200) {
|
|
241
|
-
await response.body?.cancel().catch(() => undefined);
|
|
242
|
-
return { httpStatus: response.status, failure: `HTTP ${response.status}` };
|
|
243
|
-
}
|
|
244
|
-
let raw;
|
|
245
|
-
try {
|
|
246
|
-
raw = await readCappedBody(response);
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
return { httpStatus: 200, failure: controller.signal.aborted ? "timeout" : "network" };
|
|
250
|
-
}
|
|
251
|
-
if (raw === undefined)
|
|
252
|
-
return { httpStatus: 200, failure: "response too large" };
|
|
253
|
-
const allowed = new Set([...catalog.map((entry) => entry.id), "none"]);
|
|
254
|
-
return { httpStatus: 200, ...validatedAnswer(raw, allowed, minConfidence(env[`${jevFlag}_MIN_CONFIDENCE`])) };
|
|
255
|
-
})();
|
|
256
|
-
try {
|
|
257
|
-
return await Promise.race([request, timedOut]);
|
|
258
|
-
}
|
|
259
|
-
finally {
|
|
260
|
-
clearTimeout(timer);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
return {
|
|
264
|
-
forget(sessionID) {
|
|
265
|
-
sessions.delete(sessionID);
|
|
266
|
-
},
|
|
267
|
-
claimAwareness(sessionID, promptText) {
|
|
268
|
-
if (jevSkillHintStatus(env) !== "on" || !isJevEligiblePrompt(promptText))
|
|
269
|
-
return false;
|
|
270
|
-
const session = sessions.get(sessionID) ?? { calls: 0, noted: false, announced: false };
|
|
271
|
-
sessions.set(sessionID, session);
|
|
272
|
-
if (session.announced)
|
|
273
|
-
return false;
|
|
274
|
-
session.announced = true;
|
|
275
|
-
return true;
|
|
276
|
-
},
|
|
277
|
-
async hintFor(sessionID, promptText) {
|
|
278
|
-
if (jevSkillHintStatus(env) !== "on" || !isJevEligiblePrompt(promptText))
|
|
279
|
-
return undefined;
|
|
280
|
-
const key = (env.TYPESAFE_API_KEY ?? "").trim();
|
|
281
|
-
const session = sessions.get(sessionID) ?? { calls: 0, noted: false, announced: false };
|
|
282
|
-
sessions.set(sessionID, session);
|
|
283
|
-
const started = now();
|
|
284
|
-
// A key pasted into the prompt is masked even when it is too short for a token shape.
|
|
285
|
-
const state = redactJevPrompt(promptText.split(key).join("[secret]"));
|
|
286
|
-
let outcome;
|
|
287
|
-
if (session.calls >= boundedInteger(env[`${jevFlag}_MAX_CALLS`], defaultMaxCalls, 0, Number.MAX_SAFE_INTEGER)) {
|
|
288
|
-
outcome = { failure: "call cap reached" };
|
|
289
|
-
}
|
|
290
|
-
else {
|
|
291
|
-
session.calls += 1;
|
|
292
|
-
try {
|
|
293
|
-
outcome = await ask(state, key);
|
|
294
|
-
}
|
|
295
|
-
catch {
|
|
296
|
-
outcome = { failure: "network" };
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
const latencyMs = now() - started;
|
|
300
|
-
await trace({
|
|
301
|
-
time: new Date(started).toISOString(),
|
|
302
|
-
promptSha256: createHash("sha256").update(state).digest("hex"),
|
|
303
|
-
choice: outcome.choice ?? null,
|
|
304
|
-
confidence: outcome.confidence ?? null,
|
|
305
|
-
latencyMs,
|
|
306
|
-
httpStatus: outcome.httpStatus ?? null,
|
|
307
|
-
fallback: outcome.failure ?? null
|
|
308
|
-
});
|
|
309
|
-
if (outcome.choice !== undefined) {
|
|
310
|
-
return { kind: "hint", text: jevSkillHintText(outcome.choice), skillId: outcome.choice, latencyMs };
|
|
311
|
-
}
|
|
312
|
-
if (outcome.failure === undefined || session.noted)
|
|
313
|
-
return undefined;
|
|
314
|
-
session.noted = true;
|
|
315
|
-
return { kind: "note", text: jevSkillHintUnavailableText(outcome.failure) };
|
|
316
|
-
}
|
|
317
|
-
};
|
|
318
|
-
}
|