@omercnet/paseo-context-mode 0.0.0
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/LICENSE +21 -0
- package/NOTICE +7 -0
- package/README.md +74 -0
- package/client/actions-section.tsx +621 -0
- package/client/analytics-dashboard.tsx +721 -0
- package/client/context-mode-pill.tsx +106 -0
- package/client/context-mode-surface.tsx +414 -0
- package/client/knowledge-section.tsx +780 -0
- package/client/settings-screen.tsx +226 -0
- package/index.client.tsx +43 -0
- package/index.server.ts +54 -0
- package/package.json +72 -0
- package/paseo-plugin.json +5 -0
- package/server/actions.ts +146 -0
- package/server/analytics.ts +424 -0
- package/server/binary.ts +182 -0
- package/server/context-mode-db.ts +481 -0
- package/server/handlers.ts +200 -0
- package/server/integration.ts +441 -0
- package/server/knowledge.ts +187 -0
- package/server/mcp-process.ts +357 -0
- package/server/storage-discovery.ts +225 -0
- package/shared/actions.ts +57 -0
- package/shared/analytics.ts +134 -0
- package/shared/contracts.ts +95 -0
- package/shared/index.ts +5 -0
- package/shared/knowledge.ts +150 -0
- package/shared/settings.ts +51 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { type PluginSurfaceProps, type SettingsState, useSettings } from "@getpaseo/plugin/client";
|
|
2
|
+
import {
|
|
3
|
+
SettingsAction,
|
|
4
|
+
SettingsCard,
|
|
5
|
+
SettingsInput,
|
|
6
|
+
SettingsRow,
|
|
7
|
+
SettingsSection,
|
|
8
|
+
SettingsSelect,
|
|
9
|
+
SettingsSwitch,
|
|
10
|
+
} from "@getpaseo/plugin/client/ui";
|
|
11
|
+
import { useCallback, useMemo, useState } from "react";
|
|
12
|
+
import { Text, View } from "react-native";
|
|
13
|
+
import {
|
|
14
|
+
type ContextModeSettings,
|
|
15
|
+
ContextModeSettingsSchema,
|
|
16
|
+
contextModeSettings,
|
|
17
|
+
} from "../shared";
|
|
18
|
+
|
|
19
|
+
type ReadySettings = Extract<SettingsState<typeof contextModeSettings.schema>, { status: "ready" }>;
|
|
20
|
+
|
|
21
|
+
const binaryModeOptions = [
|
|
22
|
+
{ label: "Configured path, PATH, then bundled", value: "path" },
|
|
23
|
+
{ label: "PATH, then bundled", value: "automatic" },
|
|
24
|
+
];
|
|
25
|
+
const refreshOptions = [
|
|
26
|
+
{ label: "5 seconds", value: "5000" },
|
|
27
|
+
{ label: "15 seconds", value: "15000" },
|
|
28
|
+
{ label: "30 seconds", value: "30000" },
|
|
29
|
+
{ label: "60 seconds", value: "60000" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function ReadyContextModeSettings({
|
|
33
|
+
settings,
|
|
34
|
+
theme,
|
|
35
|
+
}: {
|
|
36
|
+
settings: ReadySettings;
|
|
37
|
+
theme: PluginSurfaceProps["theme"];
|
|
38
|
+
}) {
|
|
39
|
+
const [binaryPath, setBinaryPath] = useState(settings.values.binaryPath);
|
|
40
|
+
const styles = useMemo(
|
|
41
|
+
() => ({
|
|
42
|
+
root: { gap: 16 },
|
|
43
|
+
value: { color: theme.colors.foreground },
|
|
44
|
+
muted: { color: theme.colors.foregroundMuted },
|
|
45
|
+
error: { color: theme.colors.statusDanger },
|
|
46
|
+
}),
|
|
47
|
+
[theme],
|
|
48
|
+
);
|
|
49
|
+
const save = useCallback(
|
|
50
|
+
(patch: Partial<ContextModeSettings>) =>
|
|
51
|
+
settings.save({ ...settings.values, ...patch }, settings.revision),
|
|
52
|
+
[settings],
|
|
53
|
+
);
|
|
54
|
+
const normalizedPath = binaryPath.trim();
|
|
55
|
+
const pathResult = ContextModeSettingsSchema.safeParse({
|
|
56
|
+
...settings.values,
|
|
57
|
+
binaryMode: "path",
|
|
58
|
+
binaryPath: normalizedPath,
|
|
59
|
+
});
|
|
60
|
+
const pathError = pathResult.success
|
|
61
|
+
? null
|
|
62
|
+
: (pathResult.error.issues[0]?.message ?? "Enter an absolute executable path.");
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<View style={styles.root}>
|
|
66
|
+
<SettingsSection
|
|
67
|
+
title="Executable"
|
|
68
|
+
info="The daemon checks a configured absolute path first, then PATH, then the bundled runtime."
|
|
69
|
+
>
|
|
70
|
+
<SettingsCard>
|
|
71
|
+
<SettingsSelect
|
|
72
|
+
label="Binary lookup"
|
|
73
|
+
value={settings.values.binaryMode}
|
|
74
|
+
options={binaryModeOptions}
|
|
75
|
+
disabled={settings.saving}
|
|
76
|
+
onValueChange={(binaryMode) =>
|
|
77
|
+
void save({ binaryMode: binaryMode as ContextModeSettings["binaryMode"] })
|
|
78
|
+
}
|
|
79
|
+
/>
|
|
80
|
+
<SettingsInput
|
|
81
|
+
label="Absolute binary path"
|
|
82
|
+
hint="Optional external override. Leave empty to use PATH or the bundled runtime."
|
|
83
|
+
error={normalizedPath === settings.values.binaryPath ? null : pathError}
|
|
84
|
+
initialValue={binaryPath}
|
|
85
|
+
onChangeText={setBinaryPath}
|
|
86
|
+
placeholder="/usr/local/bin/context-mode"
|
|
87
|
+
disabled={settings.saving || settings.values.binaryMode === "automatic"}
|
|
88
|
+
/>
|
|
89
|
+
<SettingsAction
|
|
90
|
+
label="Save binary path"
|
|
91
|
+
actionLabel="Save"
|
|
92
|
+
disabled={
|
|
93
|
+
settings.saving ||
|
|
94
|
+
settings.values.binaryMode === "automatic" ||
|
|
95
|
+
normalizedPath === settings.values.binaryPath ||
|
|
96
|
+
!pathResult.success
|
|
97
|
+
}
|
|
98
|
+
onPress={() => void save({ binaryPath: normalizedPath })}
|
|
99
|
+
/>
|
|
100
|
+
</SettingsCard>
|
|
101
|
+
</SettingsSection>
|
|
102
|
+
|
|
103
|
+
<SettingsSection
|
|
104
|
+
title="Agent activation"
|
|
105
|
+
info="Context Mode is added to supported Paseo agents when their sessions are created."
|
|
106
|
+
>
|
|
107
|
+
<SettingsCard>
|
|
108
|
+
<SettingsSwitch
|
|
109
|
+
label="Automatically activate Context Mode"
|
|
110
|
+
hint="Inject the bundled MCP server into new agents and provider-specific storage variables whenever sessions open."
|
|
111
|
+
value={settings.values.autoInject}
|
|
112
|
+
disabled={settings.saving}
|
|
113
|
+
onValueChange={(autoInject) => void save({ autoInject })}
|
|
114
|
+
/>
|
|
115
|
+
<SettingsSwitch
|
|
116
|
+
label="Prefer native integrations"
|
|
117
|
+
hint="Keep OMP, Pi, and OpenCode native plugins when detected instead of registering duplicate MCP tools."
|
|
118
|
+
value={settings.values.preferNativeIntegrations}
|
|
119
|
+
disabled={settings.saving || !settings.values.autoInject}
|
|
120
|
+
onValueChange={(preferNativeIntegrations) => void save({ preferNativeIntegrations })}
|
|
121
|
+
/>
|
|
122
|
+
</SettingsCard>
|
|
123
|
+
</SettingsSection>
|
|
124
|
+
|
|
125
|
+
<SettingsSection title="Display">
|
|
126
|
+
<SettingsCard>
|
|
127
|
+
<SettingsSelect
|
|
128
|
+
label="Refresh interval"
|
|
129
|
+
value={String(settings.values.refreshIntervalMs)}
|
|
130
|
+
options={refreshOptions}
|
|
131
|
+
disabled={settings.saving}
|
|
132
|
+
onValueChange={(value) =>
|
|
133
|
+
void save({
|
|
134
|
+
refreshIntervalMs: Number(value) as ContextModeSettings["refreshIntervalMs"],
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
/>
|
|
138
|
+
</SettingsCard>
|
|
139
|
+
</SettingsSection>
|
|
140
|
+
|
|
141
|
+
<SettingsSection title="Safety">
|
|
142
|
+
<SettingsCard>
|
|
143
|
+
<SettingsRow
|
|
144
|
+
label="Storage reuse"
|
|
145
|
+
hint="Existing provider-specific Context Mode roots are reused. Session databases are never merged across providers."
|
|
146
|
+
>
|
|
147
|
+
<Text style={styles.value}>Provider scoped</Text>
|
|
148
|
+
</SettingsRow>
|
|
149
|
+
<SettingsRow label="Context Mode runtime">
|
|
150
|
+
<Text style={styles.muted}>Bundled with this Paseo plugin</Text>
|
|
151
|
+
</SettingsRow>
|
|
152
|
+
<SettingsRow label="Upgrades">
|
|
153
|
+
<Text style={styles.muted}>Explicit command only</Text>
|
|
154
|
+
</SettingsRow>
|
|
155
|
+
</SettingsCard>
|
|
156
|
+
</SettingsSection>
|
|
157
|
+
|
|
158
|
+
<SettingsSection title="Persistence">
|
|
159
|
+
<SettingsCard>
|
|
160
|
+
<SettingsRow label="Settings" hint="Shared by clients connected to this host">
|
|
161
|
+
<Text style={styles.value}>{settings.saving ? "Saving…" : "Saved on host"}</Text>
|
|
162
|
+
</SettingsRow>
|
|
163
|
+
<SettingsAction
|
|
164
|
+
label="Restore defaults"
|
|
165
|
+
actionLabel="Reset"
|
|
166
|
+
disabled={settings.saving}
|
|
167
|
+
onPress={() => void settings.reset()}
|
|
168
|
+
/>
|
|
169
|
+
{settings.saveError ? (
|
|
170
|
+
<SettingsAction
|
|
171
|
+
label="Reload saved values"
|
|
172
|
+
error={settings.saveError}
|
|
173
|
+
actionLabel="Reload"
|
|
174
|
+
disabled={settings.saving}
|
|
175
|
+
onPress={settings.reload}
|
|
176
|
+
/>
|
|
177
|
+
) : null}
|
|
178
|
+
</SettingsCard>
|
|
179
|
+
{settings.saveError ? (
|
|
180
|
+
<Text accessibilityRole="alert" style={styles.error}>
|
|
181
|
+
{settings.saveError}
|
|
182
|
+
</Text>
|
|
183
|
+
) : null}
|
|
184
|
+
</SettingsSection>
|
|
185
|
+
</View>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function ContextModeSettingsScreen({ theme }: PluginSurfaceProps) {
|
|
190
|
+
const settings = useSettings(contextModeSettings);
|
|
191
|
+
const styles = useMemo(
|
|
192
|
+
() => ({
|
|
193
|
+
text: { color: theme.colors.foreground },
|
|
194
|
+
error: { color: theme.colors.statusDanger },
|
|
195
|
+
}),
|
|
196
|
+
[theme],
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
if (settings.status === "loading") return <Text style={styles.text}>Loading settings…</Text>;
|
|
200
|
+
if (settings.status !== "ready") {
|
|
201
|
+
return (
|
|
202
|
+
<SettingsSection title="Context Mode settings">
|
|
203
|
+
<Text accessibilityRole="alert" style={styles.error}>
|
|
204
|
+
{settings.error}
|
|
205
|
+
</Text>
|
|
206
|
+
<SettingsCard>
|
|
207
|
+
<SettingsAction
|
|
208
|
+
label="Read settings again"
|
|
209
|
+
actionLabel="Reload"
|
|
210
|
+
onPress={settings.reload}
|
|
211
|
+
/>
|
|
212
|
+
{settings.status === "invalid" ? (
|
|
213
|
+
<SettingsAction
|
|
214
|
+
label="Replace invalid data with defaults"
|
|
215
|
+
actionLabel="Reset"
|
|
216
|
+
disabled={settings.saving}
|
|
217
|
+
onPress={() => void settings.reset()}
|
|
218
|
+
/>
|
|
219
|
+
) : null}
|
|
220
|
+
</SettingsCard>
|
|
221
|
+
</SettingsSection>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return <ReadyContextModeSettings key={settings.revision} settings={settings} theme={theme} />;
|
|
226
|
+
}
|
package/index.client.tsx
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { PluginClientContext, PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
2
|
+
import { contributeContextModeComposerPills } from "./client/context-mode-pill";
|
|
3
|
+
import { ContextModeSurface } from "./client/context-mode-surface";
|
|
4
|
+
import { ContextModeSettingsScreen } from "./client/settings-screen";
|
|
5
|
+
|
|
6
|
+
export default function contribute(client: PluginClientContext) {
|
|
7
|
+
function Surface(props: PluginSurfaceProps) {
|
|
8
|
+
return (
|
|
9
|
+
<ContextModeSurface {...props} onOpenSettings={() => client.openSettings("context-mode")} />
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const cleanups = [
|
|
14
|
+
contributeContextModeComposerPills(client),
|
|
15
|
+
client.addSettingsScreen({
|
|
16
|
+
id: "context-mode",
|
|
17
|
+
title: "Context Mode settings",
|
|
18
|
+
icon: "Settings",
|
|
19
|
+
Component: ContextModeSettingsScreen,
|
|
20
|
+
}),
|
|
21
|
+
client.addSurface("context-mode", Surface),
|
|
22
|
+
client.addSidebarItem({
|
|
23
|
+
id: "context-mode",
|
|
24
|
+
title: "Context Mode",
|
|
25
|
+
icon: "Gauge",
|
|
26
|
+
surface: "context-mode",
|
|
27
|
+
}),
|
|
28
|
+
client.addCommandCenterItem({
|
|
29
|
+
id: "open-context-mode",
|
|
30
|
+
title: "Open Context Mode",
|
|
31
|
+
icon: "Gauge",
|
|
32
|
+
keywords: ["context", "savings", "tokens", "knowledge", "search", "doctor", "health"],
|
|
33
|
+
context: "global",
|
|
34
|
+
onSelect({ openSurface }) {
|
|
35
|
+
openSurface("context-mode");
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
return () => {
|
|
41
|
+
for (const cleanup of cleanups.reverse()) cleanup();
|
|
42
|
+
};
|
|
43
|
+
}
|
package/index.server.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
2
|
+
import { createContextModeActionHandlers } from "./server/actions";
|
|
3
|
+
import { createContextModeAnalyticsHandler } from "./server/analytics";
|
|
4
|
+
import { resolveContextModeBinary } from "./server/binary";
|
|
5
|
+
import { createContextModeHandlers } from "./server/handlers";
|
|
6
|
+
import { injectContextModeEnvironment, injectContextModeOnCreate } from "./server/integration";
|
|
7
|
+
import { createContextModeKnowledgeHandlers } from "./server/knowledge";
|
|
8
|
+
import {
|
|
9
|
+
contextModeSettings,
|
|
10
|
+
fetchAndIndexContextMode,
|
|
11
|
+
getContextModeAnalyticsDashboard,
|
|
12
|
+
getContextModeDoctor,
|
|
13
|
+
getContextModeDoctorReport,
|
|
14
|
+
getContextModeInstallAction,
|
|
15
|
+
getContextModeIntegrationAudit,
|
|
16
|
+
getContextModeStats,
|
|
17
|
+
getContextModeStatus,
|
|
18
|
+
getContextModeUpgradeAction,
|
|
19
|
+
indexContextModePath,
|
|
20
|
+
purgeContextModeKnowledge,
|
|
21
|
+
searchContextModeKnowledge,
|
|
22
|
+
} from "./shared";
|
|
23
|
+
|
|
24
|
+
export default function contribute(server: PluginServerContext) {
|
|
25
|
+
const settings = server.registerSettings(contextModeSettings);
|
|
26
|
+
const handlers = createContextModeHandlers(settings);
|
|
27
|
+
const knowledgeHandlers = createContextModeKnowledgeHandlers(settings);
|
|
28
|
+
const actionHandlers = createContextModeActionHandlers(settings);
|
|
29
|
+
server.handle(getContextModeStatus, handlers.status);
|
|
30
|
+
server.handle(getContextModeDoctor, handlers.doctor);
|
|
31
|
+
server.handle(getContextModeStats, handlers.stats);
|
|
32
|
+
server.handle(getContextModeIntegrationAudit, handlers.audit);
|
|
33
|
+
server.handle(getContextModeAnalyticsDashboard, createContextModeAnalyticsHandler());
|
|
34
|
+
server.handle(searchContextModeKnowledge, knowledgeHandlers.search);
|
|
35
|
+
server.handle(indexContextModePath, knowledgeHandlers.indexPath);
|
|
36
|
+
server.handle(fetchAndIndexContextMode, knowledgeHandlers.fetchAndIndex);
|
|
37
|
+
server.handle(purgeContextModeKnowledge, knowledgeHandlers.purge);
|
|
38
|
+
server.handle(getContextModeDoctorReport, actionHandlers.doctor);
|
|
39
|
+
server.handle(getContextModeInstallAction, actionHandlers.install);
|
|
40
|
+
server.handle(getContextModeUpgradeAction, actionHandlers.upgrade);
|
|
41
|
+
server.before("agent.create", async ({ request }) => {
|
|
42
|
+
const current = await settings.read();
|
|
43
|
+
if (current.status !== "ready") return request;
|
|
44
|
+
const binary = await resolveContextModeBinary(current.values);
|
|
45
|
+
if (binary.state !== "found") return request;
|
|
46
|
+
return injectContextModeOnCreate(request, current.values, binary.launch);
|
|
47
|
+
});
|
|
48
|
+
server.before("agent.session_open", async ({ request }) => {
|
|
49
|
+
const current = await settings.read();
|
|
50
|
+
if (current.status !== "ready") return request;
|
|
51
|
+
return injectContextModeEnvironment(request, current.values);
|
|
52
|
+
});
|
|
53
|
+
return () => {};
|
|
54
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@omercnet/paseo-context-mode",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Provider-aware Context Mode health, savings, and MCP activation for Paseo.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Omer Cohen",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/omercnet/paseo-plugins.git",
|
|
11
|
+
"directory": "context-mode"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/omercnet/paseo-plugins/tree/main/context-mode#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/omercnet/paseo-plugins/issues"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"packageManager": "npm@11.19.1",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=24"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"paseo",
|
|
26
|
+
"paseo-plugin",
|
|
27
|
+
"context-mode",
|
|
28
|
+
"context-window",
|
|
29
|
+
"developer-tools"
|
|
30
|
+
],
|
|
31
|
+
"files": [
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"NOTICE",
|
|
34
|
+
"README.md",
|
|
35
|
+
"index.client.tsx",
|
|
36
|
+
"index.server.ts",
|
|
37
|
+
"client",
|
|
38
|
+
"server",
|
|
39
|
+
"shared",
|
|
40
|
+
"paseo-plugin.json"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"check": "biome check .",
|
|
44
|
+
"check:write": "biome check --write .",
|
|
45
|
+
"lint": "biome check .",
|
|
46
|
+
"format:check": "biome check .",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:unit": "vitest run",
|
|
49
|
+
"test:coverage": "vitest run --coverage --coverage.reporter=text --coverage.reporter=lcov --coverage.thresholds.lines=90 --coverage.thresholds.functions=85",
|
|
50
|
+
"typecheck": "tsc --noEmit -p tsconfig.client.json && tsc --noEmit -p tsconfig.server.json",
|
|
51
|
+
"verify:package": "node scripts/verify-package.mjs"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@biomejs/biome": "^2.5.10",
|
|
55
|
+
"@getpaseo/cli": "0.9.0-beta.1",
|
|
56
|
+
"@getpaseo/client": "0.9.0-beta.1",
|
|
57
|
+
"@getpaseo/plugin": "0.9.0-beta.1",
|
|
58
|
+
"@getpaseo/protocol": "0.9.0-beta.1",
|
|
59
|
+
"@tanstack/react-query": "^5.102.3",
|
|
60
|
+
"@types/node": "^24.5.2",
|
|
61
|
+
"@types/react": "^19.2.18",
|
|
62
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
63
|
+
"react": "19.1.0",
|
|
64
|
+
"react-native": "0.81.5",
|
|
65
|
+
"typescript": "^7.0.2",
|
|
66
|
+
"vitest": "^5.0.0",
|
|
67
|
+
"zod": "^4.4.3"
|
|
68
|
+
},
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"context-mode": "1.0.169"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { PluginSettings } from "@getpaseo/plugin/server";
|
|
2
|
+
import {
|
|
3
|
+
CONTEXT_MODE_TARGET_VERSION,
|
|
4
|
+
type ContextModeAction,
|
|
5
|
+
type ContextModeActionInput,
|
|
6
|
+
type DoctorReport,
|
|
7
|
+
type DoctorStatusRow,
|
|
8
|
+
} from "../shared/actions";
|
|
9
|
+
import type { ContextModeSettings, contextModeSettings } from "../shared/settings";
|
|
10
|
+
import { type BinaryResolution, type LaunchDescriptor, resolveContextModeBinary } from "./binary";
|
|
11
|
+
import { contextModeEnvironmentFor, contextModePlatformFor } from "./knowledge";
|
|
12
|
+
import {
|
|
13
|
+
callContextModeTool,
|
|
14
|
+
inspectContextMode,
|
|
15
|
+
type McpInspection,
|
|
16
|
+
type McpProcessDependencies,
|
|
17
|
+
type McpToolArguments,
|
|
18
|
+
} from "./mcp-process";
|
|
19
|
+
|
|
20
|
+
type SettingsHandle = PluginSettings<typeof contextModeSettings.schema>;
|
|
21
|
+
|
|
22
|
+
type ActionToolCaller = (
|
|
23
|
+
launch: LaunchDescriptor,
|
|
24
|
+
name: "ctx_doctor",
|
|
25
|
+
arguments_: McpToolArguments,
|
|
26
|
+
dependencies: McpProcessDependencies,
|
|
27
|
+
) => Promise<string>;
|
|
28
|
+
|
|
29
|
+
export interface ActionHandlerDependencies {
|
|
30
|
+
osPlatform?: NodeJS.Platform;
|
|
31
|
+
resolveBinary?: (settings: ContextModeSettings) => Promise<BinaryResolution>;
|
|
32
|
+
inspect?: (
|
|
33
|
+
launch: LaunchDescriptor,
|
|
34
|
+
dependencies?: McpProcessDependencies,
|
|
35
|
+
) => Promise<McpInspection>;
|
|
36
|
+
callTool?: ActionToolCaller;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ContextModeActionHandlers {
|
|
40
|
+
doctor(input: ContextModeActionInput): Promise<DoctorReport>;
|
|
41
|
+
install(input: ContextModeActionInput): Promise<ContextModeAction>;
|
|
42
|
+
upgrade(input: ContextModeActionInput): Promise<ContextModeAction>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function parseContextModeDoctorOutput(rawOutput: string): DoctorStatusRow[] {
|
|
46
|
+
const rows: DoctorStatusRow[] = [];
|
|
47
|
+
for (const line of rawOutput.split(/\r?\n/)) {
|
|
48
|
+
const match = /^\[(OK|WARN|FAIL)\]\s+(.+)$/.exec(line.trim());
|
|
49
|
+
if (!match) continue;
|
|
50
|
+
const body = match[2] ?? "";
|
|
51
|
+
const separator = body.indexOf(":");
|
|
52
|
+
const check = (separator < 0 ? body : body.slice(0, separator)).trim().slice(0, 256);
|
|
53
|
+
if (!check) continue;
|
|
54
|
+
const detail = (separator < 0 ? "" : body.slice(separator + 1).trim()).slice(0, 4_096);
|
|
55
|
+
rows.push({
|
|
56
|
+
status: match[1] === "OK" ? "ok" : match[1] === "WARN" ? "warning" : "error",
|
|
57
|
+
check,
|
|
58
|
+
detail,
|
|
59
|
+
});
|
|
60
|
+
if (rows.length === 64) break;
|
|
61
|
+
}
|
|
62
|
+
return rows;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createContextModeActionHandlers(
|
|
66
|
+
settingsHandle: SettingsHandle,
|
|
67
|
+
dependencies: ActionHandlerDependencies = {},
|
|
68
|
+
): ContextModeActionHandlers {
|
|
69
|
+
const resolveBinary = dependencies.resolveBinary ?? resolveContextModeBinary;
|
|
70
|
+
const inspect = dependencies.inspect ?? inspectContextMode;
|
|
71
|
+
const callTool = dependencies.callTool ?? callContextModeTool;
|
|
72
|
+
const osPlatform = dependencies.osPlatform ?? process.platform;
|
|
73
|
+
|
|
74
|
+
async function resolvedBinary(): Promise<Extract<BinaryResolution, { state: "found" }>> {
|
|
75
|
+
const settings = await settingsHandle.read();
|
|
76
|
+
if (settings.status !== "ready") {
|
|
77
|
+
throw new Error(`Context Mode settings are invalid: ${settings.error}`.slice(0, 4_096));
|
|
78
|
+
}
|
|
79
|
+
const binary = await resolveBinary(settings.values);
|
|
80
|
+
if (binary.state === "missing") throw new Error(binary.message.slice(0, 4_096));
|
|
81
|
+
return binary;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function currentVersion(input: ContextModeActionInput): Promise<string | null> {
|
|
85
|
+
try {
|
|
86
|
+
const binary = await resolvedBinary();
|
|
87
|
+
const inspection = await inspect(binary.launch, {
|
|
88
|
+
env: contextModeEnvironmentFor(input.provider),
|
|
89
|
+
});
|
|
90
|
+
return inspection.version?.slice(0, 128) ?? null;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
async doctor(input) {
|
|
98
|
+
const binary = await resolvedBinary();
|
|
99
|
+
const rawOutput = await callTool(
|
|
100
|
+
binary.launch,
|
|
101
|
+
"ctx_doctor",
|
|
102
|
+
{},
|
|
103
|
+
{
|
|
104
|
+
env: contextModeEnvironmentFor(input.provider),
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
return {
|
|
108
|
+
platform: contextModePlatformFor(input.provider),
|
|
109
|
+
rows: parseContextModeDoctorOutput(rawOutput),
|
|
110
|
+
rawOutput,
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
async install(input) {
|
|
114
|
+
const platform = contextModePlatformFor(input.provider);
|
|
115
|
+
return {
|
|
116
|
+
program: osPlatform === "win32" ? "npm.cmd" : "npm",
|
|
117
|
+
args: [
|
|
118
|
+
"install",
|
|
119
|
+
"-g",
|
|
120
|
+
`context-mode@${CONTEXT_MODE_TARGET_VERSION}`,
|
|
121
|
+
"--no-audit",
|
|
122
|
+
"--no-fund",
|
|
123
|
+
],
|
|
124
|
+
currentVersion: await currentVersion(input),
|
|
125
|
+
targetVersion: CONTEXT_MODE_TARGET_VERSION,
|
|
126
|
+
platform,
|
|
127
|
+
requiresRestart: true,
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
async upgrade(input) {
|
|
131
|
+
const platform = contextModePlatformFor(input.provider);
|
|
132
|
+
const binary = await resolvedBinary();
|
|
133
|
+
const inspection = await inspect(binary.launch, {
|
|
134
|
+
env: contextModeEnvironmentFor(input.provider),
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
program: binary.launch.program,
|
|
138
|
+
args: [...binary.launch.args, "upgrade", "--platform", platform],
|
|
139
|
+
currentVersion: inspection.version?.slice(0, 128) ?? null,
|
|
140
|
+
targetVersion: "latest",
|
|
141
|
+
platform,
|
|
142
|
+
requiresRestart: true,
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|