@askthew/mcp-plugin 0.2.8 → 0.4.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/README.md +49 -11
- package/dist/cli.js +148 -10
- package/dist/index.d.ts +23 -11
- package/dist/index.js +731 -95
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +349 -0
- package/dist/install.d.ts +16 -1
- package/dist/install.js +16 -8
- package/dist/install.test.d.ts +1 -0
- package/dist/install.test.js +237 -0
- package/dist/lib/auth-magic-link.d.ts +22 -0
- package/dist/lib/auth-magic-link.js +43 -0
- package/dist/lib/free-tier-policy.d.ts +19 -0
- package/dist/lib/free-tier-policy.js +53 -0
- package/dist/lib/local-store.d.ts +99 -0
- package/dist/lib/local-store.js +423 -0
- package/dist/lib/loopback-auth.d.ts +8 -0
- package/dist/lib/loopback-auth.js +30 -0
- package/dist/lib/paths.d.ts +7 -0
- package/dist/lib/paths.js +44 -0
- package/dist/lib/telemetry.d.ts +25 -0
- package/dist/lib/telemetry.js +133 -0
- package/dist/lib/tip-engine.d.ts +18 -0
- package/dist/lib/tip-engine.js +237 -0
- package/dist/lib/upgrade-nudge.d.ts +19 -0
- package/dist/lib/upgrade-nudge.js +30 -0
- package/dist/lib/upgrade-sync.d.ts +38 -0
- package/dist/lib/upgrade-sync.js +60 -0
- package/dist/local-store.test.d.ts +1 -0
- package/dist/local-store.test.js +37 -0
- package/dist/scope.d.ts +0 -1
- package/dist/scope.js +0 -6
- package/dist/scope.test.d.ts +1 -0
- package/dist/scope.test.js +32 -0
- package/dist/tip-engine.test.d.ts +1 -0
- package/dist/tip-engine.test.js +51 -0
- package/package.json +7 -10
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { codingSessionSignalSchema, createAskTheWMcpServer, normalizeInstallTokenInput, redactCodingSessionSignal, redactProvenanceSignal, } from "./index.js";
|
|
6
|
+
test("install token normalization accepts copied shell-quoted values", () => {
|
|
7
|
+
assert.equal(normalizeInstallTokenInput("'atw_mcp_token'"), "atw_mcp_token");
|
|
8
|
+
assert.equal(normalizeInstallTokenInput('"atw_mcp_token"'), "atw_mcp_token");
|
|
9
|
+
assert.equal(normalizeInstallTokenInput(" atw_mcp_token "), "atw_mcp_token");
|
|
10
|
+
});
|
|
11
|
+
test("coding session signal schema accepts compact checkpoints", () => {
|
|
12
|
+
const parsed = codingSessionSignalSchema.parse({
|
|
13
|
+
sessionId: "session-1",
|
|
14
|
+
sequence: 2,
|
|
15
|
+
kind: "direction_change",
|
|
16
|
+
summary: "User chose app-level inference and capture-only connector behavior.",
|
|
17
|
+
evidence: [{ role: "user", excerpt: "the app does many of it" }],
|
|
18
|
+
filesTouched: ["packages/mcp-plugin/src/index.ts"],
|
|
19
|
+
commandsRun: ["npm test --workspace @askthew/mcp-plugin"],
|
|
20
|
+
metadata: { hostType: "codex" },
|
|
21
|
+
});
|
|
22
|
+
assert.equal(parsed.kind, "direction_change");
|
|
23
|
+
assert.equal(parsed.evidence[0]?.role, "user");
|
|
24
|
+
});
|
|
25
|
+
test("coding session signal redaction removes obvious secrets", () => {
|
|
26
|
+
const redacted = redactCodingSessionSignal({
|
|
27
|
+
sessionId: "session-1",
|
|
28
|
+
sequence: 3,
|
|
29
|
+
kind: "verification_result",
|
|
30
|
+
summary: "Ran command with sk_live_1234567890abcdefghijklmnop",
|
|
31
|
+
evidence: [{ role: "assistant", excerpt: "Token eyJabcdefghijklmnopqrstuv.abcdefghijklmnopqrstuv.abcdefghijklmnopqrstuv" }],
|
|
32
|
+
filesTouched: [],
|
|
33
|
+
commandsRun: ["curl -H 'Authorization: Bearer AKIA1234567890ABCDEF'"],
|
|
34
|
+
metadata: { nested: { token: "sk_test_1234567890abcdefghijklmnop" } },
|
|
35
|
+
});
|
|
36
|
+
assert.match(redacted.summary, /\[REDACTED\]/);
|
|
37
|
+
assert.match(redacted.evidence[0]?.excerpt ?? "", /\[REDACTED\]/);
|
|
38
|
+
assert.match(redacted.commandsRun[0] ?? "", /\[REDACTED\]/);
|
|
39
|
+
assert.deepEqual(redacted.metadata, { nested: { token: "[REDACTED]" } });
|
|
40
|
+
});
|
|
41
|
+
test("redacts ATW hyphen-segmented install tokens in commands", () => {
|
|
42
|
+
const redacted = redactCodingSessionSignal({
|
|
43
|
+
sessionId: "session-1",
|
|
44
|
+
sequence: 4,
|
|
45
|
+
kind: "verification_result",
|
|
46
|
+
summary: "Installed the connector.",
|
|
47
|
+
evidence: [],
|
|
48
|
+
filesTouched: [],
|
|
49
|
+
commandsRun: [
|
|
50
|
+
"npx @askthew/mcp-plugin install --token atw_mcp_aed8621b-1456-4e63-833b-3c98109b6f18_f56c5683abc123ff --host codex",
|
|
51
|
+
],
|
|
52
|
+
metadata: {},
|
|
53
|
+
});
|
|
54
|
+
assert.equal(redacted.commandsRun[0], "npx @askthew/mcp-plugin install --token [REDACTED] --host codex");
|
|
55
|
+
});
|
|
56
|
+
test("strips URL credentials and sensitive query parameters while preserving URL shape", () => {
|
|
57
|
+
const redacted = redactCodingSessionSignal({
|
|
58
|
+
sessionId: "session-1",
|
|
59
|
+
sequence: 5,
|
|
60
|
+
kind: "session_checkpoint",
|
|
61
|
+
summary: "Checked https://admin:pass@db.example.com/app?token=secret123&view=signals",
|
|
62
|
+
evidence: [],
|
|
63
|
+
filesTouched: [],
|
|
64
|
+
commandsRun: [],
|
|
65
|
+
metadata: {},
|
|
66
|
+
});
|
|
67
|
+
assert.equal(redacted.summary, "Checked https://[REDACTED]:[REDACTED]@db.example.com/app?token=[REDACTED]&view=signals");
|
|
68
|
+
});
|
|
69
|
+
test("sanitizes sensitive file paths without touching normal repo paths", () => {
|
|
70
|
+
const redacted = redactCodingSessionSignal({
|
|
71
|
+
sessionId: "session-1",
|
|
72
|
+
sequence: 6,
|
|
73
|
+
kind: "implementation_update",
|
|
74
|
+
summary: "Updated auth routes.",
|
|
75
|
+
evidence: [],
|
|
76
|
+
filesTouched: ["/Users/alice/.ssh/id_rsa", "apps/app/server/auth.ts"],
|
|
77
|
+
commandsRun: [],
|
|
78
|
+
metadata: {},
|
|
79
|
+
});
|
|
80
|
+
assert.deepEqual(redacted.filesTouched, ["[SENSITIVE_PATH]/id_rsa", "apps/app/server/auth.ts"]);
|
|
81
|
+
});
|
|
82
|
+
test("redacts CLI secret flags and preserves non-sensitive flags", () => {
|
|
83
|
+
const redacted = redactCodingSessionSignal({
|
|
84
|
+
sessionId: "session-1",
|
|
85
|
+
sequence: 7,
|
|
86
|
+
kind: "verification_result",
|
|
87
|
+
summary: "Ran setup.",
|
|
88
|
+
evidence: [],
|
|
89
|
+
filesTouched: [],
|
|
90
|
+
commandsRun: ["askthew-mcp install --token supersecret123abc --host codex --password \"secret-value\""],
|
|
91
|
+
metadata: {},
|
|
92
|
+
});
|
|
93
|
+
assert.equal(redacted.commandsRun[0], "askthew-mcp install --token [REDACTED] --host codex --password \"[REDACTED]\"");
|
|
94
|
+
});
|
|
95
|
+
test("redacts high entropy evidence and metadata without applying entropy to summary", () => {
|
|
96
|
+
const generatedSecret = "aZ9xP0vQ1rS2tU3wX4yZ5aB6cD7eF8gH9";
|
|
97
|
+
const redacted = redactCodingSessionSignal({
|
|
98
|
+
sessionId: "session-1",
|
|
99
|
+
sequence: 8,
|
|
100
|
+
kind: "session_checkpoint",
|
|
101
|
+
summary: `Architecture note ${generatedSecret}`,
|
|
102
|
+
evidence: [{ role: "assistant", excerpt: `Saw token ${generatedSecret}` }],
|
|
103
|
+
filesTouched: [],
|
|
104
|
+
commandsRun: [`curl --header X-Test:${generatedSecret}`],
|
|
105
|
+
metadata: { sample: generatedSecret },
|
|
106
|
+
});
|
|
107
|
+
assert.equal(redacted.summary, `Architecture note ${generatedSecret}`);
|
|
108
|
+
assert.equal(redacted.evidence[0]?.excerpt, "Saw token [REDACTED]");
|
|
109
|
+
assert.equal(redacted.commandsRun[0], "curl --header X-Test:[REDACTED]");
|
|
110
|
+
assert.deepEqual(redacted.metadata, { sample: "[REDACTED]" });
|
|
111
|
+
});
|
|
112
|
+
test("redacts PII and payment identifiers", () => {
|
|
113
|
+
const redacted = redactCodingSessionSignal({
|
|
114
|
+
sessionId: "session-1",
|
|
115
|
+
sequence: 9,
|
|
116
|
+
kind: "session_checkpoint",
|
|
117
|
+
summary: "Contact patient@hospital.org, SSN 123-45-6789, phone 415-555-1212, card 4242424242424242.",
|
|
118
|
+
evidence: [],
|
|
119
|
+
filesTouched: [],
|
|
120
|
+
commandsRun: [],
|
|
121
|
+
metadata: {},
|
|
122
|
+
});
|
|
123
|
+
assert.equal(redacted.summary, "Contact [REDACTED], SSN [REDACTED], phone [REDACTED], card [REDACTED].");
|
|
124
|
+
});
|
|
125
|
+
test("preserves raw decision-quality signal language and normal paths", () => {
|
|
126
|
+
const summary = [
|
|
127
|
+
"User approved switching from REST to GraphQL.",
|
|
128
|
+
"User rejected Redis caching approach.",
|
|
129
|
+
"Architecture decision: monorepo over multi-repo.",
|
|
130
|
+
].join(" ");
|
|
131
|
+
const redacted = redactCodingSessionSignal({
|
|
132
|
+
sessionId: "session-1",
|
|
133
|
+
sequence: 10,
|
|
134
|
+
kind: "direction_change",
|
|
135
|
+
summary,
|
|
136
|
+
evidence: [],
|
|
137
|
+
filesTouched: ["apps/app/server/auth.ts"],
|
|
138
|
+
commandsRun: ["askthew-mcp install --host codex"],
|
|
139
|
+
metadata: {},
|
|
140
|
+
});
|
|
141
|
+
assert.equal(redacted.summary, summary);
|
|
142
|
+
assert.deepEqual(redacted.filesTouched, ["apps/app/server/auth.ts"]);
|
|
143
|
+
assert.deepEqual(redacted.commandsRun, ["askthew-mcp install --host codex"]);
|
|
144
|
+
});
|
|
145
|
+
test("provenance metadata is recursively redacted", () => {
|
|
146
|
+
const redacted = redactProvenanceSignal({
|
|
147
|
+
source: "mcp_plugin",
|
|
148
|
+
decision: "Keep raw signal capture.",
|
|
149
|
+
rationale: "Decision V5 owns extraction.",
|
|
150
|
+
confidence: 0.9,
|
|
151
|
+
filesAffected: ["/Users/alice/.env"],
|
|
152
|
+
sessionId: "session-1",
|
|
153
|
+
metadata: {
|
|
154
|
+
nested: {
|
|
155
|
+
url: "https://admin:secret@example.com/path?api_key=secret123",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
assert.deepEqual(redacted.filesAffected, ["[SENSITIVE_PATH]/.env"]);
|
|
160
|
+
assert.equal(redacted.metadata.nested.url, "https://[REDACTED]:[REDACTED]@example.com/path?api_key=[REDACTED]");
|
|
161
|
+
});
|
|
162
|
+
test("happy-path MCP source exposes capture_session_signal and v1 API tools", () => {
|
|
163
|
+
const source = fs.readFileSync(path.resolve(process.cwd(), "src/index.ts"), "utf8");
|
|
164
|
+
for (const toolName of [
|
|
165
|
+
"capture_session_signal",
|
|
166
|
+
"list_decisions",
|
|
167
|
+
"get_decision",
|
|
168
|
+
"create_decision",
|
|
169
|
+
"update_decision",
|
|
170
|
+
"delete_decision",
|
|
171
|
+
"list_outcomes",
|
|
172
|
+
"get_outcome",
|
|
173
|
+
"list_outcome_signals",
|
|
174
|
+
"create_outcome",
|
|
175
|
+
"update_outcome",
|
|
176
|
+
"delete_outcome",
|
|
177
|
+
"get_north_star",
|
|
178
|
+
"update_north_star",
|
|
179
|
+
"list_signals",
|
|
180
|
+
"get_signal",
|
|
181
|
+
]) {
|
|
182
|
+
assert.match(source, new RegExp(`"${toolName}"`));
|
|
183
|
+
}
|
|
184
|
+
assert.doesNotMatch(source, /server\.tool\(\s*"verify_install"/);
|
|
185
|
+
assert.doesNotMatch(source, /server\.tool\(\s*"record_decision"/);
|
|
186
|
+
assert.doesNotMatch(source, /server\.tool\(\s*"infer_decisions"/);
|
|
187
|
+
assert.doesNotMatch(source, /server\.tool\(\s*"get_session_decisions"/);
|
|
188
|
+
assert.doesNotMatch(source, /server\.tool\(\s*"link_outcome"/);
|
|
189
|
+
assert.doesNotMatch(source, /server\.tool\(\s*"get_decision_feed"/);
|
|
190
|
+
});
|
|
191
|
+
test("v1 API MCP tools dispatch to expected HTTP routes with install-token auth", async () => {
|
|
192
|
+
const calls = [];
|
|
193
|
+
const server = createAskTheWMcpServer({
|
|
194
|
+
apiBaseUrl: "https://askthew.example.com",
|
|
195
|
+
sendStartupHeartbeat: false,
|
|
196
|
+
credentials: {
|
|
197
|
+
installToken: "atw_mcp_tools",
|
|
198
|
+
clientId: "codex",
|
|
199
|
+
clientLabel: "Codex",
|
|
200
|
+
serverName: "askthew",
|
|
201
|
+
},
|
|
202
|
+
fetchImpl: (async (url, init) => {
|
|
203
|
+
calls.push({
|
|
204
|
+
url: String(url),
|
|
205
|
+
method: String(init?.method ?? "GET"),
|
|
206
|
+
headers: init?.headers,
|
|
207
|
+
body: init?.body ? JSON.parse(String(init.body)) : undefined,
|
|
208
|
+
});
|
|
209
|
+
return new Response(JSON.stringify({ ok: true, route: String(url), method: init?.method }), {
|
|
210
|
+
status: 200,
|
|
211
|
+
});
|
|
212
|
+
}),
|
|
213
|
+
});
|
|
214
|
+
const tools = server._registeredTools;
|
|
215
|
+
const cases = [
|
|
216
|
+
{ name: "list_decisions", payload: { limit: 5, cursor: "c1" }, method: "GET", path: "/api/decisions?limit=5&cursor=c1" },
|
|
217
|
+
{ name: "get_decision", payload: { id: "d1" }, method: "GET", path: "/api/decisions/d1" },
|
|
218
|
+
{ name: "create_decision", payload: { content: "Adopt Bun" }, method: "POST", path: "/api/decisions" },
|
|
219
|
+
{ name: "update_decision", payload: { id: "d1", headline: "Adopt Bun v2" }, method: "PATCH", path: "/api/decisions/d1" },
|
|
220
|
+
{ name: "delete_decision", payload: { id: "d1", confirmText: "Adopt Bun v2" }, method: "DELETE", path: "/api/decisions/d1" },
|
|
221
|
+
{ name: "list_outcomes", payload: { limit: 10 }, method: "GET", path: "/api/outcomes?limit=10" },
|
|
222
|
+
{ name: "get_outcome", payload: { id: "o1" }, method: "GET", path: "/api/outcomes/o1" },
|
|
223
|
+
{ name: "list_outcome_signals", payload: { id: "o1" }, method: "GET", path: "/api/outcomes/o1/signals" },
|
|
224
|
+
{ name: "create_outcome", payload: { name: "Reduce churn" }, method: "POST", path: "/api/outcomes" },
|
|
225
|
+
{ name: "update_outcome", payload: { id: "o1", summary: "New summary" }, method: "PATCH", path: "/api/outcomes/o1" },
|
|
226
|
+
{ name: "delete_outcome", payload: { id: "o1", confirmText: "Reduce churn" }, method: "DELETE", path: "/api/outcomes/o1" },
|
|
227
|
+
{ name: "get_north_star", payload: {}, method: "GET", path: "/api/north-star" },
|
|
228
|
+
{ name: "update_north_star", payload: { metric: "Active users", current: "10", target: "100", reason: "API smoke" }, method: "POST", path: "/api/north-star" },
|
|
229
|
+
{ name: "list_signals", payload: { limit: 25, cursor: "2026-01-01T00:00:00.000Z" }, method: "GET", path: "/api/signals?limit=25&cursor=2026-01-01T00%3A00%3A00.000Z" },
|
|
230
|
+
{ name: "get_signal", payload: { id: "s1" }, method: "GET", path: "/api/signals/s1" },
|
|
231
|
+
];
|
|
232
|
+
for (const entry of cases) {
|
|
233
|
+
assert.ok(tools[entry.name], `${entry.name} should be registered`);
|
|
234
|
+
await tools[entry.name].handler(entry.payload);
|
|
235
|
+
}
|
|
236
|
+
assert.equal(calls.length, cases.length);
|
|
237
|
+
cases.forEach((entry, index) => {
|
|
238
|
+
const call = calls[index];
|
|
239
|
+
assert.equal(call.method, entry.method);
|
|
240
|
+
assert.equal(call.url, `https://askthew.example.com${entry.path}`);
|
|
241
|
+
assert.deepEqual(call.headers, {
|
|
242
|
+
...(entry.method === "GET" ? {} : { "Content-Type": "application/json" }),
|
|
243
|
+
Authorization: "Bearer atw_mcp_tools",
|
|
244
|
+
});
|
|
245
|
+
if (entry.method !== "GET") {
|
|
246
|
+
assert.equal(call.body.installToken, "atw_mcp_tools");
|
|
247
|
+
assert.equal(call.body.clientId, "codex");
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
test("v1 API MCP tools return server errors as JSON text", async () => {
|
|
252
|
+
const server = createAskTheWMcpServer({
|
|
253
|
+
apiBaseUrl: "https://askthew.example.com",
|
|
254
|
+
sendStartupHeartbeat: false,
|
|
255
|
+
credentials: {
|
|
256
|
+
installToken: "atw_mcp_tools",
|
|
257
|
+
clientId: "codex",
|
|
258
|
+
},
|
|
259
|
+
fetchImpl: (async () => new Response(JSON.stringify({ error: "Nope", code: "nope" }), {
|
|
260
|
+
status: 500,
|
|
261
|
+
})),
|
|
262
|
+
});
|
|
263
|
+
const result = await server._registeredTools.get_signal.handler({ id: "s1" });
|
|
264
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
265
|
+
assert.equal(parsed.ok, false);
|
|
266
|
+
assert.equal(parsed.status, 500);
|
|
267
|
+
assert.equal(parsed.code, "nope");
|
|
268
|
+
});
|
|
269
|
+
test("createAskTheWMcpServer sends startup heartbeat and automated setup signal", async () => {
|
|
270
|
+
const calls = [];
|
|
271
|
+
createAskTheWMcpServer({
|
|
272
|
+
apiBaseUrl: "https://askthew.example.com",
|
|
273
|
+
credentials: {
|
|
274
|
+
installToken: "atw_mcp_startup",
|
|
275
|
+
clientId: "codex",
|
|
276
|
+
clientLabel: "Codex",
|
|
277
|
+
serverName: "askthew",
|
|
278
|
+
hostType: "codex",
|
|
279
|
+
},
|
|
280
|
+
runtimeMetadata: {
|
|
281
|
+
test_scope: "startup",
|
|
282
|
+
},
|
|
283
|
+
fetchImpl: (async (url, init) => {
|
|
284
|
+
calls.push({
|
|
285
|
+
url: String(url),
|
|
286
|
+
body: JSON.parse(String(init?.body ?? "{}")),
|
|
287
|
+
});
|
|
288
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
289
|
+
}),
|
|
290
|
+
});
|
|
291
|
+
for (let attempt = 0; attempt < 20 && calls.length < 2; attempt += 1) {
|
|
292
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
293
|
+
}
|
|
294
|
+
assert.equal(calls.length, 2);
|
|
295
|
+
assert.equal(calls[0]?.url, "https://askthew.example.com/api/connectors/mcp/heartbeat");
|
|
296
|
+
assert.equal(calls[0]?.body.installToken, "atw_mcp_startup");
|
|
297
|
+
assert.equal(calls[0]?.body.clientId, "codex");
|
|
298
|
+
assert.equal(calls[1]?.url, "https://askthew.example.com/api/ingest/mcp");
|
|
299
|
+
assert.equal(calls[1]?.body.installToken, "atw_mcp_startup");
|
|
300
|
+
assert.equal(calls[1]?.body.clientId, "codex");
|
|
301
|
+
const signal = calls[1]?.body.sessionSignal;
|
|
302
|
+
assert.equal(signal.kind, "setup_complete");
|
|
303
|
+
assert.equal(signal.sequence, 0);
|
|
304
|
+
assert.match(signal.sessionId, /^mcp-startup:codex:/);
|
|
305
|
+
assert.equal(signal.metadata.automated, true);
|
|
306
|
+
assert.equal(signal.metadata.operational, true);
|
|
307
|
+
assert.equal(signal.metadata.origin, "mcp_server_startup");
|
|
308
|
+
assert.equal(signal.metadata.test_scope, "startup");
|
|
309
|
+
});
|
|
310
|
+
test("createAskTheWMcpServer accepts hosted connector identity overrides", async () => {
|
|
311
|
+
const calls = [];
|
|
312
|
+
const server = createAskTheWMcpServer({
|
|
313
|
+
apiBaseUrl: "https://askthew.example.com",
|
|
314
|
+
sendStartupHeartbeat: false,
|
|
315
|
+
credentials: {
|
|
316
|
+
installToken: "atw_mcp_remote",
|
|
317
|
+
clientId: "claude_remote",
|
|
318
|
+
clientLabel: "Claude Desktop / Cowork",
|
|
319
|
+
serverName: "askthew_workspace_a",
|
|
320
|
+
},
|
|
321
|
+
runtimeMetadata: {
|
|
322
|
+
connector_mode: "remote_mcp",
|
|
323
|
+
},
|
|
324
|
+
fetchImpl: (async (url, init) => {
|
|
325
|
+
calls.push({
|
|
326
|
+
url: String(url),
|
|
327
|
+
body: JSON.parse(String(init?.body ?? "{}")),
|
|
328
|
+
});
|
|
329
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
330
|
+
}),
|
|
331
|
+
});
|
|
332
|
+
const tool = server._registeredTools.capture_session_signal;
|
|
333
|
+
assert.ok(tool);
|
|
334
|
+
await tool.handler({
|
|
335
|
+
sessionId: "session-remote",
|
|
336
|
+
sequence: 1,
|
|
337
|
+
kind: "setup_complete",
|
|
338
|
+
summary: "Remote connector setup complete.",
|
|
339
|
+
evidence: [],
|
|
340
|
+
filesTouched: [],
|
|
341
|
+
commandsRun: [],
|
|
342
|
+
metadata: {},
|
|
343
|
+
});
|
|
344
|
+
assert.equal(calls[0]?.url, "https://askthew.example.com/api/ingest/mcp");
|
|
345
|
+
assert.equal(calls[0]?.body.installToken, "atw_mcp_remote");
|
|
346
|
+
assert.equal(calls[0]?.body.clientId, "claude_remote");
|
|
347
|
+
assert.equal(calls[0]?.body.clientLabel, "Claude Desktop / Cowork");
|
|
348
|
+
assert.equal(calls[0]?.body.sessionSignal.metadata.connector_mode, "remote_mcp");
|
|
349
|
+
});
|
package/dist/install.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
export type SupportedHostType = "claude_code" | "codex" | "cursor";
|
|
2
2
|
interface HostConfigInput {
|
|
3
3
|
hostType: SupportedHostType;
|
|
4
|
-
token
|
|
4
|
+
token?: string;
|
|
5
5
|
apiUrl: string;
|
|
6
6
|
serverName: string;
|
|
7
7
|
clientId?: string;
|
|
8
8
|
clientLabel?: string;
|
|
9
|
+
free?: boolean;
|
|
9
10
|
}
|
|
10
11
|
interface InstallHostConfigInput extends HostConfigInput {
|
|
11
12
|
dryRun?: boolean;
|
|
@@ -20,6 +21,13 @@ export declare function createServerEntry(input: HostConfigInput): {
|
|
|
20
21
|
command: string;
|
|
21
22
|
args: string[];
|
|
22
23
|
env: {
|
|
24
|
+
ASKTHEW_HOST_TYPE: SupportedHostType;
|
|
25
|
+
ASKTHEW_SERVER_NAME: string;
|
|
26
|
+
ASKTHEW_CLIENT_LABEL?: string | undefined;
|
|
27
|
+
ASKTHEW_CLIENT_ID?: string | undefined;
|
|
28
|
+
ASKTHEW_FREE_MODE: string;
|
|
29
|
+
ASKTHEW_API_URL: string;
|
|
30
|
+
} | {
|
|
23
31
|
ASKTHEW_HOST_TYPE: SupportedHostType;
|
|
24
32
|
ASKTHEW_SERVER_NAME: string;
|
|
25
33
|
ASKTHEW_CLIENT_LABEL?: string | undefined;
|
|
@@ -40,6 +48,13 @@ export declare function createHostConfigSnippet(input: HostConfigInput): {
|
|
|
40
48
|
command: string;
|
|
41
49
|
args: string[];
|
|
42
50
|
env: {
|
|
51
|
+
ASKTHEW_HOST_TYPE: SupportedHostType;
|
|
52
|
+
ASKTHEW_SERVER_NAME: string;
|
|
53
|
+
ASKTHEW_CLIENT_LABEL?: string | undefined;
|
|
54
|
+
ASKTHEW_CLIENT_ID?: string | undefined;
|
|
55
|
+
ASKTHEW_FREE_MODE: string;
|
|
56
|
+
ASKTHEW_API_URL: string;
|
|
57
|
+
} | {
|
|
43
58
|
ASKTHEW_HOST_TYPE: SupportedHostType;
|
|
44
59
|
ASKTHEW_SERVER_NAME: string;
|
|
45
60
|
ASKTHEW_CLIENT_LABEL?: string | undefined;
|
package/dist/install.js
CHANGED
|
@@ -22,8 +22,8 @@ export function createServerEntry(input) {
|
|
|
22
22
|
command: "npx",
|
|
23
23
|
args: ["-y", "--prefer-online", "@askthew/mcp-plugin@latest"],
|
|
24
24
|
env: {
|
|
25
|
-
ASKTHEW_INSTALL_TOKEN: input.token,
|
|
26
25
|
ASKTHEW_API_URL: input.apiUrl,
|
|
26
|
+
...(input.free ? { ASKTHEW_FREE_MODE: "1" } : { ASKTHEW_INSTALL_TOKEN: input.token ?? "" }),
|
|
27
27
|
...(input.clientId ? { ASKTHEW_CLIENT_ID: input.clientId } : {}),
|
|
28
28
|
...(input.clientLabel ? { ASKTHEW_CLIENT_LABEL: input.clientLabel } : {}),
|
|
29
29
|
ASKTHEW_HOST_TYPE: input.hostType,
|
|
@@ -124,7 +124,7 @@ export function mergeHostSettings(input) {
|
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
126
|
export function formatInstallCommand(input) {
|
|
127
|
-
|
|
127
|
+
const parts = [
|
|
128
128
|
"npx",
|
|
129
129
|
"-y",
|
|
130
130
|
"--prefer-online",
|
|
@@ -132,17 +132,22 @@ export function formatInstallCommand(input) {
|
|
|
132
132
|
"install",
|
|
133
133
|
"--host",
|
|
134
134
|
input.hostType,
|
|
135
|
-
"--token",
|
|
136
|
-
JSON.stringify(input.token),
|
|
137
135
|
"--api-url",
|
|
138
136
|
JSON.stringify(input.apiUrl),
|
|
139
137
|
"--server-name",
|
|
140
138
|
JSON.stringify(input.serverName),
|
|
141
|
-
]
|
|
139
|
+
];
|
|
140
|
+
if (input.free) {
|
|
141
|
+
parts.push("--free");
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
parts.splice(7, 0, "--token", JSON.stringify(input.token ?? ""));
|
|
145
|
+
}
|
|
146
|
+
return parts.join(" ");
|
|
142
147
|
}
|
|
143
148
|
export function verificationNextStep(hostType) {
|
|
144
149
|
const hostLabel = hostType === "claude_code" ? "Claude Code" : hostType === "cursor" ? "Cursor" : "Codex";
|
|
145
|
-
return `Refresh Ask The W to confirm the plugin shows Installed. Restart or reload ${hostLabel} if it is already open.
|
|
150
|
+
return `Refresh Ask The W to confirm the plugin shows Installed. Restart or reload ${hostLabel} if it is already open. At the start of every new ${hostLabel} session in this repo, the installed project instructions tell the coding agent to send a setup_complete update before plan mode or exploration. Choose Always allow if ${hostLabel} asks for Ask The W tool permission. list_mcp_resources/list_mcp_resource_templates may be empty for this tool-driven connector and are not failure signals.`;
|
|
146
151
|
}
|
|
147
152
|
export function installHostConfig(input) {
|
|
148
153
|
const settingsPath = resolveSettingsPath({
|
|
@@ -229,6 +234,11 @@ function behaviorInstructions(hostType) {
|
|
|
229
234
|
"",
|
|
230
235
|
`This workspace uses the Ask The W Plugin. When working in this repo with ${hostLabel}, send compact Ask The W updates automatically with the MCP tool \`capture_session_signal\`.`,
|
|
231
236
|
"",
|
|
237
|
+
"Startup update:",
|
|
238
|
+
`- At the start of every new ${hostLabel} session in this repo, before plan mode, exploration, or any normal reply, call \`capture_session_signal\` with \`kind: "setup_complete"\` if the tool is available.`,
|
|
239
|
+
`- If the tool is unavailable, tell the user to restart or reload ${hostLabel}; do not use \`list_mcp_resources\` or \`list_mcp_resource_templates\` as pass/fail checks.`,
|
|
240
|
+
"- If you realize later in the conversation that the startup call was missed, send it immediately with `metadata.recovered_missed_startup=true`.",
|
|
241
|
+
"",
|
|
232
242
|
"Send an update:",
|
|
233
243
|
"- after the user accepts or rejects product, architecture, or implementation direction",
|
|
234
244
|
"- after meaningful implementation changes",
|
|
@@ -238,8 +248,6 @@ function behaviorInstructions(hostType) {
|
|
|
238
248
|
"",
|
|
239
249
|
"Keep updates compact: short summary, minimal evidence excerpts, files touched, commands run, and useful metadata. Do not send full transcripts. Redact obvious secrets before sending.",
|
|
240
250
|
"",
|
|
241
|
-
"If this is the first session after install or reload, immediately send a `setup_complete` update with `capture_session_signal` before continuing. If the host asks for Ask The W tool permission, the user should choose Always allow if available.",
|
|
242
|
-
"",
|
|
243
251
|
ASKTHEW_INSTRUCTIONS_END,
|
|
244
252
|
"",
|
|
245
253
|
].join("\n");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|