forked from ultraworkers/claw-code
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhintRecommendation.ts
More file actions
164 lines (146 loc) · 5.3 KB
/
Copy pathhintRecommendation.ts
File metadata and controls
164 lines (146 loc) · 5.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
/**
* Plugin-hint recommendations.
*
* Companion to lspRecommendation.ts: where LSP recommendations are triggered
* by file edits, plugin hints are triggered by CLIs/SDKs emitting a
* `<claude-code-hint />` tag to stderr (detected by the Bash/PowerShell tools).
*
* State persists in GlobalConfig.claudeCodeHints — a show-once record per
* plugin and a disabled flag (user picked "don't show again"). Official-
* marketplace filtering is hardcoded for v1.
*/
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
logEvent,
} from '../../services/analytics/index.js'
import {
type ClaudeCodeHint,
hasShownHintThisSession,
setPendingHint,
} from '../claudeCodeHints.js'
import { getGlobalConfig, saveGlobalConfig } from '../config.js'
import { logForDebugging } from '../debug.js'
import { isPluginInstalled } from './installedPluginsManager.js'
import { getPluginById } from './marketplaceManager.js'
import {
isOfficialMarketplaceName,
parsePluginIdentifier,
} from './pluginIdentifier.js'
import { isPluginBlockedByPolicy } from './pluginPolicy.js'
/**
* Hard cap on `claudeCodeHints.plugin[]` — bounds config growth. Each shown
* plugin appends one slug; past this point we stop prompting (and stop
* appending) rather than let the config grow without limit.
*/
const MAX_SHOWN_PLUGINS = 100
export type PluginHintRecommendation = {
pluginId: string
pluginName: string
marketplaceName: string
pluginDescription?: string
sourceCommand: string
}
/**
* Pre-store gate called by shell tools when a `type="plugin"` hint is detected.
* Drops the hint if:
*
* - a dialog has already been shown this session
* - user has disabled hints
* - the shown-plugins list has hit the config-growth cap
* - plugin slug doesn't parse as `name@marketplace`
* - marketplace isn't official (hardcoded for v1)
* - plugin is already installed
* - plugin was already shown in a prior session
*
* Synchronous on purpose — shell tools shouldn't await a marketplace lookup
* just to strip a stderr line. The async marketplace-cache check happens
* later in resolvePluginHint (hook side).
*/
export function maybeRecordPluginHint(hint: ClaudeCodeHint): void {
if (!getFeatureValue_CACHED_MAY_BE_STALE('tengu_lapis_finch', false)) return
if (hasShownHintThisSession()) return
const state = getGlobalConfig().claudeCodeHints
if (state?.disabled) return
const shown = state?.plugin ?? []
if (shown.length >= MAX_SHOWN_PLUGINS) return
const pluginId = hint.value
const { name, marketplace } = parsePluginIdentifier(pluginId)
if (!name || !marketplace) return
if (!isOfficialMarketplaceName(marketplace)) return
if (shown.includes(pluginId)) return
if (isPluginInstalled(pluginId)) return
if (isPluginBlockedByPolicy(pluginId)) return
// Bound repeat lookups on the same slug — a CLI that emits on every
// invocation shouldn't trigger N resolve cycles for the same plugin.
if (triedThisSession.has(pluginId)) return
triedThisSession.add(pluginId)
setPendingHint(hint)
}
const triedThisSession = new Set<string>()
/** Test-only reset. */
export function _resetHintRecommendationForTesting(): void {
triedThisSession.clear()
}
/**
* Resolve the pending hint to a renderable recommendation. Runs the async
* marketplace lookup that the sync pre-store gate skipped. Returns null if
* the plugin isn't in the marketplace cache — the hint is discarded.
*/
export async function resolvePluginHint(
hint: ClaudeCodeHint,
): Promise<PluginHintRecommendation | null> {
const pluginId = hint.value
const { name, marketplace } = parsePluginIdentifier(pluginId)
const pluginData = await getPluginById(pluginId)
logEvent('tengu_plugin_hint_detected', {
_PROTO_plugin_name: (name ??
'') as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
_PROTO_marketplace_name: (marketplace ??
'') as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED,
result: (pluginData
? 'passed'
: 'not_in_cache') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
if (!pluginData) {
logForDebugging(
`[hintRecommendation] ${pluginId} not found in marketplace cache`,
)
return null
}
return {
pluginId,
pluginName: pluginData.entry.name,
marketplaceName: marketplace ?? '',
pluginDescription: pluginData.entry.description,
sourceCommand: hint.sourceCommand,
}
}
/**
* Record that a prompt for this plugin was surfaced. Called regardless of
* the user's yes/no response — show-once semantics.
*/
export function markHintPluginShown(pluginId: string): void {
saveGlobalConfig(current => {
const existing = current.claudeCodeHints?.plugin ?? []
if (existing.includes(pluginId)) return current
return {
...current,
claudeCodeHints: {
...current.claudeCodeHints,
plugin: [...existing, pluginId],
},
}
})
}
/** Called when the user picks "don't show plugin installation hints again". */
export function disableHintRecommendations(): void {
saveGlobalConfig(current => {
if (current.claudeCodeHints?.disabled) return current
return {
...current,
claudeCodeHints: { ...current.claudeCodeHints, disabled: true },
}
})
}