@litfamily/litopencode 1.0.25 → 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 +14 -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/deliverable-hedge-guard.js +176 -10
- 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-humanizer/SKILL.md +1 -1
- package/skills/lit-humanizer/references/taxonomy.md +2 -2
- package/skills/lit-humanizer/rules.json +10 -7
- package/skills/lit-humanizer/scripts/core.mjs +13 -2
- package/skills/lit-pptx/SKILL.md +1 -1
- package/skills/managed-skill-manifest.json +3 -3
- 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
|
@@ -884,14 +884,15 @@
|
|
|
884
884
|
"id": "ko-negative-pledge",
|
|
885
885
|
"lang": "ko",
|
|
886
886
|
"severity": "block",
|
|
887
|
-
"pattern": "(?<![가-힣])(?:이|본|해당|위|아래)\\s+(?:자료|문서|보고서|슬라이드|덱|표|그림|그래프|차트|수치|결과|비교|분석|내용|예시)(?:은|는)\\s[^.!?\\n]{0,40}?(?:(?:해석하|의미하|주장하|대표하)(?:지\\s*(?:[*_]{1,2}\\s*)?(?:않(?:음|는다|습니다)|말\\s
|
|
887
|
+
"pattern": "(?<![가-힣])(?:이|본|해당|위|아래)\\s+(?:자료|문서|보고서|슬라이드|덱|표|그림|그래프|차트|수치|결과|비교|분석|내용|예시)(?:은|는)\\s[^.!?\\n]{0,40}?(?:(?:해석하|의미하|주장하|대표하)(?:지\\s*(?:[*_]{1,2}\\s*)?(?:않(?:음|는다|습니다)|말\\s*것|마세요|마십시오|말아\\s*주(?:세요|십시오))|(?:으)?면\\s*(?:[*_]{1,2}\\s*)?안\\s*(?:됨|된다|됩니다)(?![가-힣]))|(?:(?:비교|결론|벤치마크|순위|대표값|일반화)(?:이|가)|목적이|(?:위한|려는)\\s*것이)\\s*(?:[*_]{1,2}\\s*)?(?:아님|아니다|아닙니다)(?![가-힣])|순위표(?:이|가)\\s*(?:[*_]{1,2}\\s*)?(?:아님|아니다|아닙니다|아니고)(?![가-힣]))|(?:비교|결론|벤치마크|순위|대표값|일반화)(?:처럼|으로|로)\\s*(?:[*_]{1,2}\\s*)?(?:쓰|사용하|해석하|읽|간주하|인용하)(?:지\\s*(?:[*_]{1,2}\\s*)?(?:않(?:음|는다|습니다)|말\\s*것|마세요|마십시오|말아\\s*주(?:세요|십시오))|(?:으)?면\\s*(?:[*_]{1,2}\\s*)?안\\s*(?:됨|된다|됩니다)(?![가-힣]))|(?:공개|배포|대외|외부|홍보|발표|보고|비교|인용|공식)용\\s*(?:자료|문서|수치|표|그래프)(?:이|가)\\s*(?:[*_]{1,2}\\s*)?(?:아님|아니다|아닙니다)(?![가-힣])|(?<![가-힣])(?:숫자|수치)(?:들)?(?:은|는|이|가|을|를|끼리|도)(?![가-힣])(?:(?!면\\s)[^.!?\\n]){0,40}?(?<![와과]\\s)(?<![와과])비교할\\s*수\\s*(?:는\\s*)?없(?:다|습니다|음|나)(?![가-힣])|(?<![가-힣])(?:숫자|수치)(?:들)?끼리\\s*(?:서로\\s*|직접\\s*)?비교(?:해서|하여|해)\\s[^.!?\\n]{0,30}?[\"”’']\\s*(?:는|라는)\\s*결론[^.!?\\n]{0,8}?(?:수\\s*없(?:다|습니다)(?![가-힣])|(?:으)?면\\s*(?:[*_]{1,2}\\s*)?안\\s*(?:됨|된다|됩니다)(?![가-힣]))|(?:(?<![가-힣])(?:숫자|수치)(?:들)?|(?<![가-힣])(?:이|위|아래|본)\\s+(?:표|값|자료|그래프|차트))[^.!?\\n]{0,40}?(?:빠르|느리)다고\\s*판단하(?:지\\s*(?:[*_]{1,2}\\s*)?(?:말\\s*것|마세요|마십시오|말아\\s*주(?:세요|십시오))|(?:으)?면\\s*(?:[*_]{1,2}\\s*)?안\\s*(?:됨|된다|됩니다)(?![가-힣]))|(?:빠르|느리)다[\"”’']?\\s*(?:는|라는)\\s*식으로\\s*읽(?:지\\s*(?:[*_]{1,2}\\s*)?(?:말\\s*것|마세요|마십시오|말아\\s*주(?:세요|십시오))|(?:으)?면\\s*(?:[*_]{1,2}\\s*)?안\\s*(?:됨|된다|됩니다)(?![가-힣]))|만으로\\s[^.!?\\n]{0,40}?(?<![가-힣])순위를\\s*정하[^.!?\\n]{0,25}?(?:수\\s*없(?:다|습니다|음)(?![가-힣])|(?:해|하)서는\\s*안\\s*(?:됨|된다|됩니다)(?![가-힣]))|(?<![가-힣])순위[^.!?\\n]{0,20}?근거로\\s*(?:[*_]{1,2}\\s*)?사용할\\s*수\\s*(?:는\\s*)?없(?:다|습니다|음)(?![가-힣])|할\\s*수\\s*없는\\s*것\\s{0,3}:\\s{0,3}(?:[*_]{1,2}\\s{0,3})?[^.!?\\n]{0,30}?(?<![가-힣])순위를?\\s*매기거나|(?<![가-힣])표의\\s+(?:나열\\s+)?순서(?:는|가)\\s[^.!?\\n]{0,20}?(?<![가-힣])순위(?:(?:를|을)\\s*뜻하지\\s*(?:[*_]{1,2}\\s*)?않(?:음|는다|습니다)|(?:가|이)\\s*(?:[*_]{1,2}\\s*)?(?:아님|아니다|아닙니다)(?![가-힣]))",
|
|
888
888
|
"flags": "m",
|
|
889
889
|
"scope": "line",
|
|
890
890
|
"context": [
|
|
891
891
|
"fenced_code",
|
|
892
|
-
"quoted_user_text"
|
|
892
|
+
"quoted_user_text",
|
|
893
|
+
"blockquote_callout"
|
|
893
894
|
],
|
|
894
|
-
"rationale": "A sentence that tells the reader what the document, table or number is not for belongs in the reply or becomes a data fact.
|
|
895
|
+
"rationale": "A sentence that tells the reader what the document, table or number is not for belongs in the reply or becomes a data fact. Shapes: a self-reference topic (이/본/해당/위/아래 + document noun + 은/는) with a negated claim verb, a claim noun + 아님 (순위표 also + 아니고) or 위한/-려는 것이 아님, within one bullet; a comparison or ranking noun + (으)로/처럼 + a negated use or reading verb (순위처럼 읽지 말아 주세요); a purpose-용 data noun (공개용 수치) + 아님; 숫자/수치 + particle said to be incomparable (서로 비교할 수 없다) or compared among themselves toward a quoted verdict (숫자끼리 비교해 \"…빠르다\"는 결론을 내리면 안 됩니다); a speed judgment forbidden for these numbers or a quoted verdict forbidden as a reading (이 숫자로 … 느리다고 판단하면 안 됩니다); ranking refused for this data (…만으로 순위를 정할 수 없다, 순위의 근거로 사용할 수 없다, 할 수 없는 것: 순위를 매기거나); and a table's row order said not to be a ranking (표의 순서는 순위가 아니다). Use verbs after a self-reference, 근거/평가/증거/추천, inference limits (결론 내릴 수 없다, 근거가 되지 않는다), operating rules (…근거로 사용하지 마십시오), conditional method facts (…면 비교할 수 없다, 합계와 비교하면 안 됩니다), type-system rules (숫자 문자열을 그대로 비교하지 마십시오), reported speech and explainers (…면 안 된다는 반론, …면 안 되는 이유), order notices without a table anchor, and legal or financial notices (대체하, 보장하, 권유) stay outside the pattern because operating rules, method lines, manuals, discussions and standard notices use them.",
|
|
895
896
|
"fixture_ids": [
|
|
896
897
|
"ko-negative-pledge:positive",
|
|
897
898
|
"ko-negative-pledge:negative"
|
|
@@ -906,7 +907,8 @@
|
|
|
906
907
|
"scope": "document",
|
|
907
908
|
"context": [
|
|
908
909
|
"fenced_code",
|
|
909
|
-
"quoted_user_text"
|
|
910
|
+
"quoted_user_text",
|
|
911
|
+
"blockquote_callout"
|
|
910
912
|
],
|
|
911
913
|
"rationale": "Two or more sentences that end in 해석하지/주장하지/대표하지 않음 or a claim noun + 이/가 + 아님 turn a document into a list of disclaimers. 의미하, 간주하, 인용하 and the use verbs are left out because repeated definitions, data-processing rules, method lines and operating rules end that way; a single such sentence stays with the warn-only ko-status-negation.",
|
|
912
914
|
"fixture_ids": [
|
|
@@ -918,14 +920,15 @@
|
|
|
918
920
|
"id": "en-negative-pledge",
|
|
919
921
|
"lang": "en",
|
|
920
922
|
"severity": "block",
|
|
921
|
-
"pattern": "\\b(?:this|these)\\s+(?:deck|report|document|analysis|table|chart|figure|figures|numbers|results|slide|comparison)\\s+(?:(?:is|are)\\s+not\\s+(?:intended|meant|to\\s+be\\s+(?:read|used|taken|interpreted))(?:\\s+to\\s+be\\s+(?:read|used|taken|interpreted))?\\s+as\\b(?!\\s*(?:a\\s+substitute|a\\s+replacement|(?:legal|investment|medical|professional)\\s+advice))\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|(?:should|must)\\s+not\\s+be\\s+(?:read|used|taken|interpreted)\\s+as\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|makes?\\s+no\\s+claims?\\s+(?:about|that|regarding|as\\s+to)\\b)|\\b(?:this|these)\\s+(?:is|are)\\s+not\\s+(?:intended|meant)\\s+as\\b(?!\\s*(?:a\\s+substitute|a\\s+replacement|(?:legal|investment|medical|professional)\\s+advice))\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|\\bthe\\s+(?:deck|report|document|analysis)\\s+makes?\\s+no\\s+claims?\\s+(?:about|that|regarding|as\\s+to)\\b|(?:^|\\s[-*]\\s)\\s*(?:[-*]\\s+|\\d+\\.\\s+)?(?:[*_]{1,2})?not\\s+(?:intended|meant|to\\s+be\\s+read|to\\s+be\\s+used)\\s+as\\b(?!\\s*(?:a\\s+substitute|a\\s+replacement|(?:legal|investment|medical|professional)\\s+advice))\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|\\b(?:this|these)\\s+(?:table|chart|figure|figures|numbers|results|comparison|analysis)\\s+(?:does|do)\\s+not\\s+constitute\\s+(?:an?\\s+)?[^.!?]{0,60}?\\b(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)",
|
|
923
|
+
"pattern": "\\b(?:this|these)\\s+(?:deck|report|document|analysis|table|chart|figure|figures|numbers|results|slide|comparison)\\s+(?:(?:is|are)\\s+not\\s+(?:intended|meant|to\\s+be\\s+(?:read|used|taken|interpreted))(?:\\s+to\\s+be\\s+(?:read|used|taken|interpreted))?\\s+as\\b(?!\\s*(?:a\\s+substitute|a\\s+replacement|(?:legal|investment|medical|professional)\\s+advice))\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|(?:should|must)\\s+not\\s+be\\s+(?:read|used|taken|interpreted)\\s+as\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|makes?\\s+no\\s+claims?\\s+(?:about|that|regarding|as\\s+to)\\b)|\\b(?:this|these)\\s+(?:is|are)\\s+not\\s+(?:intended|meant)\\s+as\\b(?!\\s*(?:a\\s+substitute|a\\s+replacement|(?:legal|investment|medical|professional)\\s+advice))\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|\\bthe\\s+(?:deck|report|document|analysis)\\s+makes?\\s+no\\s+claims?\\s+(?:about|that|regarding|as\\s+to)\\b|(?:^|\\s[-*]\\s)\\s*(?:[-*]\\s+|\\d+\\.\\s+)?(?:[*_]{1,2})?not\\s+(?:intended|meant|to\\s+be\\s+read|to\\s+be\\s+used)\\s+as\\b(?!\\s*(?:a\\s+substitute|a\\s+replacement|(?:legal|investment|medical|professional)\\s+advice))\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|\\b(?:this|these)\\s+(?:table|chart|figure|figures|numbers|results|comparison|analysis)\\s+(?:does|do)\\s+not\\s+constitute\\s+(?:an?\\s+)?[^.!?]{0,60}?\\b(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|\\bthey\\s+are\\s+not\\s+(?:an?\\s+)?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)(?=\\s*(?:[.!?*_]|$))|\\b(?:do\\s+not|don[’']t)\\s+compare\\s+(?:these|those)\\s+(?:numbers|figures|results|values|scores|latencies)(?:\\s+(?:with|to)\\s+(?:each\\s+other|one\\s+another)\\b|(?=\\s*(?:[.!?*_]|$)))|\\b(?:these|those|the)\\s+(?:numbers|figures)\\s+are\\s+not\\s+comparable(?:\\s+(?:with|to)\\s+(?:each\\s+other|one\\s+another)\\b|(?=\\s*(?:[.!?*_]|$)))|\\b(?:this|the|these|our)\\s+(?:table|chart|figure|graph|list|report|document|deck|slide|page|summary|overview|analysis)s?\\s+(?:is|are)\\s+not\\s+(?:(?:meant|intended)\\s+)?(?:(?:to\\s+be\\s+)?(?:an?\\s+)?(?:ranking|benchmark|comparison|leaderboard|endorsement)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)(?=\\s*(?:[.!?*_]|$))|to\\s+(?:rank|compare|benchmark)\\b)|\\b(?:do\\s+not|don[’']t)\\s+read\\s+(?:this|the|these|our)\\s+(?:table|chart|figure|graph|list|report|document|deck|slide|page|summary|overview|analysis)s?\\s+as\\s+(?:an?\\s+)?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)",
|
|
922
924
|
"flags": "i",
|
|
923
925
|
"scope": "sentence",
|
|
924
926
|
"context": [
|
|
925
927
|
"fenced_code",
|
|
926
|
-
"quoted_user_text"
|
|
928
|
+
"quoted_user_text",
|
|
929
|
+
"blockquote_callout"
|
|
927
930
|
],
|
|
928
|
-
"rationale": "A sentence that says the deck, report or numbers are not intended, meant or to be read as a comparison, benchmark or ranking, or make no claim about something belongs in the reply, and so does a table, chart or set of numbers that does not constitute one. Requiring one of those nouns keeps safety labels and advice, offer or diagnosis notices clean; \"does not constitute\" for a report or document stays a notice form, and \"the\" is not a subject except with \"makes no claim\".",
|
|
931
|
+
"rationale": "A sentence that says the deck, report or numbers are not intended, meant or to be read as a comparison, benchmark or ranking, or make no claim about something belongs in the reply, and so does a table, chart or set of numbers that does not constitute one; \"they are not a ranking.\", \"do not compare these numbers (with each other).\" and \"the numbers are not comparable.\" are the same refusal when they close the sentence. Requiring one of those nouns keeps safety labels and advice, offer or diagnosis notices clean; a refusal that continues (a ranking of schools, comparable with the previous release, compare these values directly; …, this is not a benchmark, it is …) describes or instructs and stays clean; a deliverable subject (this table, the report, our deck) that is not a ranking, benchmark, comparison, leaderboard or endorsement, is not meant to rank or compare, or is not to be read as a ranking is the same refusal, while a bare \"this\" is not a subject; \"does not constitute\" for a report or document stays a notice form, and \"the\" is not a subject except with \"makes no claim\" or \"numbers are not comparable\".",
|
|
929
932
|
"fixture_ids": [
|
|
930
933
|
"en-negative-pledge:positive",
|
|
931
934
|
"en-negative-pledge:negative"
|
|
@@ -82,7 +82,12 @@ function sourceRows(text) {
|
|
|
82
82
|
if (/^\s{0,3}>\s?/.test(line)) {
|
|
83
83
|
captionPending = 0;
|
|
84
84
|
figureActive = false;
|
|
85
|
-
|
|
85
|
+
// A quote that opens with a bold label (> **주의:** ...) is the author's own callout, not quoted user
|
|
86
|
+
// text. Only rules that list blockquote_callout read it, with the quote marker blanked to spaces.
|
|
87
|
+
const callout = /^\s{0,3}>\s?(?:\*\*|__)[^*_\n]{1,24}(?:\*\*|__)/.test(line)
|
|
88
|
+
? inlineCodeAsSpaces(line.replace(/^\s{0,3}>\s?/, (prefix) => ' '.repeat(prefix.length)))
|
|
89
|
+
: '';
|
|
90
|
+
return { line, number: index + 1, candidate: '', callout, skipped: true, caption: false, captionAttribution: false };
|
|
86
91
|
}
|
|
87
92
|
const candidate = inlineCodeAsSpaces(line);
|
|
88
93
|
const caption = isCaption(line);
|
|
@@ -149,7 +154,8 @@ function splitSentences(span) {
|
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
function ruleSegments(rows, rule, cache) {
|
|
152
|
-
const
|
|
157
|
+
const readCallouts = rule.context.includes('blockquote_callout');
|
|
158
|
+
const cacheKey = `${rule.scope}/${rule.context.includes('table_figure_caption')}/${readCallouts}`;
|
|
153
159
|
if (cache.has(cacheKey)) return cache.get(cacheKey);
|
|
154
160
|
const skipCaptions = rule.context.includes('table_figure_caption');
|
|
155
161
|
const segments = [];
|
|
@@ -163,6 +169,11 @@ function ruleSegments(rows, rule, cache) {
|
|
|
163
169
|
|
|
164
170
|
for (const row of rows) {
|
|
165
171
|
const captionExempt = skipCaptions && (row.caption || row.captionAttribution);
|
|
172
|
+
if (readCallouts && row.callout) {
|
|
173
|
+
flush();
|
|
174
|
+
segments.push({ text: row.callout, line: row.number });
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
166
177
|
if (row.skipped || captionExempt || (rule.scope === 'sentence' && !row.candidate.trim())) {
|
|
167
178
|
flush();
|
|
168
179
|
continue;
|
package/skills/lit-pptx/SKILL.md
CHANGED
|
@@ -29,7 +29,7 @@ Read the brief, supplied sources, audience, delivery (presented in a room or sen
|
|
|
29
29
|
|
|
30
30
|
Under bare `lit`, if the request gives no source facts, choose a realistic concrete example and deliver a complete deck without asking. Label each invented name and figure as a sample or assumption: the frontmatter `notice:` line puts a sample-data tag on every slide, and the reply repeats it. Never leave bracketed placeholders or silently present examples as verified results. For an explicit non-lit request, a clarification question about the content is allowed; a question about the look is not needed, because the direction step answers it.
|
|
31
31
|
|
|
32
|
-
Under `lit`, create a `.pptx` by default. Do not ask a theme, colour or font question merely to begin: choose the tonality yourself (procedure step 2), build the whole deck, and name the choice and two alternatives in the reply. An explicit user choice of tonality, legacy template or learned template overrides the choice. Plain Markdown or HTML alone is appropriate only if the user asks for those formats. A request for a website interface belongs to `frontend-ui-ux`; a scientific data figure belongs to `lit-scientific-visualization`.
|
|
32
|
+
Under `lit`, create a `.pptx` by default. Do not ask a theme, colour or font question merely to begin: choose the tonality yourself (procedure step 2), build the whole deck, and name the choice and two alternatives in the reply. An explicit user choice of tonality, legacy template or learned template overrides the choice. Plain Markdown or HTML alone is appropriate only if the user asks for those formats. A requested Markdown or plain-text deliverable (`slides.md`, "slide text in Markdown") is read as it stands, so it is not the compiler source: give each slide a heading, its bullets and its speaker notes as ordinary Markdown, with no frontmatter, no `:::` directive blocks and no `layout:`, `title:` or `tonality:` keys, and do not compile a `.pptx` unless one was asked for. The spec dialect belongs only to a source this skill compiles, or to a source the user explicitly asked for. A request for a website interface belongs to `frontend-ui-ux`; a scientific data figure belongs to `lit-scientific-visualization`.
|
|
33
33
|
|
|
34
34
|
## #contract.mode_matrix
|
|
35
35
|
|
|
@@ -293,15 +293,15 @@
|
|
|
293
293
|
},
|
|
294
294
|
{
|
|
295
295
|
"path": "references/taxonomy.md",
|
|
296
|
-
"sha256": "
|
|
296
|
+
"sha256": "6836096bef306685749d8d0639026ff63e42f2e452a14b762f4d6241421080c8"
|
|
297
297
|
},
|
|
298
298
|
{
|
|
299
299
|
"path": "rules.json",
|
|
300
|
-
"sha256": "
|
|
300
|
+
"sha256": "d79e5619f673912e032bcd54fd24ea299b1b2e22643fb128cbe522ad020cc6eb"
|
|
301
301
|
},
|
|
302
302
|
{
|
|
303
303
|
"path": "scripts/core.mjs",
|
|
304
|
-
"sha256": "
|
|
304
|
+
"sha256": "abaff0390680e9c2602cde701f7224acd3586ebed0dcb2a807570359baac62fc"
|
|
305
305
|
},
|
|
306
306
|
{
|
|
307
307
|
"path": "scripts/detect.mjs",
|
|
@@ -5,7 +5,7 @@ import process from "node:process";
|
|
|
5
5
|
import { readStableRegularFile } from "../skills/frontend-ui-ux/scripts/stable-file-read.mjs";
|
|
6
6
|
|
|
7
7
|
// Shown by the GitHub pages only; the npm cards never reference them, so they stay out of the package.
|
|
8
|
-
const GITHUB_ONLY_README_MEDIA = /^docs\/assets\/readme\/(?:
|
|
8
|
+
const GITHUB_ONLY_README_MEDIA = /^docs\/assets\/readme\/(?:(?:install-output|doctor-output|ignition-toast|planner-permissions)-(?:dark|light)\.webp|status-card-(?:idle|working|recent)\.webp|auto-handoff-(?:settings|compact|doctor)-(?:dark|light)\.webp|promo(?:-[^/]+)?\.[^/]+|promo-source(?:\/.*)?)$/u;
|
|
9
9
|
// The landing README links these GitHub pages for what moved off it (guide, skills gallery, screens,
|
|
10
10
|
// artwork). They load images by paths relative to docs/, and they stay out of the package with the social preview.
|
|
11
11
|
const GITHUB_DOC_PAGES = ["docs/guide.md", "docs/guide-Ko-KR.md", "docs/skills.md", "docs/skills-Ko-KR.md", "docs/screens.md", "docs/screens-Ko-KR.md", "docs/brand.md", "docs/brand-Ko-KR.md"];
|
|
@@ -18,7 +18,7 @@ const FORBIDDEN_PATH_RULES = [
|
|
|
18
18
|
{ label: "local asset generator", pattern: /^generate_cover\.py$/u },
|
|
19
19
|
{ label: "repository cover PNG", pattern: /^cover\.png$/u },
|
|
20
20
|
{ label: "repository cover vector", pattern: /^docs\/assets\/cover\.svg$/u },
|
|
21
|
-
{ label: "GitHub-only README media (
|
|
21
|
+
{ label: "GitHub-only README media (on-screen pictures, status card, automatic handoff, promo film)", pattern: GITHUB_ONLY_README_MEDIA },
|
|
22
22
|
{ label: "unapproved README presentation asset", pattern: /^docs\/assets\/readme\/(?!(?:ascii-readme|badge-version|badge-license|lucide-book-open|lucide-play|lucide-shield-check)\.svg$|litopencode-wordmark\.svg$|litopencode-clay-icon\.png$|(?:litfamily-machines|poster)\.png$|ignition-film\.mp4$|ignition-readme\.gif$|(?:Lucide-LICENSE|JetBrainsMono-OFL)\.txt$)/u },
|
|
23
23
|
{ label: "maintainer release checklist", pattern: /^docs\/release-checklist\.md$/u },
|
|
24
24
|
{ label: "GitHub-only docs page, social preview or demo film", pattern: GITHUB_ONLY_DOCS },
|
package/dist/jev-skill-hint.d.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
export declare const jevEndpoint = "https://api.typesafe.ai/v1/systemone";
|
|
2
|
-
export declare const jevFlag = "LITOPENCODE_JEV";
|
|
3
|
-
/** Shown once per session so a user who switched the hint on notices it; never carries the key. */
|
|
4
|
-
export declare const jevSkillHintAwarenessText = "\u2726 Jev skill hint is ON";
|
|
5
|
-
export type JevFetch = (url: string, init: RequestInit) => Promise<Response>;
|
|
6
|
-
export type JevCatalogEntry = {
|
|
7
|
-
readonly id: string;
|
|
8
|
-
readonly description: string;
|
|
9
|
-
};
|
|
10
|
-
export type JevSkillHintStatus = "off" | "on" | "key-missing";
|
|
11
|
-
export type JevSkillHintPart = {
|
|
12
|
-
readonly kind: "hint";
|
|
13
|
-
readonly text: string;
|
|
14
|
-
readonly skillId: string;
|
|
15
|
-
readonly latencyMs: number;
|
|
16
|
-
} | {
|
|
17
|
-
readonly kind: "note";
|
|
18
|
-
readonly text: string;
|
|
19
|
-
};
|
|
20
|
-
export type JevSkillHintOptions = {
|
|
21
|
-
readonly env?: NodeJS.ProcessEnv;
|
|
22
|
-
readonly fetch?: JevFetch;
|
|
23
|
-
readonly catalog?: readonly JevCatalogEntry[];
|
|
24
|
-
readonly traceFile?: string;
|
|
25
|
-
/** Project root: every folder between it and the trace file must be a real folder, never a symlink. */
|
|
26
|
-
readonly traceRoot?: string;
|
|
27
|
-
readonly now?: () => number;
|
|
28
|
-
};
|
|
29
|
-
export declare function jevSkillHintStatus(env?: NodeJS.ProcessEnv): JevSkillHintStatus;
|
|
30
|
-
export declare function jevSkillHintDoctorLine(env?: NodeJS.ProcessEnv): string;
|
|
31
|
-
export declare function jevSkillHintText(skillId: string): string;
|
|
32
|
-
/** The visible toast names only the validated skill id and the request latency. */
|
|
33
|
-
export declare function jevSkillHintToastText(skillId: string, latencyMs: number): string;
|
|
34
|
-
export declare function jevSkillHintUnavailableText(reason: string): string;
|
|
35
|
-
/** Eligibility that depends on the prompt text alone; routing and command checks live in the hook. */
|
|
36
|
-
export declare function isJevEligiblePrompt(text: string): boolean;
|
|
37
|
-
/**
|
|
38
|
-
* Redact a bounded window first, then truncate, so a secret that straddles the 2,000-character cut is
|
|
39
|
-
* masked whole. When either cut happened, a token run left dangling at the end is masked as well.
|
|
40
|
-
*/
|
|
41
|
-
export declare function redactJevPrompt(text: string): string;
|
|
42
|
-
export declare function defaultJevCatalog(): readonly JevCatalogEntry[];
|
|
43
|
-
export declare function buildJevRequestBody(state: string, catalog: readonly JevCatalogEntry[], model: string): string;
|
|
44
|
-
export type JevSkillHint = {
|
|
45
|
-
hintFor(sessionID: string, promptText: string): Promise<JevSkillHintPart | undefined>;
|
|
46
|
-
/** True exactly once per session: on its first eligible turn while both switches are on. */
|
|
47
|
-
claimAwareness(sessionID: string, promptText: string): boolean;
|
|
48
|
-
forget(sessionID: string): void;
|
|
49
|
-
};
|
|
50
|
-
export declare function createJevSkillHint(options?: JevSkillHintOptions): JevSkillHint;
|
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
|
-
}
|