SLOPSHOPPER

plus

All claudecode-plus-mod Mods in one install: prompt-shield, cache-guard, human-tone, clear-intent and agent-charter.

newprompt
v0.6.0no licenseupdated 2026-10-07boxiaolanya2008/claudecode-plus-mod/bundle
A shopper browsing a rack in a slop shop
README

plus

鑱氬悎鍖咃細涓€娆¤鍏ㄦ湰浠撳簱鎵€鏈?Mod銆傜敓鎴愭枃浠讹紝涓嶈鎵嬫敼銆?

claude plugin install plus@claudecode-plus-mod --scope user

閲嶆柊鐢熸垚锛堜粨搴撴牴鐩綍锛夛細

./scripts/build-bundle.ps1 -Version 0.1.0
Source 11 files
hooks/register.ts 35 lines
1import type { On } from 'claude-code'
2
3import { register as agentCharter } from './agent-charter/register'
4import { register as cacheGuard } from './cache-guard/register'
5import { register as clearIntent } from './clear-intent/register'
6import { register as humanTone } from './human-tone/register'
7import { register as promptShield } from './prompt-shield/register'
8
9type PlusOptions = {
10  verbose?: boolean
11  forbidAutoCompact?: boolean
12  dropAttachmentTypes?: string[]
13  mode?: string
14  extra?: string
15  confirmFirst?: boolean
16  charterMode?: string
17}
18
19/**
20 * Registers every Mod in the bundle through one entry: each Mod reads only
21 * the option keys it owns, so one options object serves all of them.
22 *
23 * @param on the engine's registrar
24 * @param options shared options; `verbose` for all Mods, `forbidAutoCompact`
25 * and `dropAttachmentTypes` for cache-guard, `mode` and `extra` for
26 * human-tone, `confirmFirst` for clear-intent, `charterMode` for
27 * agent-charter
28 */
29export function register(on: On, options: PlusOptions = {}): void {
30  promptShield(on, options)
31  cacheGuard(on, options)
32  humanTone(on, options)
33  clearIntent(on, options)
34  agentCharter(on, options)
35}
hooks/agent-charter/register.ts 43 lines
1import type { On } from 'claude-code'
2
3import { CHARTER_ID, applyCharter, buildCharter, resolveCharterMode } from './charter'
4
5type CharterOptions = {
6  charterMode?: string
7  verbose?: boolean
8}
9
10function report($: unknown, message: string, verbose: boolean): void {
11  const ui = ($ as { ui: { log: (text: string, opts?: object) => void } }).ui
12  if (verbose) {
13    ui.log(message)
14  } else {
15    ui.log(message, { to: 'debug' })
16  }
17}
18
19/**
20 * Pins the agent charter into the system prompt. Append mode (default) keeps
21 * the engine's composition, tools and safety rails included, and adds the
22 * charter last among shared sections. Replace mode drops everything else:
23 * use it only when you accept a dumber, unguarded agent.
24 *
25 * @param on the engine's registrar
26 * @param options `charterMode` append or replace; `verbose` surfaces lines
27 */
28export function register(on: On, options: CharterOptions = {}): void {
29  const mode = resolveCharterMode(options.charterMode)
30  const verbose = options.verbose === true
31  const mine = { id: CHARTER_ID, text: buildCharter(), scope: 'shared' as const }
32
33  on('session.start', ($, e, next) => {
34    report($, `[agent-charter] active in ${mode} mode`, verbose)
35    return next(e)
36  })
37
38  on('prompt.compose', async (_$, e, next) => {
39    const out = await next(e)
40    return { sections: applyCharter(out.sections, mine, mode) }
41  })
42}
43
hooks/cache-guard/register.ts 202 lines
1import type { On } from 'claude-code'
2
3import {
4  auditCompact,
5  auditCustody,
6  auditKey,
7  cacheLine,
8  evidenceKey,
9  fingerprintMessages,
10  isColdMiss,
11  previewMessage,
12  pruneKeys,
13  stabilize,
14} from './fingerprint'
15
16type GuardOptions = {
17  verbose?: boolean
18  forbidAutoCompact?: boolean
19  dropAttachmentTypes?: string[]
20}
21
22function report($: unknown, message: string, verbose: boolean): void {
23  const ui = ($ as { ui: { log: (text: string, opts?: object) => void } }).ui
24  if (verbose) {
25    ui.log(message)
26  } else {
27    ui.log(message, { to: 'debug' })
28  }
29}
30
31/**
32 * Registers cache-prefix stabilization. The provider matches on exact prefix
33 * and the engine places every cache mark itself, so nothing here forces a
34 * hit. What it does is erase the misses we cause ourselves: volatile-only
35 * drift in recomposed sections and context blocks is pinned back to the
36 * last settled text, real content changes pass through with a warning, and
37 * per-turn numbers prove the delta.
38 *
39 * @param on the engine's registrar
40 * @param options `verbose` surfaces each line in the transcript;
41 * `forbidAutoCompact` vetoes threshold-triggered compaction;
42 * `dropAttachmentTypes` leaves named attachment types out of the request
43 */
44export function register(on: On, options: GuardOptions = {}): void {
45  const verbose = options.verbose === true
46  const settledSections = new Map<string, string>()
47  const settledBlocks = new Map<string, string>()
48  const dropped = new Set(options.dropAttachmentTypes ?? [])
49  let lastChain: string[] | undefined
50  let lastPreview: string[] = []
51
52  on('session.start', ($, e, next) => {
53    report($, '[cache-guard] active, watching session.measure, prompt.section, prompt.context, turn.complete, session.compact', verbose)
54    return next(e)
55  })
56
57  on('session.measure', async ($, e, next) => {
58    const result = await next(e)
59    try {
60      const usage = await $.session.usage({ breakdown: 'summary' })
61      const api = usage.breakdown?.apiUsage
62      if (api !== undefined && api !== null) {
63        report($, `[cache-guard] ${cacheLine(api)}`, verbose)
64        if (isColdMiss(api)) {
65          report($, '[cache-guard] cold miss: nothing served from cache this turn, the request prefix changed', verbose)
66        }
67      }
68    } catch {
69      report($, '[cache-guard] usage unavailable, skipping cache report', verbose)
70    }
71    return result
72  })
73
74  on('prompt.section', async ($, e, next) => {
75    const out = await next(e)
76    if (out.text === null) {
77      return out
78    }
79    const stable = stabilize(settledSections.get(e.name), out.text)
80    settledSections.set(e.name, stable.text)
81    if (stable.pinned) {
82      report($, `[cache-guard] section "${e.name}" pinned to last settled text, volatile-only drift erased`, verbose)
83    } else if (stable.drifted) {
84      report($, `[cache-guard] section "${e.name}" content changed, next request prefix will miss cache`, verbose)
85    }
86    return { text: stable.text }
87  })
88
89  on('prompt.context', async ($, e, next) => {
90    const out = await next(e)
91    const blocks = out.blocks.map((block) => {
92      const stable = stabilize(settledBlocks.get(block.name), block.text)
93      settledBlocks.set(block.name, stable.text)
94      if (stable.pinned) {
95        report($, `[cache-guard] context block "${block.name}" pinned to last settled text`, verbose)
96      } else if (stable.drifted) {
97        report($, `[cache-guard] context block "${block.name}" content changed`, verbose)
98      }
99      return { ...block, text: stable.text }
100    })
101    return { ...out, blocks }
102  })
103
104  on('prompt.attachment', async ($, e, next) => {
105    if (dropped.has(e.type)) {
106      report($, `[cache-guard] attachment "${e.type}" dropped by dropAttachmentTypes`, verbose)
107      return { text: null }
108    }
109    return next(e)
110  })
111
112  on('turn.complete', async ($, e, next) => {
113    const result = await next(e)
114    if (e.agentId !== undefined) {
115      return result
116    }
117    try {
118      const messages = await $.session.messages()
119      const curr = fingerprintMessages(messages)
120      const preview = messages.map(previewMessage)
121      if (lastChain === undefined) {
122        report($, `[cache-guard] custody baseline: ${curr.length} messages`, verbose)
123      } else {
124        const audit = auditCustody(lastChain, curr)
125        if (audit.dropped.length > 0 || audit.moved) {
126          const ui = ($ as { ui: { log: (text: string) => void } }).ui
127          for (const index of audit.dropped) {
128            ui.log(`[cache-guard] custody violation: message #${index} vanished between turns: ${lastPreview[index] ?? ''}`)
129          }
130          if (audit.moved) {
131            ui.log('[cache-guard] custody violation: message order changed between turns')
132          }
133        } else {
134          report($, `[cache-guard] custody ok: ${curr.length} messages, +${audit.appended} appended, none lost`, verbose)
135        }
136      }
137      lastChain = curr
138      lastPreview = preview
139    } catch {
140      report($, '[cache-guard] custody check skipped, messages unreadable', verbose)
141    }
142    return result
143  })
144
145  if (options.forbidAutoCompact === true) {
146    on('session.compact', { trigger: 'auto' }, () => ({
147      skip: '[cache-guard] auto-compact vetoed by forbidAutoCompact; run /compact manually when ready',
148    }))
149  }
150
151  on('session.compact', async ($, e, next) => {
152    const before = fingerprintMessages(e.messages)
153    let at: number
154    try {
155      at = await $.clock.now()
156    } catch {
157      at = Date.now()
158    }
159
160    let evidence = 'store-unavailable'
161    try {
162      await $.store.set(evidenceKey(at), { trigger: e.trigger, at, messages: e.messages })
163      const keys = await $.store.keys()
164      for (const key of pruneKeys(keys, 'compact-evidence:', 2)) {
165        await $.store.delete(key)
166      }
167      evidence = `full snapshot of ${e.messages.length} messages`
168    } catch {
169      try {
170        await $.store.set(evidenceKey(at), { trigger: e.trigger, at, fingerprints: before })
171        evidence = `fingerprints only, full text over store limit`
172      } catch {
173        evidence = 'store-unavailable'
174      }
175    }
176
177    const result = await next(e)
178    if (!('messages' in result) || result.messages === undefined) {
179      report($, `[cache-guard] compact (${e.trigger}) skipped`, verbose)
180      return result
181    }
182    const after = fingerprintMessages(result.messages)
183    const audit = auditCompact(before, after)
184    try {
185      await $.store.set(auditKey(at), { trigger: e.trigger, at, before: before.length, after: after.length, audit })
186      const keys = await $.store.keys()
187      for (const key of pruneKeys(keys, 'compact-audit:', 10)) {
188        await $.store.delete(key)
189      }
190    } catch {
191      report($, '[cache-guard] audit record not stored', verbose)
192    }
193    report(
194      $,
195      `[cache-guard] compact (${e.trigger}): ${before.length} -> ${after.length} messages, ` +
196        `kept ${audit.kept}, dropped ${audit.dropped}, added ${audit.added}; evidence locked (${evidence}); prefix cache resets after compact`,
197      verbose,
198    )
199    return result
200  })
201}
202
hooks/clear-intent/register.ts 41 lines
1import type { On } from 'claude-code'
2
3import { intentNote } from './intent'
4
5type IntentOptions = {
6  confirmFirst?: boolean
7  verbose?: boolean
8}
9
10function report($: unknown, message: string, verbose: boolean): void {
11  const ui = ($ as { ui: { log: (text: string, opts?: object) => void } }).ui
12  if (verbose) {
13    ui.log(message)
14  } else {
15    ui.log(message, { to: 'debug' })
16  }
17}
18
19/**
20 * Attaches the intent protocol beside every submitted prompt, where the
21 * model reads it and the user never sees it. Vague input earns the strong
22 * note: restate, wait for confirmation, read-only until then.
23 *
24 * @param on the engine's registrar
25 * @param options `confirmFirst` upgrades every prompt to the strong note
26 */
27export function register(on: On, options: IntentOptions = {}): void {
28  const confirmFirst = options.confirmFirst === true
29  const verbose = options.verbose === true
30
31  on('session.start', ($, e, next) => {
32    report($, '[clear-intent] active, intent protocol armed for prompt.submit', verbose)
33    return next(e)
34  })
35
36  on('prompt.submit', (_$, e, next) => {
37    const { note } = intentNote(e.text, confirmFirst)
38    return next({ ...e, context: [...(e.context ?? []), note] })
39  })
40}
41
hooks/human-tone/register.ts 43 lines
1import type { On } from 'claude-code'
2
3import { SECTION_ID, insertShared, resolveMode, styleGuide } from './guide'
4
5type ToneOptions = {
6  mode?: string
7  extra?: string
8  verbose?: boolean
9}
10
11function report($: unknown, message: string, verbose: boolean): void {
12  const ui = ($ as { ui: { log: (text: string, opts?: object) => void } }).ui
13  if (verbose) {
14    ui.log(message)
15  } else {
16    ui.log(message, { to: 'debug' })
17  }
18}
19
20/**
21 * Appends one static style section to the system prompt, last among the
22 * shared sections so the engine's cache boundary stays valid. Static text
23 * costs no prompt cache; `extra` is the only varying part, keep it stable.
24 *
25 * @param on the engine's registrar
26 * @param options `mode` picks default, terse or warm; `extra` appends lines
27 */
28export function register(on: On, options: ToneOptions = {}): void {
29  const text = styleGuide(resolveMode(options.mode), options.extra)
30  const verbose = options.verbose === true
31
32  on('session.start', ($, e, next) => {
33    report($, '[human-tone] active, style section armed for prompt.compose', verbose)
34    return next(e)
35  })
36
37  on('prompt.compose', async (_$, e, next) => {
38    const out = await next(e)
39    const mine = { id: SECTION_ID, text, scope: 'shared' as const }
40    return { sections: insertShared(out.sections, mine) }
41  })
42}
43
hooks/prompt-shield/register.ts 85 lines
1import type { On } from 'claude-code'
2
3import { preview, removedCount, stripInvisible } from './sanitize'
4
5type ShieldOptions = {
6  verbose?: boolean
7}
8
9function report($: unknown, message: string, verbose: boolean): void {
10  const ui = ($ as { ui: { log: (text: string, opts?: object) => void } }).ui
11  if (verbose) {
12    ui.log(message)
13  } else {
14    ui.log(message, { to: 'debug' })
15  }
16}
17
18/**
19 * Registers prompt hygiene hooks: prompt.submit/context/attachment and
20 * attribution.text are scanned on the way out, invisible characters that can
21 * carry a hidden signature are removed, the cleaned value continues down.
22 * Every hook fires on engine events alone, so the mod works from session
23 * start with no model call needed; session.start only announces readiness.
24 *
25 * @param on the engine's registrar
26 * @param options the plugin's options; `verbose` surfaces each strip in the transcript
27 */
28export function register(on: On, options: ShieldOptions = {}): void {
29  const verbose = options.verbose === true
30
31  on('session.start', ($, e, next) => {
32    report($, '[prompt-shield] active, watching prompt.submit, prompt.context, prompt.attachment, attribution.text', verbose)
33    return next(e)
34  })
35
36  on('prompt.submit', ($, e, next) => {
37    const text = stripInvisible(e.text)
38    const dropped = removedCount(e.text, text)
39    const context = e.context?.map((entry) => stripInvisible(entry))
40    const contextDropped = (e.context ?? []).reduce(
41      (sum, entry, i) => sum + removedCount(entry, context?.[i] ?? entry),
42      0,
43    )
44    if (dropped > 0 || contextDropped > 0) {
45      report($, `[prompt-shield] prompt.submit stripped ${dropped + contextDropped} invisible chars: ${preview(e.text)}`, verbose)
46    }
47    return next({ ...e, text, context })
48  })
49
50  on('prompt.context', async ($, e, next) => {
51    const blocks = e.blocks.map((block) => {
52      const text = stripInvisible(block.text)
53      const dropped = removedCount(block.text, text)
54      if (dropped > 0) {
55        report($, `[prompt-shield] prompt.context block "${block.name}" stripped ${dropped} invisible chars`, verbose)
56      }
57      return { ...block, text }
58    })
59    return next({ ...e, blocks })
60  })
61
62  on('prompt.attachment', async ($, e, next) => {
63    const out = await next(e)
64    if (out.text === null) {
65      return out
66    }
67    const text = stripInvisible(out.text)
68    const dropped = removedCount(out.text, text)
69    if (dropped > 0) {
70      report($, `[prompt-shield] prompt.attachment "${e.type}" stripped ${dropped} invisible chars`, verbose)
71    }
72    return { text }
73  })
74
75  on('attribution.text', async ($, e, next) => {
76    const out = await next(e)
77    const text = stripInvisible(out.text)
78    const dropped = removedCount(out.text, text)
79    if (dropped > 0) {
80      report($, `[prompt-shield] attribution.text "${e.kind}" stripped ${dropped} invisible chars`, verbose)
81    }
82    return { text }
83  })
84}
85
hooks/agent-charter/charter.ts 86 lines
1/**
2 * Agent charter: one static section pinned into the system prompt. Static on
3 * purpose, so it sits on the shared side of the cache boundary in append
4 * mode. Replace mode drops the engine's own composition entirely, tools
5 * included: only for operators who know what they are giving up.
6 */
7
8export const CHARTER_ID = 'plus:agent-charter'
9
10export type CharterMode = 'append' | 'replace'
11
12const CHARTER = [
13  '# 代理宪章',
14  '',
15  '## Skill 与 Plugin 优先',
16  '开工前先列出本次任务相关的 skill 和已装 plugin,有现成能力先用,不重复造轮子。',
17  '项目级 skill 优先于通用做法;调用了哪个 skill,在总结里注明。',
18  '不要臆造不存在的 skill、命令或 API,不确定就先查。',
19  '',
20  '## 注释规范(按语言)',
21  '注释只解释为什么,不解释是什么;禁止装饰性注释(分隔线、星号框、ASCII 艺术)。',
22  '- Java: Javadoc',
23  '- Python: Docstring',
24  '- C#: XML 文档注释',
25  '- PHP: PHPDoc',
26  '- C / C++: Doxygen',
27  '- Go: Go Doc,注释紧贴声明并以名称开头',
28  '- Rust: Rustdoc(/// 对外,//! 对内)',
29  '- Ruby: RDoc / YARD',
30  '- Kotlin: KDoc',
31  '',
32  '## 提交信息格式',
33  '严格遵守:[模块][n/m]{【类型】【版本】中文\\英文描述}(需求号)',
34  '示例:[鉴权][2/5]{【修复】【v2.3】登录超时 Login timeout}(REQ-1234)',
35  'n/m 是本次系列提交的序号与总数;无需求号时括号内写实际事由,不许空着。',
36  '',
37  '## 分支与 PR 纪律',
38  '每次修改从主分支切新分支,一事一分支,禁止直接在主分支上改。',
39  '自测通过、确认无误后才合并;合并后删除分支,不许堆积。',
40  'PR 必须讲清三件事:改了什么、为什么改、怎么验证的;缺任何一件都不许合。',
41].join('\n')
42
43/**
44 * @param raw the `charterMode` option as received, anything unexpected falls back
45 * @returns append by default, replace only when explicitly asked
46 */
47export function resolveCharterMode(raw: unknown): CharterMode {
48  if (raw === 'replace') {
49    return 'replace'
50  }
51  return 'append'
52}
53
54/**
55 * @returns the full charter text
56 */
57export function buildCharter(): string {
58  return CHARTER
59}
60
61export type ComposableSection = {
62  id: string
63  text: string
64  scope: 'shared' | 'session'
65}
66
67/**
68 * @param sections the engine's composed list, every shared one ahead of every session one
69 * @param mine the charter section to add
70 * @param mode append keeps the engine's composition and adds the charter last
71 * among shared; replace drops everything and returns the charter alone
72 * @returns the sections the model will read
73 */
74export function applyCharter(
75  sections: readonly ComposableSection[],
76  mine: ComposableSection,
77  mode: CharterMode,
78): ComposableSection[] {
79  if (mode === 'replace') {
80    return [mine]
81  }
82  const shared = sections.filter((section) => section.scope === 'shared')
83  const session = sections.filter((section) => section.scope !== 'shared')
84  return [...shared, mine, ...session]
85}
86
hooks/cache-guard/fingerprint.ts 223 lines
1/**
2 * Byte-stability helpers: the provider's prompt cache hits only on an exact
3 * prefix match, so any drift in what the engine sends is a guaranteed miss.
4 * These functions detect that drift; they send nothing and change nothing.
5 */
6
7/**
8 * @param value any JSON-like value
9 * @returns canonical string with sorted keys; the engine's `handle` dropped so
10 * identity metadata never counts as content drift
11 */
12export function stableStringify(value: unknown): string {
13  const seen = new Set<object>()
14
15  function encode(node: unknown): string {
16    if (node === null || typeof node !== 'object') {
17      const text = JSON.stringify(node)
18      return text === undefined ? 'undefined' : text
19    }
20    if (seen.has(node)) {
21      return '"[circular]"'
22    }
23    seen.add(node)
24    if (Array.isArray(node)) {
25      return `[${node.map(encode).join(',')}]`
26    }
27    const record = node as Record<string, unknown>
28    const keys = Object.keys(record)
29      .filter((key) => key !== 'handle')
30      .sort()
31    return `{${keys.map((key) => `${JSON.stringify(key)}:${encode(record[key])}`).join(',')}}`
32  }
33
34  try {
35    return encode(value)
36  } catch {
37    return String(value)
38  }
39}
40
41/**
42 * @param text canonical text
43 * @returns 8-hex-digit FNV-1a over UTF-16 units; sync and dependency-free on
44 * purpose, so hooks never wait on crypto for a change-detection hash
45 */
46export function fnv1a(text: string): string {
47  let hash = 0x811c9dc5
48  for (let i = 0; i < text.length; i++) {
49    hash ^= text.charCodeAt(i)
50    hash = Math.imul(hash, 0x01000193)
51  }
52  return (hash >>> 0).toString(16).padStart(8, '0')
53}
54
55/**
56 * @param messages transcript messages in `$.session.messages()` shape
57 * @returns one content hash per message, order preserved
58 */
59export function fingerprintMessages(messages: readonly unknown[]): string[] {
60  return messages.map((message) => fnv1a(stableStringify(message)))
61}
62
63export type CompactAudit = {
64  kept: number
65  dropped: number
66  added: number
67}
68
69/**
70 * @param before content hashes going into compaction
71 * @param after content hashes coming out of it
72 * @returns multiset diff: kept, dropped, added
73 */
74export function auditCompact(before: readonly string[], after: readonly string[]): CompactAudit {
75  const remaining = new Map<string, number>()
76  for (const hash of after) {
77    remaining.set(hash, (remaining.get(hash) ?? 0) + 1)
78  }
79  let kept = 0
80  for (const hash of before) {
81    const count = remaining.get(hash) ?? 0
82    if (count > 0) {
83      kept++
84      remaining.set(hash, count - 1)
85    }
86  }
87  let added = 0
88  for (const count of remaining.values()) {
89    added += count
90  }
91  return { kept, dropped: before.length - kept, added }
92}
93
94export function evidenceKey(at: number): string {
95  return `compact-evidence:${at}`
96}
97
98export function auditKey(at: number): string {
99  return `compact-audit:${at}`
100}
101
102/**
103 * @param keys every key in the plugin store, insertion ordered
104 * @param prefix only keys under this prefix are managed, foreign keys untouched
105 * @param keep how many newest snapshots survive
106 * @returns keys to delete, oldest first
107 */
108export function pruneKeys(keys: readonly string[], prefix: string, keep: number): string[] {
109  const owned = keys.filter((key) => key.startsWith(prefix))
110  const stamped = owned
111    .map((key) => ({ key, at: Number(key.slice(prefix.length)) }))
112    .filter((entry) => Number.isFinite(entry.at))
113    .sort((a, b) => b.at - a.at)
114  return stamped.slice(keep).map((entry) => entry.key)
115}
116
117export type CustodyAudit = {
118  appended: number
119  dropped: number[]
120  moved: boolean
121}
122
123/**
124 * @param prev content hashes at the end of the previous turn
125 * @param curr content hashes now
126 * @returns append-only check: previously seen messages must survive in order.
127 * `dropped` names previous indexes that vanished, `moved` flags a reorder,
128 * `appended` counts genuinely new tail messages.
129 */
130export function auditCustody(prev: readonly string[], curr: readonly string[]): CustodyAudit {
131  const dropped: number[] = []
132  let cursor = 0
133  for (let i = 0; i < prev.length; i++) {
134    const found = curr.indexOf(prev[i] as string, cursor)
135    if (found === -1) {
136      dropped.push(i)
137    } else {
138      cursor = found + 1
139    }
140  }
141  const matched = prev.length - dropped.length
142  const isPrefix = matched === prev.length && curr.slice(0, prev.length).every((hash, i) => hash === prev[i])
143  return { appended: curr.length - matched, dropped, moved: matched > 0 && !isPrefix }
144}
145
146/**
147 * @param message one transcript message
148 * @returns short `[role] head...` preview for violation logs; never throws,
149 * never dumps full tool results into the transcript
150 */
151export function previewMessage(message: unknown): string {
152  if (typeof message === 'object' && message !== null) {
153    const record = message as Record<string, unknown>
154    const role = typeof record.role === 'string' ? record.role : '?'
155    const raw = typeof record.text === 'string' ? record.text : stableStringify(record.text)
156    const flat = raw.replace(/\s+/g, ' ')
157    const head = flat.length > 80 ? `${flat.slice(0, 80)}...` : flat
158    return `[${role}] ${head}`
159  }
160  return stableStringify(message).slice(0, 80)
161}
162
163export type ApiUsage = {
164  input_tokens: number
165  cache_creation_input_tokens: number
166  cache_read_input_tokens: number
167  output_tokens: number
168}
169
170/**
171 * @param text section or context text as the engine composed it
172 * @returns text with the volatile parts normalized: ISO datetimes collapse to
173 * their date, clock times to 00:00, CRLF to LF. Dates stay (useful), the
174 * ever-ticking clock goes (pure cache poison). Code-shaped text without
175 * timestamps passes through untouched.
176 */
177export function normalizeVolatile(text: string): string {
178  return text
179    .replace(/\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?/g, (match) => match.slice(0, 10))
180    .replace(/\b\d{2}:\d{2}(?::\d{2})?\b/g, '00:00')
181    .replace(/\r\n/g, '\n')
182}
183
184export type Stabilized = {
185  text: string
186  pinned: boolean
187  drifted: boolean
188}
189
190/**
191 * @param previous the text this key last settled on, absent on first sight
192 * @param current the text the engine composed just now
193 * @returns pinned text when only volatile parts moved (a free hit saved),
194 * current text with drift flagged when content really changed
195 */
196export function stabilize(previous: string | undefined, current: string): Stabilized {
197  if (previous === undefined || previous === current) {
198    return { text: current, pinned: false, drifted: false }
199  }
200  if (normalizeVolatile(previous) === normalizeVolatile(current)) {
201    return { text: previous, pinned: true, drifted: false }
202  }
203  return { text: current, pinned: false, drifted: true }
204}
205
206/**
207 * @param api the last response's token counts as the API reported them
208 * @returns one log line, e.g. `cache read 18000/20000 input (90%), created 0, output 320`
209 */
210export function cacheLine(api: ApiUsage): string {
211  const total = api.input_tokens + api.cache_creation_input_tokens + api.cache_read_input_tokens
212  const pct = total === 0 ? 100 : Math.round((api.cache_read_input_tokens / total) * 100)
213  return `cache read ${api.cache_read_input_tokens}/${total} input (${pct}%), created ${api.cache_creation_input_tokens}, output ${api.output_tokens}`
214}
215
216/**
217 * @param api the last response's token counts
218 * @returns true when nothing was served from cache despite a non-empty request
219 */
220export function isColdMiss(api: ApiUsage): boolean {
221  return api.cache_read_input_tokens === 0 && api.input_tokens + api.cache_creation_input_tokens > 0
222}
223
hooks/clear-intent/intent.ts 38 lines
1/**
2 * Intent protocol: a short confirmation-first note attached beside every
3 * prompt. The model cannot be forced into an inner loop from outside, so the
4 * lever is the instruction it reads: restate the goal, mark what is assumed,
5 * and stop before load-bearing actions on vague input.
6 */
7
8export type IntentLevel = 'strong' | 'standard'
9
10const STANDARD_NOTE = [
11  '意图协议:动工前先用一句话说清用户到底要什么。',
12  '严格区分【已确认】和【脑补】;靠脑补才能成立的关键步骤,先问一句再动手。',
13  '禁止过度解读:没说的需求不许自行脑补实现。',
14].join('\n')
15
16const STRONG_NOTE = [
17  '意图协议(本次输入模糊):必须先用一句话复述你的理解并等用户确认。',
18  '确认之前只做只读操作,不写文件、不跑会产生副作用的命令。',
19  '禁止按自己的猜测直接开工,猜错比追问贵。',
20].join('\n')
21
22const VAGUE_PATTERNS = [/随便/, /看着办/, /都可以/, /都行/, /帮我(一下)?$/, /^(hi|hello|在吗|你好)[!!.。]*$/i]
23
24/**
25 * @param text the prompt as typed
26 * @param confirmFirst when true, every prompt gets the strong note
27 * @returns the note level and text to attach beside the prompt
28 */
29export function intentNote(text: string, confirmFirst = false): { level: IntentLevel; note: string } {
30  const trimmed = text.trim()
31  const short = [...trimmed].length < 12
32  const vague = VAGUE_PATTERNS.some((pattern) => pattern.test(trimmed))
33  if (confirmFirst || short || vague) {
34    return { level: 'strong', note: STRONG_NOTE }
35  }
36  return { level: 'standard', note: STANDARD_NOTE }
37}
38
hooks/human-tone/guide.ts 70 lines
1/**
2 * House style texts: one static section pinned into the system prompt.
3 * Static on purpose, so it sits on the shared side of the cache boundary
4 * without spending anyone's prompt cache.
5 */
6
7export const SECTION_ID = 'plus:human-tone'
8
9export type ToneMode = 'default' | 'terse' | 'warm'
10
11const DEFAULT_GUIDE = [
12  '说话像人,不像客服。先给结论,再给细节。',
13  '禁止空洞吹捧("很好的问题"、"你问得太对了"之类一律不许)。',
14  '不确定的直说不确定,不编;能查的先查再下结论。',
15  '用户用什么语言,就用什么语言回。',
16  '格式克制:不用 emoji,不滥用表格和标题,短回答优先。',
17  '代码先给能跑的,再解释为什么;不贴没验证过的写法。',
18].join('\n')
19
20const TERSE_GUIDE = [
21  '极简模式:能一句话不说两句,能给代码不给散文。',
22  '禁止寒暄、吹捧、总结陈词,直接上干货。',
23  '不确定的用一句标出,不展开。',
24].join('\n')
25
26const WARM_GUIDE = [
27  '在默认风格上多一点人情味:共情先行,但不说废话。',
28  '夸只夸具体的,不夸空泛的;安慰给方案,不给鸡汤。',
29  '其余与默认一致:先结论后细节,不确定直说。',
30].join('\n')
31
32/**
33 * @param raw the `mode` option as received, anything unexpected falls back
34 * @returns a known tone mode, never throws on user input
35 */
36export function resolveMode(raw: unknown): ToneMode {
37  if (raw === 'terse' || raw === 'warm' || raw === 'default') {
38    return raw
39  }
40  return 'default'
41}
42
43/**
44 * @param mode which built-in guide to use
45 * @param extra user-supplied lines appended verbatim, absent when empty
46 * @returns the full section text
47 */
48export function styleGuide(mode: ToneMode, extra?: string): string {
49  const base = mode === 'terse' ? TERSE_GUIDE : mode === 'warm' ? WARM_GUIDE : DEFAULT_GUIDE
50  const tail = extra === undefined || extra.trim() === '' ? '' : `\n${extra.trim()}`
51  return `${base}${tail}`
52}
53
54export type ComposableSection = {
55  id: string
56  text: string
57  scope: 'shared' | 'session'
58}
59
60/**
61 * @param sections the engine's composed list, every shared one ahead of every session one
62 * @param mine the style section to add
63 * @returns list with the style section last among shared, so the engine's own boundary stays valid
64 */
65export function insertShared(sections: readonly ComposableSection[], mine: ComposableSection): ComposableSection[] {
66  const shared = sections.filter((section) => section.scope === 'shared')
67  const session = sections.filter((section) => section.scope !== 'shared')
68  return [...shared, mine, ...session]
69}
70
hooks/prompt-shield/sanitize.ts 28 lines
1/**
2 * @param text raw text about to leave the client
3 * @returns text with invisible fingerprint characters removed
4 */
5export function stripInvisible(text: string): string {
6  return text
7    .replace(/[\u00AD\u034F\u061C\u115F\u1160\u17B4\u17B5\u180E\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF\uFE00-\uFE0F]/g, '')
8    .replace(/[\uE0000-\uE007F\uE0100-\uE01EF]/gu, '');
9}
10
11/**
12 * @param before original text
13 * @param after sanitized text
14 * @returns number of characters removed
15 */
16export function removedCount(before: string, after: string): number {
17  return before.length - after.length;
18}
19
20/**
21 * @param text raw text
22 * @returns one-line preview with control characters escaped, for audit logs
23 */
24export function preview(text: string): string {
25  const escaped = text.replace(/[\u0000-\u001F\u007F-\u009F]/g, (c) => `\\u{${c.codePointAt(0)?.toString(16)}}`);
26  return escaped.length > 120 ? `${escaped.slice(0, 120)}…` : escaped;
27}
28