@askthew/mcp-plugin 0.4.10 → 0.4.12
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/README.md +43 -179
- package/dist/cli.d.ts +0 -6
- package/dist/cli.js +255 -723
- package/dist/cloud-client.d.ts +80 -0
- package/dist/cloud-client.js +150 -0
- package/dist/index.d.ts +20 -123
- package/dist/index.js +155 -1353
- package/dist/install.d.ts +33 -119
- package/dist/install.js +140 -614
- package/dist/lib/paths.d.ts +2 -2
- package/dist/lib/paths.js +6 -6
- package/dist/outbox.d.ts +32 -0
- package/dist/outbox.js +101 -0
- package/dist/redaction.d.ts +11 -0
- package/dist/redaction.js +52 -0
- package/package.json +2 -2
- package/dist/lib/cli-actions.d.ts +0 -28
- package/dist/lib/cli-actions.js +0 -104
- package/dist/lib/free-install-registration.d.ts +0 -27
- package/dist/lib/free-install-registration.js +0 -52
- package/dist/lib/free-tier-policy.d.ts +0 -22
- package/dist/lib/free-tier-policy.js +0 -52
- package/dist/lib/local-identity.d.ts +0 -44
- package/dist/lib/local-identity.js +0 -81
- package/dist/lib/local-store.d.ts +0 -130
- package/dist/lib/local-store.js +0 -606
- package/dist/lib/loopback-auth.d.ts +0 -8
- package/dist/lib/loopback-auth.js +0 -30
- package/dist/lib/telemetry.d.ts +0 -25
- package/dist/lib/telemetry.js +0 -155
- package/dist/lib/timeline-insights.d.ts +0 -23
- package/dist/lib/timeline-insights.js +0 -115
- package/dist/lib/tip-engine.d.ts +0 -18
- package/dist/lib/tip-engine.js +0 -237
- package/dist/lib/upgrade-nudge.d.ts +0 -19
- package/dist/lib/upgrade-nudge.js +0 -37
- package/dist/lib/upgrade-sync.d.ts +0 -38
- package/dist/lib/upgrade-sync.js +0 -60
package/dist/install.js
CHANGED
|
@@ -2,17 +2,12 @@ import fs from "node:fs";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { askTheWDataDir,
|
|
5
|
+
import { askTheWDataDir, installMetadataPath, writePrivateJson } from "./lib/paths.js";
|
|
6
6
|
import { resolvePluginScope } from "./scope.js";
|
|
7
|
-
export const DEFAULT_FREE_SERVER_NAME = "askthew-free";
|
|
8
|
-
export const DEFAULT_WORKSPACE_SERVER_NAME = "askthew-workspace";
|
|
9
|
-
const LEGACY_DEFAULT_SERVER_NAME = "askthew";
|
|
10
|
-
const ASKTHEW_INSTRUCTIONS_START = "<!-- @askthew/mcp-plugin v1 - managed block, do not hand-edit -->";
|
|
11
|
-
const ASKTHEW_INSTRUCTIONS_END = "<!-- /@askthew/mcp-plugin v1 -->";
|
|
12
|
-
const LEGACY_ASKTHEW_INSTRUCTIONS_START = "<!-- ASKTHEW_PLUGIN_INSTRUCTIONS_START -->";
|
|
13
|
-
const LEGACY_ASKTHEW_INSTRUCTIONS_END = "<!-- ASKTHEW_PLUGIN_INSTRUCTIONS_END -->";
|
|
14
|
-
const INSTALL_RECEIPTS_SCHEMA_VERSION = 1;
|
|
15
7
|
const requirePackageJson = createRequire(import.meta.url);
|
|
8
|
+
const INSTRUCTIONS_START = "<!-- ASKTHEW BEGIN -->";
|
|
9
|
+
const INSTRUCTIONS_END = "<!-- ASKTHEW END -->";
|
|
10
|
+
export const DEFAULT_SERVER_NAME = "askthew";
|
|
16
11
|
function isRecord(value) {
|
|
17
12
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
13
|
}
|
|
@@ -25,12 +20,6 @@ export function packageVersion() {
|
|
|
25
20
|
return "unknown";
|
|
26
21
|
}
|
|
27
22
|
}
|
|
28
|
-
export function defaultServerNameForTier(free) {
|
|
29
|
-
return free ? DEFAULT_FREE_SERVER_NAME : DEFAULT_WORKSPACE_SERVER_NAME;
|
|
30
|
-
}
|
|
31
|
-
function defaultServerNamesToRemove() {
|
|
32
|
-
return [DEFAULT_FREE_SERVER_NAME, DEFAULT_WORKSPACE_SERVER_NAME, LEGACY_DEFAULT_SERVER_NAME];
|
|
33
|
-
}
|
|
34
23
|
function packageSpecFromPin(env = process.env) {
|
|
35
24
|
const pin = env.ASKTHEW_PIN?.trim() || packageVersion();
|
|
36
25
|
if (!pin || pin === "unknown")
|
|
@@ -41,59 +30,35 @@ function packageSpecFromPin(env = process.env) {
|
|
|
41
30
|
}
|
|
42
31
|
export function resolveSettingsPath(input) {
|
|
43
32
|
const homeDirectory = input.homeDirectory ?? os.homedir();
|
|
44
|
-
if (input.hostType === "codex")
|
|
33
|
+
if (input.hostType === "codex")
|
|
45
34
|
return path.join(homeDirectory, ".codex", "config.toml");
|
|
46
|
-
|
|
47
|
-
if (input.hostType === "cursor") {
|
|
35
|
+
if (input.hostType === "cursor")
|
|
48
36
|
return path.join(homeDirectory, ".cursor", "mcp.json");
|
|
49
|
-
}
|
|
50
37
|
return path.join(homeDirectory, ".claude.json");
|
|
51
38
|
}
|
|
52
39
|
export function createServerEntry(input) {
|
|
53
40
|
const scope = resolvePluginScope(input.cwd ?? process.cwd());
|
|
54
|
-
const env = {
|
|
55
|
-
ASKTHEW_API_URL: input.apiUrl,
|
|
56
|
-
...(input.free ? { ASKTHEW_FREE_MODE: "1" } : { ASKTHEW_INSTALL_TOKEN: input.token ?? "" }),
|
|
57
|
-
...(input.clientId ? { ASKTHEW_CLIENT_ID: input.clientId } : {}),
|
|
58
|
-
...(input.clientLabel ? { ASKTHEW_CLIENT_LABEL: input.clientLabel } : {}),
|
|
59
|
-
ASKTHEW_HOST_TYPE: input.hostType,
|
|
60
|
-
ASKTHEW_SERVER_NAME: input.serverName,
|
|
61
|
-
ASKTHEW_REPO_NAME: scope.repoName,
|
|
62
|
-
...(scope.repoRoot ? { ASKTHEW_REPO_ROOT: scope.repoRoot } : {}),
|
|
63
|
-
...(scope.appPath ? { ASKTHEW_APP_PATH: scope.appPath } : {}),
|
|
64
|
-
...(scope.serviceName ? { ASKTHEW_SERVICE_NAME: scope.serviceName } : {}),
|
|
65
|
-
};
|
|
66
41
|
if (input.serverEntrypoint) {
|
|
67
42
|
return {
|
|
68
43
|
command: "node",
|
|
69
44
|
args: [path.resolve(input.serverEntrypoint)],
|
|
70
|
-
env,
|
|
45
|
+
env: serverEnvironment(input, scope),
|
|
71
46
|
};
|
|
72
47
|
}
|
|
73
48
|
return {
|
|
74
49
|
command: "npx",
|
|
75
50
|
args: ["-y", "--package", packageSpecFromPin(), "askthew-mcp"],
|
|
76
|
-
env,
|
|
51
|
+
env: serverEnvironment(input, scope),
|
|
77
52
|
};
|
|
78
53
|
}
|
|
79
|
-
|
|
80
|
-
if (input.hostType === "codex") {
|
|
81
|
-
const toml = createCodexTomlSection(input);
|
|
82
|
-
return {
|
|
83
|
-
settingsPath: resolveSettingsPath({ hostType: input.hostType }),
|
|
84
|
-
snippet: toml,
|
|
85
|
-
json: toml,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
const snippet = {
|
|
89
|
-
mcpServers: {
|
|
90
|
-
[input.serverName]: createServerEntry(input),
|
|
91
|
-
},
|
|
92
|
-
};
|
|
54
|
+
function serverEnvironment(input, scope) {
|
|
93
55
|
return {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
56
|
+
ASKTHEW_API_URL: (input.apiUrl ?? "https://app.askthew.com").replace(/\/+$/, ""),
|
|
57
|
+
ASKTHEW_HOST_TYPE: input.hostType,
|
|
58
|
+
ASKTHEW_REPO_NAME: scope.repoName,
|
|
59
|
+
...(scope.repoRoot ? { ASKTHEW_REPO_ROOT: scope.repoRoot } : {}),
|
|
60
|
+
...(scope.appPath ? { ASKTHEW_APP_PATH: scope.appPath } : {}),
|
|
61
|
+
...(scope.serviceName ? { ASKTHEW_SERVICE_NAME: scope.serviceName } : {}),
|
|
97
62
|
};
|
|
98
63
|
}
|
|
99
64
|
function escapeTomlString(value) {
|
|
@@ -102,104 +67,37 @@ function escapeTomlString(value) {
|
|
|
102
67
|
function tomlKey(value) {
|
|
103
68
|
return /^[A-Za-z0-9_-]+$/.test(value) ? value : escapeTomlString(value);
|
|
104
69
|
}
|
|
105
|
-
function
|
|
70
|
+
function codexTomlSection(input) {
|
|
106
71
|
const entry = createServerEntry(input);
|
|
107
72
|
const args = entry.args.map(escapeTomlString).join(", ");
|
|
108
73
|
const env = Object.entries(entry.env)
|
|
109
74
|
.map(([key, value]) => `${key} = ${escapeTomlString(String(value))}`)
|
|
110
75
|
.join(", ");
|
|
111
76
|
return [
|
|
112
|
-
`[mcp_servers.${tomlKey(input.serverName)}]`,
|
|
77
|
+
`[mcp_servers.${tomlKey(input.serverName ?? DEFAULT_SERVER_NAME)}]`,
|
|
113
78
|
`command = ${escapeTomlString(entry.command)}`,
|
|
114
79
|
`args = [${args}]`,
|
|
115
80
|
`env = { ${env} }`,
|
|
116
81
|
].join("\n");
|
|
117
82
|
}
|
|
118
83
|
function removeCodexTomlServer(content, serverName) {
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
return content.replace(
|
|
123
|
-
}
|
|
124
|
-
function serverNamesForRemoval(serverName) {
|
|
125
|
-
const trimmed = serverName?.trim();
|
|
126
|
-
if (!trimmed)
|
|
127
|
-
return defaultServerNamesToRemove();
|
|
128
|
-
return trimmed === LEGACY_DEFAULT_SERVER_NAME
|
|
129
|
-
? [LEGACY_DEFAULT_SERVER_NAME]
|
|
130
|
-
: [trimmed, LEGACY_DEFAULT_SERVER_NAME];
|
|
84
|
+
const escaped = serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
85
|
+
const quoted = escapeTomlString(serverName).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
86
|
+
const pattern = new RegExp(`\\n?\\[mcp_servers\\.(?:${escaped}|${quoted})\\]\\n[\\s\\S]*?(?=\\n\\[[^\\]]+\\]|$)`, "g");
|
|
87
|
+
return content.replace(pattern, "").trimEnd();
|
|
131
88
|
}
|
|
132
89
|
function mergeCodexSettings(input) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
next = removeCodexTomlServer(next, input.serverName);
|
|
138
|
-
return `${next}${next ? "\n\n" : ""}${createCodexTomlSection(input)}\n`;
|
|
139
|
-
}
|
|
140
|
-
function expandHome(inputPath, homeDirectory = os.homedir()) {
|
|
141
|
-
if (inputPath === "~")
|
|
142
|
-
return homeDirectory;
|
|
143
|
-
if (inputPath.startsWith("~/"))
|
|
144
|
-
return path.join(homeDirectory, inputPath.slice(2));
|
|
145
|
-
return inputPath;
|
|
146
|
-
}
|
|
147
|
-
function pathAliases(inputPath, homeDirectory = os.homedir()) {
|
|
148
|
-
const aliases = new Set();
|
|
149
|
-
const expanded = expandHome(inputPath, homeDirectory);
|
|
150
|
-
const resolved = path.resolve(expanded);
|
|
151
|
-
aliases.add(resolved);
|
|
152
|
-
if (resolved.startsWith("/private/tmp/"))
|
|
153
|
-
aliases.add(`/tmp/${resolved.slice("/private/tmp/".length)}`);
|
|
154
|
-
if (resolved.startsWith("/tmp/"))
|
|
155
|
-
aliases.add(`/private/tmp/${resolved.slice("/tmp/".length)}`);
|
|
156
|
-
try {
|
|
157
|
-
aliases.add(fs.realpathSync.native(resolved));
|
|
158
|
-
}
|
|
159
|
-
catch {
|
|
160
|
-
try {
|
|
161
|
-
aliases.add(fs.realpathSync(resolved));
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
// The project may have been deleted; lexical aliases are still useful.
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return aliases;
|
|
168
|
-
}
|
|
169
|
-
function equivalentProjectPath(left, right, homeDirectory = os.homedir()) {
|
|
170
|
-
const leftAliases = pathAliases(left, homeDirectory);
|
|
171
|
-
const rightAliases = pathAliases(right, homeDirectory);
|
|
172
|
-
for (const alias of leftAliases) {
|
|
173
|
-
if (rightAliases.has(alias))
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
function claudeProjectKeys(input) {
|
|
179
|
-
const homeDirectory = input.homeDirectory ?? os.homedir();
|
|
180
|
-
const cwdAliases = new Set();
|
|
181
|
-
for (const cwd of input.cwds) {
|
|
182
|
-
for (const alias of pathAliases(cwd, homeDirectory)) {
|
|
183
|
-
cwdAliases.add(alias);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
for (const projectKey of Object.keys(input.existingProjects)) {
|
|
187
|
-
if (input.cwds.some((cwd) => equivalentProjectPath(projectKey, cwd, homeDirectory))) {
|
|
188
|
-
cwdAliases.add(projectKey);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return Array.from(cwdAliases);
|
|
90
|
+
const serverName = input.serverName ?? DEFAULT_SERVER_NAME;
|
|
91
|
+
const existing = removeCodexTomlServer(input.existingSettings.trimEnd(), serverName);
|
|
92
|
+
return `${existing}${existing ? "\n\n" : ""}${codexTomlSection(input)}\n`;
|
|
192
93
|
}
|
|
193
94
|
function mergeClaudeCodeSettings(input) {
|
|
194
95
|
const cwd = path.resolve(input.cwd ?? process.cwd());
|
|
96
|
+
const serverName = input.serverName ?? DEFAULT_SERVER_NAME;
|
|
195
97
|
const existingSettings = isRecord(input.existingSettings) ? input.existingSettings : {};
|
|
196
98
|
const existingProjects = isRecord(existingSettings.projects) ? existingSettings.projects : {};
|
|
197
99
|
const existingProject = isRecord(existingProjects[cwd]) ? existingProjects[cwd] : {};
|
|
198
100
|
const existingMcpServers = isRecord(existingProject.mcpServers) ? existingProject.mcpServers : {};
|
|
199
|
-
const nextMcpServers = { ...existingMcpServers };
|
|
200
|
-
if (input.serverName !== LEGACY_DEFAULT_SERVER_NAME && LEGACY_DEFAULT_SERVER_NAME in nextMcpServers) {
|
|
201
|
-
delete nextMcpServers[LEGACY_DEFAULT_SERVER_NAME];
|
|
202
|
-
}
|
|
203
101
|
return {
|
|
204
102
|
...existingSettings,
|
|
205
103
|
projects: {
|
|
@@ -207,58 +105,46 @@ function mergeClaudeCodeSettings(input) {
|
|
|
207
105
|
[cwd]: {
|
|
208
106
|
...existingProject,
|
|
209
107
|
mcpServers: {
|
|
210
|
-
...
|
|
211
|
-
[
|
|
108
|
+
...existingMcpServers,
|
|
109
|
+
[serverName]: createServerEntry(input),
|
|
212
110
|
},
|
|
213
111
|
},
|
|
214
112
|
},
|
|
215
113
|
};
|
|
216
114
|
}
|
|
217
|
-
|
|
115
|
+
function mergeCursorSettings(input) {
|
|
116
|
+
const serverName = input.serverName ?? DEFAULT_SERVER_NAME;
|
|
218
117
|
const existingSettings = isRecord(input.existingSettings) ? input.existingSettings : {};
|
|
219
118
|
const existingMcpServers = isRecord(existingSettings.mcpServers) ? existingSettings.mcpServers : {};
|
|
220
|
-
const nextMcpServers = { ...existingMcpServers };
|
|
221
|
-
if (input.serverName !== LEGACY_DEFAULT_SERVER_NAME && LEGACY_DEFAULT_SERVER_NAME in nextMcpServers) {
|
|
222
|
-
delete nextMcpServers[LEGACY_DEFAULT_SERVER_NAME];
|
|
223
|
-
}
|
|
224
119
|
return {
|
|
225
120
|
...existingSettings,
|
|
226
121
|
mcpServers: {
|
|
227
|
-
...
|
|
228
|
-
[
|
|
122
|
+
...existingMcpServers,
|
|
123
|
+
[serverName]: createServerEntry(input),
|
|
229
124
|
},
|
|
230
125
|
};
|
|
231
126
|
}
|
|
232
|
-
export function
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
"--host",
|
|
242
|
-
input.hostType,
|
|
243
|
-
...(input.free ? ["--free"] : ["--token", JSON.stringify(input.token ?? "")]),
|
|
244
|
-
"--api-url",
|
|
245
|
-
JSON.stringify(input.apiUrl),
|
|
246
|
-
"--server-name",
|
|
247
|
-
JSON.stringify(input.serverName),
|
|
248
|
-
];
|
|
249
|
-
if (input.free) {
|
|
250
|
-
if (input.email) {
|
|
251
|
-
parts.push("--email", JSON.stringify(input.email));
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
if (input.serverEntrypoint) {
|
|
255
|
-
parts.push("--server-entrypoint", JSON.stringify(input.serverEntrypoint));
|
|
127
|
+
export function createHostConfigSnippet(input) {
|
|
128
|
+
if (input.hostType === "codex") {
|
|
129
|
+
const snippet = codexTomlSection(input);
|
|
130
|
+
return {
|
|
131
|
+
settingsPath: resolveSettingsPath({ hostType: input.hostType }),
|
|
132
|
+
snippet,
|
|
133
|
+
json: snippet,
|
|
134
|
+
language: "toml",
|
|
135
|
+
};
|
|
256
136
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
137
|
+
const snippet = {
|
|
138
|
+
mcpServers: {
|
|
139
|
+
[input.serverName ?? DEFAULT_SERVER_NAME]: createServerEntry(input),
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
settingsPath: resolveSettingsPath({ hostType: input.hostType }),
|
|
144
|
+
snippet,
|
|
145
|
+
json: JSON.stringify(snippet, null, 2),
|
|
146
|
+
language: "json",
|
|
147
|
+
};
|
|
262
148
|
}
|
|
263
149
|
export function installHostConfig(input) {
|
|
264
150
|
const settingsPath = resolveSettingsPath({
|
|
@@ -270,11 +156,9 @@ export function installHostConfig(input) {
|
|
|
270
156
|
if (fs.existsSync(settingsPath)) {
|
|
271
157
|
const raw = fs.readFileSync(settingsPath, "utf8");
|
|
272
158
|
existingText = raw;
|
|
273
|
-
if (raw.trim().length > 0) {
|
|
159
|
+
if (raw.trim().length > 0 && input.hostType !== "codex") {
|
|
274
160
|
try {
|
|
275
|
-
|
|
276
|
-
existingSettings = JSON.parse(raw);
|
|
277
|
-
}
|
|
161
|
+
existingSettings = JSON.parse(raw);
|
|
278
162
|
}
|
|
279
163
|
catch (error) {
|
|
280
164
|
const detail = error instanceof Error ? error.message : "Unknown parse failure.";
|
|
@@ -283,28 +167,17 @@ export function installHostConfig(input) {
|
|
|
283
167
|
}
|
|
284
168
|
}
|
|
285
169
|
const hostInput = {
|
|
286
|
-
existingSettings,
|
|
287
170
|
hostType: input.hostType,
|
|
288
|
-
token: input.token,
|
|
289
171
|
apiUrl: input.apiUrl,
|
|
290
|
-
serverName: input.serverName,
|
|
291
|
-
clientId: input.clientId,
|
|
292
|
-
clientLabel: input.clientLabel,
|
|
293
|
-
free: input.free,
|
|
172
|
+
serverName: input.serverName ?? DEFAULT_SERVER_NAME,
|
|
294
173
|
cwd: input.cwd,
|
|
295
174
|
serverEntrypoint: input.serverEntrypoint,
|
|
296
175
|
};
|
|
297
176
|
const json = input.hostType === "codex"
|
|
298
|
-
? mergeCodexSettings({
|
|
299
|
-
...hostInput,
|
|
300
|
-
existingSettings: existingText,
|
|
301
|
-
})
|
|
177
|
+
? mergeCodexSettings({ ...hostInput, existingSettings: existingText })
|
|
302
178
|
: JSON.stringify(input.hostType === "claude_code"
|
|
303
|
-
? mergeClaudeCodeSettings({
|
|
304
|
-
|
|
305
|
-
cwd: input.cwd,
|
|
306
|
-
})
|
|
307
|
-
: mergeHostSettings(hostInput), null, 2);
|
|
179
|
+
? mergeClaudeCodeSettings({ ...hostInput, existingSettings, cwd: input.cwd })
|
|
180
|
+
: mergeCursorSettings({ ...hostInput, existingSettings }), null, 2);
|
|
308
181
|
if (!input.dryRun) {
|
|
309
182
|
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
310
183
|
fs.writeFileSync(settingsPath, json.endsWith("\n") ? json : `${json}\n`, "utf8");
|
|
@@ -313,461 +186,114 @@ export function installHostConfig(input) {
|
|
|
313
186
|
settingsPath,
|
|
314
187
|
json,
|
|
315
188
|
wroteFile: !input.dryRun,
|
|
316
|
-
nextStep: verificationNextStep(input.hostType),
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
function normalizeInstructionPaths(paths) {
|
|
320
|
-
return Array.from(new Set(paths.map((entry) => path.resolve(entry))));
|
|
321
|
-
}
|
|
322
|
-
function normalizeReceipt(receipt) {
|
|
323
|
-
return {
|
|
324
|
-
...receipt,
|
|
325
|
-
settingsPath: path.resolve(receipt.settingsPath),
|
|
326
|
-
cwd: path.resolve(receipt.cwd),
|
|
327
|
-
instructionPaths: normalizeInstructionPaths(receipt.instructionPaths ?? []),
|
|
328
|
-
dataDir: path.resolve(receipt.dataDir || askTheWDataDir()),
|
|
329
|
-
serverEntrypoint: receipt.serverEntrypoint ? path.resolve(receipt.serverEntrypoint) : undefined,
|
|
330
189
|
};
|
|
331
190
|
}
|
|
332
|
-
function receiptKey(receipt) {
|
|
333
|
-
return `${receipt.hostType}\u0000${receipt.serverName}\u0000${path.resolve(receipt.cwd)}`;
|
|
334
|
-
}
|
|
335
|
-
export function readInstallReceipts(env = process.env) {
|
|
336
|
-
const parsed = readJsonFile(installReceiptsPath(env));
|
|
337
|
-
if (!parsed || parsed.schemaVersion !== INSTALL_RECEIPTS_SCHEMA_VERSION || !Array.isArray(parsed.installs)) {
|
|
338
|
-
return [];
|
|
339
|
-
}
|
|
340
|
-
return parsed.installs
|
|
341
|
-
.filter((receipt) => Boolean(receipt?.hostType && receipt.serverName && receipt.settingsPath && receipt.cwd))
|
|
342
|
-
.map(normalizeReceipt);
|
|
343
|
-
}
|
|
344
|
-
export function writeInstallReceipt(receipt, env = process.env) {
|
|
345
|
-
const nextReceipt = normalizeReceipt({
|
|
346
|
-
...receipt,
|
|
347
|
-
dataDir: receipt.dataDir ?? askTheWDataDir(env),
|
|
348
|
-
installedAt: receipt.installedAt ?? new Date().toISOString(),
|
|
349
|
-
});
|
|
350
|
-
const receipts = readInstallReceipts(env).filter((entry) => receiptKey(entry) !== receiptKey(nextReceipt));
|
|
351
|
-
receipts.push(nextReceipt);
|
|
352
|
-
receipts.sort((left, right) => left.installedAt.localeCompare(right.installedAt));
|
|
353
|
-
writePrivateJson(installReceiptsPath(env), {
|
|
354
|
-
schemaVersion: INSTALL_RECEIPTS_SCHEMA_VERSION,
|
|
355
|
-
installs: receipts,
|
|
356
|
-
});
|
|
357
|
-
return nextReceipt;
|
|
358
|
-
}
|
|
359
|
-
export function findInstallReceipts(input, env = process.env) {
|
|
360
|
-
return readInstallReceipts(env).filter((receipt) => receipt.hostType === input.hostType &&
|
|
361
|
-
(!input.serverName || receipt.serverName === input.serverName));
|
|
362
|
-
}
|
|
363
|
-
export function removeInstallReceipts(input, env = process.env) {
|
|
364
|
-
const cwd = input.cwd ? path.resolve(input.cwd) : undefined;
|
|
365
|
-
const receipts = readInstallReceipts(env);
|
|
366
|
-
const next = receipts.filter((receipt) => {
|
|
367
|
-
if (receipt.hostType !== input.hostType)
|
|
368
|
-
return true;
|
|
369
|
-
if (input.serverName && receipt.serverName !== input.serverName)
|
|
370
|
-
return true;
|
|
371
|
-
if (cwd && receipt.cwd !== cwd)
|
|
372
|
-
return true;
|
|
373
|
-
return false;
|
|
374
|
-
});
|
|
375
|
-
writePrivateJson(installReceiptsPath(env), {
|
|
376
|
-
schemaVersion: INSTALL_RECEIPTS_SCHEMA_VERSION,
|
|
377
|
-
installs: next,
|
|
378
|
-
});
|
|
379
|
-
return receipts.length - next.length;
|
|
380
|
-
}
|
|
381
191
|
export function uninstallHostConfig(input) {
|
|
382
192
|
const settingsPath = resolveSettingsPath({
|
|
383
193
|
hostType: input.hostType,
|
|
384
194
|
homeDirectory: input.homeDirectory,
|
|
385
195
|
});
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
let removedServer = false;
|
|
390
|
-
if (fs.existsSync(settingsPath)) {
|
|
391
|
-
foundConfigFile = true;
|
|
392
|
-
const raw = fs.readFileSync(settingsPath, "utf8");
|
|
393
|
-
if (input.hostType === "codex") {
|
|
394
|
-
json = raw;
|
|
395
|
-
for (const serverName of serverNames) {
|
|
396
|
-
const before = json;
|
|
397
|
-
json = removeCodexTomlServer(json, serverName);
|
|
398
|
-
removedServer = removedServer || before !== json;
|
|
399
|
-
}
|
|
400
|
-
json = json ? `${json}\n` : "";
|
|
401
|
-
}
|
|
402
|
-
else {
|
|
403
|
-
const parsed = raw.trim() ? JSON.parse(raw) : {};
|
|
404
|
-
if (input.hostType === "claude_code") {
|
|
405
|
-
const cwdInputs = input.cwds && input.cwds.length > 0 ? input.cwds : [input.cwd ?? process.cwd()];
|
|
406
|
-
const existingProjects = isRecord(parsed.projects) ? parsed.projects : {};
|
|
407
|
-
const nextProjects = { ...existingProjects };
|
|
408
|
-
const projectKeys = claudeProjectKeys({
|
|
409
|
-
existingProjects,
|
|
410
|
-
cwds: cwdInputs,
|
|
411
|
-
homeDirectory: input.homeDirectory,
|
|
412
|
-
});
|
|
413
|
-
for (const cwd of projectKeys) {
|
|
414
|
-
const existingProject = isRecord(existingProjects[cwd]) ? existingProjects[cwd] : {};
|
|
415
|
-
const existingMcpServers = isRecord(existingProject.mcpServers) ? existingProject.mcpServers : {};
|
|
416
|
-
const nextServers = { ...existingMcpServers };
|
|
417
|
-
const beforeCount = Object.keys(nextServers).length;
|
|
418
|
-
for (const serverName of serverNames) {
|
|
419
|
-
delete nextServers[serverName];
|
|
420
|
-
}
|
|
421
|
-
const removedHere = Object.keys(nextServers).length !== beforeCount;
|
|
422
|
-
removedServer = removedServer || removedHere;
|
|
423
|
-
if (removedHere || isRecord(existingProjects[cwd])) {
|
|
424
|
-
nextProjects[cwd] = {
|
|
425
|
-
...existingProject,
|
|
426
|
-
mcpServers: nextServers,
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
json = JSON.stringify({
|
|
431
|
-
...parsed,
|
|
432
|
-
projects: nextProjects,
|
|
433
|
-
}, null, 2);
|
|
434
|
-
}
|
|
435
|
-
else {
|
|
436
|
-
const existingMcpServers = isRecord(parsed.mcpServers) ? parsed.mcpServers : {};
|
|
437
|
-
const nextServers = { ...existingMcpServers };
|
|
438
|
-
const beforeCount = Object.keys(nextServers).length;
|
|
439
|
-
for (const serverName of serverNames) {
|
|
440
|
-
delete nextServers[serverName];
|
|
441
|
-
}
|
|
442
|
-
removedServer = Object.keys(nextServers).length !== beforeCount;
|
|
443
|
-
json = JSON.stringify({ ...parsed, mcpServers: nextServers }, null, 2);
|
|
444
|
-
}
|
|
445
|
-
json = `${json}\n`;
|
|
446
|
-
}
|
|
447
|
-
if (!input.dryRun) {
|
|
448
|
-
fs.writeFileSync(settingsPath, json, "utf8");
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
return {
|
|
452
|
-
settingsPath,
|
|
453
|
-
json,
|
|
454
|
-
removedServerName: serverNames.join(", "),
|
|
455
|
-
removedServerNames: serverNames,
|
|
456
|
-
foundConfigFile,
|
|
457
|
-
removedServer,
|
|
458
|
-
wroteFile: !input.dryRun && foundConfigFile,
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
function updateNpxPackageArgs(args, packageSpec) {
|
|
462
|
-
let changed = false;
|
|
463
|
-
const nextArgs = args.map((arg, index) => {
|
|
464
|
-
if (typeof arg !== "string")
|
|
465
|
-
return arg;
|
|
466
|
-
if (arg === "--package" && typeof args[index + 1] === "string")
|
|
467
|
-
return arg;
|
|
468
|
-
if (index > 0 && args[index - 1] === "--package" && arg.startsWith("@askthew/mcp-plugin@")) {
|
|
469
|
-
changed = changed || arg !== packageSpec;
|
|
470
|
-
return packageSpec;
|
|
471
|
-
}
|
|
472
|
-
if (arg.startsWith("@askthew/mcp-plugin@")) {
|
|
473
|
-
changed = changed || arg !== packageSpec;
|
|
474
|
-
return packageSpec;
|
|
475
|
-
}
|
|
476
|
-
return arg;
|
|
477
|
-
});
|
|
478
|
-
return { args: nextArgs, changed };
|
|
479
|
-
}
|
|
480
|
-
function updateServerEntryPackage(entry, packageSpec) {
|
|
481
|
-
if (!isRecord(entry) || !Array.isArray(entry.args))
|
|
482
|
-
return { entry, changed: false };
|
|
483
|
-
const updated = updateNpxPackageArgs(entry.args, packageSpec);
|
|
484
|
-
if (!updated.changed)
|
|
485
|
-
return { entry, changed: false };
|
|
486
|
-
return {
|
|
487
|
-
entry: {
|
|
488
|
-
...entry,
|
|
489
|
-
args: updated.args,
|
|
490
|
-
},
|
|
491
|
-
changed: true,
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
function updateCodexPackageSpec(input) {
|
|
495
|
-
let changed = false;
|
|
496
|
-
let next = input.content;
|
|
497
|
-
for (const serverName of input.serverNames) {
|
|
498
|
-
const escapedServerName = serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
499
|
-
const quotedServerName = escapeTomlString(serverName).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
500
|
-
const sectionPattern = new RegExp(`(\\n?\\[mcp_servers\\.(?:${escapedServerName}|${quotedServerName})\\]\\n[\\s\\S]*?)(?=\\n\\[[^\\]]+\\]|$)`, "g");
|
|
501
|
-
next = next.replace(sectionPattern, (section) => {
|
|
502
|
-
const updated = section.replace(/@askthew\/mcp-plugin@[^"',\]\s]+/g, input.packageSpec);
|
|
503
|
-
changed = changed || updated !== section;
|
|
504
|
-
return updated;
|
|
505
|
-
});
|
|
196
|
+
const serverName = input.serverName ?? DEFAULT_SERVER_NAME;
|
|
197
|
+
if (!fs.existsSync(settingsPath)) {
|
|
198
|
+
return { settingsPath, removed: false, wroteFile: false };
|
|
506
199
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
upgradedServer = updated.changed;
|
|
527
|
-
if (updated.changed) {
|
|
528
|
-
for (const serverName of serverNames)
|
|
529
|
-
upgradedServerNames.add(serverName);
|
|
530
|
-
}
|
|
200
|
+
const raw = fs.readFileSync(settingsPath, "utf8");
|
|
201
|
+
let next = raw;
|
|
202
|
+
let removed = false;
|
|
203
|
+
if (input.hostType === "codex") {
|
|
204
|
+
next = `${removeCodexTomlServer(raw, serverName)}\n`;
|
|
205
|
+
removed = next !== raw;
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
const parsed = raw.trim() ? JSON.parse(raw) : {};
|
|
209
|
+
if (input.hostType === "claude_code") {
|
|
210
|
+
const cwd = path.resolve(input.cwd ?? process.cwd());
|
|
211
|
+
const projects = isRecord(parsed.projects) ? { ...parsed.projects } : {};
|
|
212
|
+
const project = isRecord(projects[cwd]) ? { ...projects[cwd] } : {};
|
|
213
|
+
const servers = isRecord(project.mcpServers) ? { ...project.mcpServers } : {};
|
|
214
|
+
removed = serverName in servers;
|
|
215
|
+
delete servers[serverName];
|
|
216
|
+
project.mcpServers = servers;
|
|
217
|
+
projects[cwd] = project;
|
|
218
|
+
next = `${JSON.stringify({ ...parsed, projects }, null, 2)}\n`;
|
|
531
219
|
}
|
|
532
220
|
else {
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const projectKeys = claudeProjectKeys({
|
|
538
|
-
existingProjects,
|
|
539
|
-
cwds: cwdInputs,
|
|
540
|
-
homeDirectory: input.homeDirectory,
|
|
541
|
-
});
|
|
542
|
-
const nextProjects = { ...existingProjects };
|
|
543
|
-
for (const cwd of projectKeys) {
|
|
544
|
-
const existingProject = isRecord(existingProjects[cwd]) ? existingProjects[cwd] : {};
|
|
545
|
-
const existingMcpServers = isRecord(existingProject.mcpServers) ? existingProject.mcpServers : {};
|
|
546
|
-
const nextServers = { ...existingMcpServers };
|
|
547
|
-
let touchedProject = false;
|
|
548
|
-
for (const serverName of serverNames) {
|
|
549
|
-
const updated = updateServerEntryPackage(nextServers[serverName], packageSpec);
|
|
550
|
-
if (updated.changed) {
|
|
551
|
-
nextServers[serverName] = updated.entry;
|
|
552
|
-
upgradedServer = true;
|
|
553
|
-
touchedProject = true;
|
|
554
|
-
upgradedServerNames.add(serverName);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
if (touchedProject) {
|
|
558
|
-
nextProjects[cwd] = {
|
|
559
|
-
...existingProject,
|
|
560
|
-
mcpServers: nextServers,
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
json = JSON.stringify({ ...parsed, projects: nextProjects }, null, 2);
|
|
565
|
-
}
|
|
566
|
-
else {
|
|
567
|
-
const existingMcpServers = isRecord(parsed.mcpServers) ? parsed.mcpServers : {};
|
|
568
|
-
const nextServers = { ...existingMcpServers };
|
|
569
|
-
for (const serverName of serverNames) {
|
|
570
|
-
const updated = updateServerEntryPackage(nextServers[serverName], packageSpec);
|
|
571
|
-
if (updated.changed) {
|
|
572
|
-
nextServers[serverName] = updated.entry;
|
|
573
|
-
upgradedServer = true;
|
|
574
|
-
upgradedServerNames.add(serverName);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
json = JSON.stringify({ ...parsed, mcpServers: nextServers }, null, 2);
|
|
578
|
-
}
|
|
579
|
-
json = `${json}\n`;
|
|
580
|
-
}
|
|
581
|
-
if (!input.dryRun && upgradedServer) {
|
|
582
|
-
fs.writeFileSync(settingsPath, json.endsWith("\n") ? json : `${json}\n`, "utf8");
|
|
221
|
+
const servers = isRecord(parsed.mcpServers) ? { ...parsed.mcpServers } : {};
|
|
222
|
+
removed = serverName in servers;
|
|
223
|
+
delete servers[serverName];
|
|
224
|
+
next = `${JSON.stringify({ ...parsed, mcpServers: servers }, null, 2)}\n`;
|
|
583
225
|
}
|
|
584
226
|
}
|
|
585
|
-
|
|
586
|
-
settingsPath,
|
|
587
|
-
|
|
588
|
-
packageSpec,
|
|
589
|
-
foundConfigFile,
|
|
590
|
-
upgradedServer,
|
|
591
|
-
upgradedServerNames: Array.from(upgradedServerNames),
|
|
592
|
-
wroteFile: !input.dryRun && foundConfigFile && upgradedServer,
|
|
593
|
-
};
|
|
227
|
+
if (!input.dryRun)
|
|
228
|
+
fs.writeFileSync(settingsPath, next, "utf8");
|
|
229
|
+
return { settingsPath, removed, wroteFile: !input.dryRun };
|
|
594
230
|
}
|
|
595
|
-
export
|
|
596
|
-
|
|
597
|
-
const scope = resolvePluginScope(input.cwd ?? process.cwd());
|
|
598
|
-
const apiUrl = input.apiUrl.replace(/\/$/, "");
|
|
599
|
-
const response = await fetcher(`${apiUrl}/api/connectors/mcp/heartbeat`, {
|
|
600
|
-
method: "POST",
|
|
601
|
-
headers: {
|
|
602
|
-
"Content-Type": "application/json",
|
|
603
|
-
},
|
|
604
|
-
body: JSON.stringify({
|
|
605
|
-
installToken: input.token,
|
|
606
|
-
clientId: input.clientId || input.hostType,
|
|
607
|
-
clientLabel: input.clientLabel,
|
|
608
|
-
hostType: input.hostType,
|
|
609
|
-
serverName: input.serverName,
|
|
610
|
-
repoName: scope.repoName,
|
|
611
|
-
...(scope.repoRoot ? { repoRoot: scope.repoRoot } : {}),
|
|
612
|
-
...(scope.appPath ? { appPath: scope.appPath } : {}),
|
|
613
|
-
...(scope.serviceName ? { serviceName: scope.serviceName } : {}),
|
|
614
|
-
}),
|
|
615
|
-
});
|
|
616
|
-
return response.ok;
|
|
617
|
-
}
|
|
618
|
-
function detectStackGuidance(cwd) {
|
|
619
|
-
const packagePath = path.join(cwd, "package.json");
|
|
620
|
-
if (!fs.existsSync(packagePath)) {
|
|
621
|
-
return [];
|
|
622
|
-
}
|
|
623
|
-
let manifest = {};
|
|
624
|
-
try {
|
|
625
|
-
manifest = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
626
|
-
}
|
|
627
|
-
catch {
|
|
628
|
-
return [];
|
|
629
|
-
}
|
|
630
|
-
const deps = {
|
|
631
|
-
...(manifest.dependencies ?? {}),
|
|
632
|
-
...(manifest.devDependencies ?? {}),
|
|
633
|
-
};
|
|
634
|
-
const names = new Set(Object.keys(deps));
|
|
635
|
-
const guidance = [];
|
|
636
|
-
if (names.has("next")) {
|
|
637
|
-
guidance.push("- Next.js detected: after changing route handlers, server actions, middleware, or cache behavior, capture `verification_result` with the command/result.");
|
|
638
|
-
}
|
|
639
|
-
if (names.has("express") || names.has("@types/express")) {
|
|
640
|
-
guidance.push("- Express detected: after changing middleware, request validation, or response envelopes, capture `verification_result` with the command/result.");
|
|
641
|
-
}
|
|
642
|
-
if (names.has("vite") || names.has("@vitejs/plugin-react")) {
|
|
643
|
-
guidance.push("- Vite detected: after changing client entrypoints, env handling, or build config, capture `verification_result` with the command/result.");
|
|
644
|
-
}
|
|
645
|
-
if (names.has("openai") || names.has("@openai/agents") || names.has("@ai-sdk/openai")) {
|
|
646
|
-
guidance.push("- OpenAI SDK detected: after editing prompts, model calls, streaming/SSE, or schema-validated responses, capture `direction_change` for prompt choices and `verification_result` for checked outputs.");
|
|
647
|
-
}
|
|
648
|
-
return guidance.slice(0, 4);
|
|
231
|
+
export function instructionFileForHost(hostType, cwd = process.cwd()) {
|
|
232
|
+
return path.join(path.resolve(cwd), hostType === "claude_code" ? "CLAUDE.md" : "AGENTS.md");
|
|
649
233
|
}
|
|
650
|
-
function
|
|
651
|
-
const hostLabel = hostType === "claude_code" ? "Claude Code" : hostType === "
|
|
652
|
-
const stackGuidance = detectStackGuidance(cwd);
|
|
234
|
+
export function installBehaviorInstructions(input) {
|
|
235
|
+
const hostLabel = input.hostType === "claude_code" ? "Claude Code" : input.hostType === "codex" ? "Codex" : "Cursor";
|
|
653
236
|
return [
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
"## Ask The W Plugin",
|
|
237
|
+
INSTRUCTIONS_START,
|
|
238
|
+
`Ask The W is installed for ${hostLabel}.`,
|
|
657
239
|
"",
|
|
658
|
-
|
|
659
|
-
"",
|
|
660
|
-
"
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
"",
|
|
665
|
-
"Schema:",
|
|
666
|
-
"- `sessionId` (string, required): stable across one session. Derive as `<repo-name>-<YYYYMMDD>-<git-HEAD-short>`. Reuse for every call in the session.",
|
|
667
|
-
"- `sequence` (integer >= 0, required): start at 0, increment by 1 per call within the same `sessionId`.",
|
|
668
|
-
"- `kind` (enum, required): one of `setup_complete`, `session_checkpoint`, `direction_change`, `implementation_update`, `verification_result`, `final_summary`.",
|
|
669
|
-
"- `summary` (string <= 2000, required).",
|
|
670
|
-
"- `evidence`, `filesTouched`, `commandsRun`, `metadata`: optional.",
|
|
671
|
-
"",
|
|
672
|
-
"Example:",
|
|
673
|
-
'{ "sessionId": "thesisengine-20260508-a1b2c3d", "sequence": 0, "kind": "setup_complete", "summary": "..." }',
|
|
674
|
-
"",
|
|
675
|
-
"Send an update:",
|
|
676
|
-
"- after the user accepts or rejects product, architecture, or implementation direction",
|
|
677
|
-
"- before using tools that write files, after meaningful implementation changes",
|
|
678
|
-
"- after tests, builds, lint, or other verification",
|
|
679
|
-
"- at the final summary",
|
|
680
|
-
...(stackGuidance.length > 0 ? ["", "Stack-specific nudges:", ...stackGuidance] : []),
|
|
681
|
-
"",
|
|
682
|
-
"Keep updates compact: short summary, minimal evidence excerpts, files touched, commands run, and useful metadata. Do not send full transcripts. Redact obvious secrets in evidence excerpts (commands, file paths, log lines). Server-side redaction (AWS, Stripe, GitHub, JWT, PEM, OpenAI/Anthropic, DSNs, emails, SSN) runs as a safety net, but agent-side redaction is still preferred.",
|
|
683
|
-
"",
|
|
684
|
-
ASKTHEW_INSTRUCTIONS_END,
|
|
240
|
+
"- After meaningful work, call `capture_session_signal` with a concise summary, bare evidence, `scopeKey`, and a ULID `idempotencyKey`.",
|
|
241
|
+
"- Capture direction changes, implementation checkpoints, verification results, and final summaries. Aim for 5-15 useful signals per session.",
|
|
242
|
+
"- When the user asks what happened or what changed, call `recap`. When they ask for a next-step nudge, call `coach`.",
|
|
243
|
+
"- When the user makes a durable architecture, API, deployment, or product decision, call `create_decision`.",
|
|
244
|
+
"- Do not send full transcripts. Redact obvious secrets before capture.",
|
|
245
|
+
INSTRUCTIONS_END,
|
|
685
246
|
"",
|
|
686
247
|
].join("\n");
|
|
687
248
|
}
|
|
688
|
-
function
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
"
|
|
694
|
-
"",
|
|
695
|
-
behaviorInstructions("cursor", cwd),
|
|
696
|
-
].join("\n");
|
|
697
|
-
}
|
|
698
|
-
function upsertMarkedBlock(existing, block) {
|
|
699
|
-
for (const [startMarker, endMarker] of [
|
|
700
|
-
[ASKTHEW_INSTRUCTIONS_START, ASKTHEW_INSTRUCTIONS_END],
|
|
701
|
-
[LEGACY_ASKTHEW_INSTRUCTIONS_START, LEGACY_ASKTHEW_INSTRUCTIONS_END],
|
|
702
|
-
]) {
|
|
703
|
-
const startIndex = existing.indexOf(startMarker);
|
|
704
|
-
const endIndex = existing.indexOf(endMarker);
|
|
705
|
-
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex)
|
|
706
|
-
continue;
|
|
707
|
-
const afterEnd = endIndex + endMarker.length;
|
|
708
|
-
return `${existing.slice(0, startIndex).trimEnd()}\n\n${block.trimEnd()}\n${existing.slice(afterEnd).trimStart()}`.trimEnd() + "\n";
|
|
249
|
+
function replaceMarkedBlock(existing, block) {
|
|
250
|
+
const start = existing.indexOf(INSTRUCTIONS_START);
|
|
251
|
+
const end = existing.indexOf(INSTRUCTIONS_END);
|
|
252
|
+
if (start >= 0 && end >= start) {
|
|
253
|
+
const after = end + INSTRUCTIONS_END.length;
|
|
254
|
+
return `${existing.slice(0, start).trimEnd()}\n\n${block}${existing.slice(after).trimStart()}`.trimEnd() + "\n";
|
|
709
255
|
}
|
|
710
|
-
return `${existing.trimEnd()}${existing.trim() ? "\n\n" : ""}${block
|
|
256
|
+
return `${existing.trimEnd()}${existing.trim() ? "\n\n" : ""}${block}`.trimEnd() + "\n";
|
|
711
257
|
}
|
|
712
|
-
export function
|
|
713
|
-
const
|
|
714
|
-
const
|
|
715
|
-
const
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const existing = fs.existsSync(instructionsPath) ? fs.readFileSync(instructionsPath, "utf8") : "";
|
|
720
|
-
const next = upsertMarkedBlock(existing, markdownBlock);
|
|
721
|
-
if (!input.dryRun) {
|
|
722
|
-
fs.mkdirSync(path.dirname(instructionsPath), { recursive: true });
|
|
723
|
-
fs.writeFileSync(instructionsPath, next, "utf8");
|
|
724
|
-
}
|
|
725
|
-
writtenPaths.push(instructionsPath);
|
|
726
|
-
}
|
|
727
|
-
if (input.hostType === "cursor") {
|
|
728
|
-
const cursorPath = path.join(cwd, ".cursor", "rules", "askthew.mdc");
|
|
729
|
-
if (!input.dryRun) {
|
|
730
|
-
fs.mkdirSync(path.dirname(cursorPath), { recursive: true });
|
|
731
|
-
fs.writeFileSync(cursorPath, cursorBehaviorInstructions(cwd), "utf8");
|
|
732
|
-
}
|
|
733
|
-
writtenPaths.push(cursorPath);
|
|
734
|
-
primaryPath = cursorPath;
|
|
258
|
+
export function writeBehaviorInstructions(input) {
|
|
259
|
+
const filePath = instructionFileForHost(input.hostType, input.cwd);
|
|
260
|
+
const block = installBehaviorInstructions(input);
|
|
261
|
+
const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
|
|
262
|
+
const next = replaceMarkedBlock(existing, block);
|
|
263
|
+
if (!input.dryRun) {
|
|
264
|
+
fs.writeFileSync(filePath, next, "utf8");
|
|
735
265
|
}
|
|
736
266
|
return {
|
|
737
|
-
|
|
738
|
-
|
|
267
|
+
filePath,
|
|
268
|
+
text: next,
|
|
739
269
|
wroteFile: !input.dryRun,
|
|
740
|
-
content: markdownBlock,
|
|
741
270
|
};
|
|
742
271
|
}
|
|
743
|
-
export function
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
let next = existing;
|
|
752
|
-
for (const [startMarker, endMarker] of [
|
|
753
|
-
[ASKTHEW_INSTRUCTIONS_START, ASKTHEW_INSTRUCTIONS_END],
|
|
754
|
-
[LEGACY_ASKTHEW_INSTRUCTIONS_START, LEGACY_ASKTHEW_INSTRUCTIONS_END],
|
|
755
|
-
]) {
|
|
756
|
-
next = next.replace(new RegExp(`\\n?${startMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`, "g"), "\n");
|
|
757
|
-
}
|
|
758
|
-
next = next.trimEnd() + "\n";
|
|
759
|
-
if (!input.dryRun)
|
|
760
|
-
fs.writeFileSync(instructionsPath, next, "utf8");
|
|
761
|
-
touchedPaths.push(instructionsPath);
|
|
762
|
-
}
|
|
763
|
-
const cursorPath = path.join(cwd, ".cursor", "rules", "askthew.mdc");
|
|
764
|
-
if (input.hostType === "cursor" && fs.existsSync(cursorPath)) {
|
|
765
|
-
if (!input.dryRun)
|
|
766
|
-
fs.rmSync(cursorPath, { force: true });
|
|
767
|
-
touchedPaths.push(cursorPath);
|
|
768
|
-
}
|
|
769
|
-
return {
|
|
770
|
-
paths: touchedPaths,
|
|
771
|
-
wroteFile: !input.dryRun,
|
|
272
|
+
export function writeInstallMetadata(input) {
|
|
273
|
+
const metadata = {
|
|
274
|
+
host: input.hostType,
|
|
275
|
+
version: packageVersion(),
|
|
276
|
+
api_url: input.apiUrl ?? "https://app.askthew.com",
|
|
277
|
+
tier: input.tier ?? "free",
|
|
278
|
+
workspace_id: input.workspaceId ?? null,
|
|
279
|
+
last_sync: new Date().toISOString(),
|
|
772
280
|
};
|
|
281
|
+
writePrivateJson(installMetadataPath(input.env), metadata);
|
|
282
|
+
return metadata;
|
|
283
|
+
}
|
|
284
|
+
export function formatInstallCommand(input) {
|
|
285
|
+
return [
|
|
286
|
+
"npx",
|
|
287
|
+
"-y",
|
|
288
|
+
"--package",
|
|
289
|
+
"@askthew/mcp-plugin",
|
|
290
|
+
"askthew-mcp",
|
|
291
|
+
"install",
|
|
292
|
+
"--host",
|
|
293
|
+
input.hostType,
|
|
294
|
+
...(input.bindToken ? ["--bind", input.bindToken] : []),
|
|
295
|
+
].join(" ");
|
|
296
|
+
}
|
|
297
|
+
export function dataDirSummary(env = process.env) {
|
|
298
|
+
return askTheWDataDir(env);
|
|
773
299
|
}
|