@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,237 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { formatInstallCommand, installBehaviorInstructions, installHostConfig, mergeHostSettings, resolveSettingsPath, sendInstallHeartbeat, verificationNextStep, } from "./install.js";
|
|
7
|
+
test("mergeHostSettings preserves unrelated MCP servers and replaces askthew", () => {
|
|
8
|
+
const merged = mergeHostSettings({
|
|
9
|
+
existingSettings: {
|
|
10
|
+
theme: "dark",
|
|
11
|
+
mcpServers: {
|
|
12
|
+
github: {
|
|
13
|
+
command: "github-mcp",
|
|
14
|
+
},
|
|
15
|
+
askthew: {
|
|
16
|
+
command: "old-command",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
hostType: "codex",
|
|
21
|
+
token: "token-123",
|
|
22
|
+
apiUrl: "https://askthew.example.com",
|
|
23
|
+
serverName: "askthew_workspace_a",
|
|
24
|
+
});
|
|
25
|
+
assert.deepEqual(merged.theme, "dark");
|
|
26
|
+
assert.deepEqual(Object.keys(merged.mcpServers ?? {}).sort(), ["askthew_workspace_a", "github"]);
|
|
27
|
+
assert.deepEqual(merged.mcpServers.askthew_workspace_a, {
|
|
28
|
+
command: "npx",
|
|
29
|
+
args: ["-y", "--prefer-online", "@askthew/mcp-plugin@latest"],
|
|
30
|
+
env: {
|
|
31
|
+
ASKTHEW_INSTALL_TOKEN: "token-123",
|
|
32
|
+
ASKTHEW_API_URL: "https://askthew.example.com",
|
|
33
|
+
ASKTHEW_HOST_TYPE: "codex",
|
|
34
|
+
ASKTHEW_SERVER_NAME: "askthew_workspace_a",
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
test("installHostConfig writes Claude Code local MCP settings and stays idempotent", () => {
|
|
39
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-mcp-install-"));
|
|
40
|
+
const tempProject = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-claude-project-"));
|
|
41
|
+
const settingsPath = resolveSettingsPath({
|
|
42
|
+
hostType: "claude_code",
|
|
43
|
+
homeDirectory: tempHome,
|
|
44
|
+
});
|
|
45
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
46
|
+
fs.writeFileSync(settingsPath, JSON.stringify({
|
|
47
|
+
projects: {
|
|
48
|
+
[tempProject]: {
|
|
49
|
+
mcpServers: {
|
|
50
|
+
github: {
|
|
51
|
+
command: "github-mcp",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
}, null, 2), "utf8");
|
|
57
|
+
const first = installHostConfig({
|
|
58
|
+
hostType: "claude_code",
|
|
59
|
+
token: "token-456",
|
|
60
|
+
apiUrl: "https://askthew.example.com",
|
|
61
|
+
serverName: "askthew_workspace_a",
|
|
62
|
+
clientId: "claude_code",
|
|
63
|
+
clientLabel: "Claude Code",
|
|
64
|
+
homeDirectory: tempHome,
|
|
65
|
+
cwd: tempProject,
|
|
66
|
+
});
|
|
67
|
+
const afterFirst = fs.readFileSync(settingsPath, "utf8");
|
|
68
|
+
const second = installHostConfig({
|
|
69
|
+
hostType: "claude_code",
|
|
70
|
+
token: "token-456",
|
|
71
|
+
apiUrl: "https://askthew.example.com",
|
|
72
|
+
serverName: "askthew_workspace_a",
|
|
73
|
+
clientId: "claude_code",
|
|
74
|
+
clientLabel: "Claude Code",
|
|
75
|
+
homeDirectory: tempHome,
|
|
76
|
+
cwd: tempProject,
|
|
77
|
+
});
|
|
78
|
+
const afterSecond = fs.readFileSync(settingsPath, "utf8");
|
|
79
|
+
const parsed = JSON.parse(afterSecond);
|
|
80
|
+
const projectServers = parsed.projects[tempProject].mcpServers;
|
|
81
|
+
assert.equal(first.settingsPath, settingsPath);
|
|
82
|
+
assert.equal(second.settingsPath, settingsPath);
|
|
83
|
+
assert.equal(afterFirst, afterSecond);
|
|
84
|
+
assert.deepEqual(Object.keys(projectServers ?? {}).sort(), ["askthew_workspace_a", "github"]);
|
|
85
|
+
assert.equal(projectServers.askthew_workspace_a.env.ASKTHEW_INSTALL_TOKEN, "token-456");
|
|
86
|
+
assert.equal(projectServers.askthew_workspace_a.env.ASKTHEW_CLIENT_ID, "claude_code");
|
|
87
|
+
assert.equal(projectServers.askthew_workspace_a.env.ASKTHEW_CLIENT_LABEL, "Claude Code");
|
|
88
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
89
|
+
fs.rmSync(tempProject, { recursive: true, force: true });
|
|
90
|
+
});
|
|
91
|
+
test("installHostConfig writes Codex MCP servers to config.toml", () => {
|
|
92
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-mcp-multi-workspace-"));
|
|
93
|
+
const settingsPath = resolveSettingsPath({
|
|
94
|
+
hostType: "codex",
|
|
95
|
+
homeDirectory: tempHome,
|
|
96
|
+
});
|
|
97
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
98
|
+
fs.writeFileSync(settingsPath, [
|
|
99
|
+
'model = "gpt-5"',
|
|
100
|
+
"",
|
|
101
|
+
"[mcp_servers.github]",
|
|
102
|
+
'command = "github-mcp"',
|
|
103
|
+
"",
|
|
104
|
+
].join("\n"), "utf8");
|
|
105
|
+
installHostConfig({
|
|
106
|
+
hostType: "codex",
|
|
107
|
+
token: "token-a",
|
|
108
|
+
apiUrl: "https://askthew.example.com",
|
|
109
|
+
serverName: "askthew_workspace_a",
|
|
110
|
+
homeDirectory: tempHome,
|
|
111
|
+
});
|
|
112
|
+
installHostConfig({
|
|
113
|
+
hostType: "codex",
|
|
114
|
+
token: "token-b",
|
|
115
|
+
apiUrl: "https://askthew.example.com",
|
|
116
|
+
serverName: "askthew_workspace_b",
|
|
117
|
+
homeDirectory: tempHome,
|
|
118
|
+
});
|
|
119
|
+
const toml = fs.readFileSync(settingsPath, "utf8");
|
|
120
|
+
assert.match(toml, /\[mcp_servers\.github\]/);
|
|
121
|
+
assert.match(toml, /\[mcp_servers\.askthew_workspace_a\]/);
|
|
122
|
+
assert.match(toml, /\[mcp_servers\.askthew_workspace_b\]/);
|
|
123
|
+
assert.match(toml, /ASKTHEW_INSTALL_TOKEN = "token-a"/);
|
|
124
|
+
assert.match(toml, /ASKTHEW_INSTALL_TOKEN = "token-b"/);
|
|
125
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
126
|
+
});
|
|
127
|
+
test("installHostConfig dry run returns merged JSON without writing a file", () => {
|
|
128
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-mcp-dry-run-"));
|
|
129
|
+
const settingsPath = resolveSettingsPath({
|
|
130
|
+
hostType: "codex",
|
|
131
|
+
homeDirectory: tempHome,
|
|
132
|
+
});
|
|
133
|
+
const result = installHostConfig({
|
|
134
|
+
hostType: "codex",
|
|
135
|
+
token: "token-789",
|
|
136
|
+
apiUrl: "https://askthew.example.com",
|
|
137
|
+
serverName: "askthew_workspace_a",
|
|
138
|
+
homeDirectory: tempHome,
|
|
139
|
+
dryRun: true,
|
|
140
|
+
});
|
|
141
|
+
assert.equal(result.wroteFile, false);
|
|
142
|
+
assert.equal(fs.existsSync(settingsPath), false);
|
|
143
|
+
assert.match(result.json, /ASKTHEW_INSTALL_TOKEN/);
|
|
144
|
+
assert.match(result.json, /\[mcp_servers\.askthew_workspace_a\]/);
|
|
145
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
146
|
+
});
|
|
147
|
+
test("installHostConfig writes Cursor global mcp.json", () => {
|
|
148
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-cursor-mcp-"));
|
|
149
|
+
const settingsPath = resolveSettingsPath({
|
|
150
|
+
hostType: "cursor",
|
|
151
|
+
homeDirectory: tempHome,
|
|
152
|
+
});
|
|
153
|
+
installHostConfig({
|
|
154
|
+
hostType: "cursor",
|
|
155
|
+
token: "token-cursor",
|
|
156
|
+
apiUrl: "https://askthew.example.com",
|
|
157
|
+
serverName: "askthew",
|
|
158
|
+
homeDirectory: tempHome,
|
|
159
|
+
});
|
|
160
|
+
const parsed = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
161
|
+
assert.deepEqual(Object.keys(parsed.mcpServers ?? {}), ["askthew"]);
|
|
162
|
+
assert.equal(parsed.mcpServers.askthew.command, "npx");
|
|
163
|
+
assert.equal(parsed.mcpServers.askthew.env.ASKTHEW_HOST_TYPE, "cursor");
|
|
164
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
165
|
+
});
|
|
166
|
+
test("formatInstallCommand emits the one-command guided install form", () => {
|
|
167
|
+
const command = formatInstallCommand({
|
|
168
|
+
hostType: "codex",
|
|
169
|
+
token: "token-abc",
|
|
170
|
+
apiUrl: "https://askthew.example.com",
|
|
171
|
+
serverName: "askthew_workspace_a",
|
|
172
|
+
});
|
|
173
|
+
assert.equal(command, 'npx -y --prefer-online @askthew/mcp-plugin@latest install --host codex --token "token-abc" --api-url "https://askthew.example.com" --server-name "askthew_workspace_a"');
|
|
174
|
+
});
|
|
175
|
+
test("verificationNextStep points users to every-session startup capture", () => {
|
|
176
|
+
const nextStep = verificationNextStep("codex");
|
|
177
|
+
assert.match(nextStep, /Refresh Ask The W/);
|
|
178
|
+
assert.match(nextStep, /every new Codex session in this repo/);
|
|
179
|
+
assert.match(nextStep, /before plan mode or exploration/);
|
|
180
|
+
assert.match(nextStep, /list_mcp_resources\/list_mcp_resource_templates may be empty/);
|
|
181
|
+
});
|
|
182
|
+
test("sendInstallHeartbeat pings Ask The W after config install", async () => {
|
|
183
|
+
const calls = [];
|
|
184
|
+
const ok = await sendInstallHeartbeat({
|
|
185
|
+
hostType: "codex",
|
|
186
|
+
token: "token-abc",
|
|
187
|
+
apiUrl: "https://askthew.example.com/",
|
|
188
|
+
serverName: "askthew",
|
|
189
|
+
cwd: process.cwd(),
|
|
190
|
+
fetchImpl: async (url, init) => {
|
|
191
|
+
calls.push({
|
|
192
|
+
url: String(url),
|
|
193
|
+
body: JSON.parse(String(init?.body ?? "{}")),
|
|
194
|
+
});
|
|
195
|
+
return new Response("{}", { status: 200 });
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
assert.equal(ok, true);
|
|
199
|
+
assert.equal(calls[0]?.url, "https://askthew.example.com/api/connectors/mcp/heartbeat");
|
|
200
|
+
assert.equal(calls[0]?.body.installToken, "token-abc");
|
|
201
|
+
assert.equal(calls[0]?.body.hostType, "codex");
|
|
202
|
+
assert.equal(calls[0]?.body.serverName, "askthew");
|
|
203
|
+
assert.equal(calls[0]?.body.clientId, "codex");
|
|
204
|
+
});
|
|
205
|
+
test("installBehaviorInstructions adds persistent Codex tracking rules without clobbering existing instructions", () => {
|
|
206
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-agent-instructions-"));
|
|
207
|
+
const agentsPath = path.join(tempRoot, "AGENTS.md");
|
|
208
|
+
fs.writeFileSync(agentsPath, "# Existing instructions\n\nKeep this section.\n", "utf8");
|
|
209
|
+
const result = installBehaviorInstructions({
|
|
210
|
+
hostType: "codex",
|
|
211
|
+
cwd: tempRoot,
|
|
212
|
+
});
|
|
213
|
+
const contents = fs.readFileSync(agentsPath, "utf8");
|
|
214
|
+
assert.equal(result.path, agentsPath);
|
|
215
|
+
assert.match(contents, /# Existing instructions/);
|
|
216
|
+
assert.match(contents, /Ask The W Plugin/);
|
|
217
|
+
assert.match(contents, /capture_session_signal/);
|
|
218
|
+
assert.match(contents, /At the start of every new Codex session in this repo/);
|
|
219
|
+
assert.match(contents, /before plan mode, exploration, or any normal reply/);
|
|
220
|
+
assert.match(contents, /metadata\.recovered_missed_startup=true/);
|
|
221
|
+
assert.match(contents, /every 8-12 turns/);
|
|
222
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
223
|
+
});
|
|
224
|
+
test("installBehaviorInstructions creates Cursor rule file", () => {
|
|
225
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-cursor-rules-"));
|
|
226
|
+
const result = installBehaviorInstructions({
|
|
227
|
+
hostType: "cursor",
|
|
228
|
+
cwd: tempRoot,
|
|
229
|
+
});
|
|
230
|
+
assert.equal(result.path, path.join(tempRoot, ".cursor", "rules", "askthew.mdc"));
|
|
231
|
+
assert.match(fs.readFileSync(result.path, "utf8"), /alwaysApply: true/);
|
|
232
|
+
assert.match(fs.readFileSync(result.path, "utf8"), /Ask The W Plugin/);
|
|
233
|
+
assert.match(fs.readFileSync(result.path, "utf8"), /At the start of every new Cursor session/);
|
|
234
|
+
assert.match(fs.readFileSync(result.path, "utf8"), /before plan mode, exploration, or any normal reply/);
|
|
235
|
+
assert.match(fs.readFileSync(result.path, "utf8"), /metadata\.recovered_missed_startup=true/);
|
|
236
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
237
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CliCredentials } from "./free-tier-policy.js";
|
|
2
|
+
export interface MagicLinkClientOptions {
|
|
3
|
+
apiUrl?: string;
|
|
4
|
+
fetchImpl?: typeof fetch;
|
|
5
|
+
}
|
|
6
|
+
export declare function requestMagicLinkCode(input: {
|
|
7
|
+
email: string;
|
|
8
|
+
deviceLabel?: string;
|
|
9
|
+
apiUrl?: string;
|
|
10
|
+
fetchImpl?: typeof fetch;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
requestId: string;
|
|
13
|
+
expiresAt: string;
|
|
14
|
+
devCode?: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function verifyMagicLinkCode(input: {
|
|
17
|
+
requestId: string;
|
|
18
|
+
code: string;
|
|
19
|
+
apiUrl?: string;
|
|
20
|
+
fetchImpl?: typeof fetch;
|
|
21
|
+
telemetryOptOut?: boolean;
|
|
22
|
+
}): Promise<CliCredentials>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { credentialsPath, writePrivateJson } from "./paths.js";
|
|
2
|
+
function baseUrl(apiUrl) {
|
|
3
|
+
return (apiUrl?.trim() || process.env.ASKTHEW_API_URL?.trim() || "https://app.askthew.com").replace(/\/$/, "");
|
|
4
|
+
}
|
|
5
|
+
async function requestJson(route, body, options) {
|
|
6
|
+
const fetcher = options.fetchImpl ?? fetch;
|
|
7
|
+
const response = await fetcher(`${baseUrl(options.apiUrl)}${route}`, {
|
|
8
|
+
method: "POST",
|
|
9
|
+
headers: { "Content-Type": "application/json" },
|
|
10
|
+
body: JSON.stringify(body),
|
|
11
|
+
});
|
|
12
|
+
const payload = await response.json().catch(() => null);
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
const message = payload && typeof payload === "object" && "error" in payload
|
|
15
|
+
? String(payload.error)
|
|
16
|
+
: "Ask The W auth request failed.";
|
|
17
|
+
throw new Error(message);
|
|
18
|
+
}
|
|
19
|
+
return payload;
|
|
20
|
+
}
|
|
21
|
+
export async function requestMagicLinkCode(input) {
|
|
22
|
+
return requestJson("/api/cli/v1/magic-link/request", {
|
|
23
|
+
email: input.email,
|
|
24
|
+
deviceLabel: input.deviceLabel,
|
|
25
|
+
}, input);
|
|
26
|
+
}
|
|
27
|
+
export async function verifyMagicLinkCode(input) {
|
|
28
|
+
const verified = await requestJson("/api/cli/v1/magic-link/verify", {
|
|
29
|
+
requestId: input.requestId,
|
|
30
|
+
code: input.code,
|
|
31
|
+
}, input);
|
|
32
|
+
const credentials = {
|
|
33
|
+
email: verified.email,
|
|
34
|
+
userId: verified.userId,
|
|
35
|
+
cliToken: verified.cliToken,
|
|
36
|
+
cliTokenId: verified.cliTokenId,
|
|
37
|
+
accountStatus: verified.accountStatus,
|
|
38
|
+
apiUrl: input.apiUrl,
|
|
39
|
+
telemetryOptOut: input.telemetryOptOut,
|
|
40
|
+
};
|
|
41
|
+
writePrivateJson(credentialsPath(), credentials);
|
|
42
|
+
return credentials;
|
|
43
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type McpMode = "paid" | "free" | "unauthenticated";
|
|
2
|
+
export interface CliCredentials {
|
|
3
|
+
email?: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
cliToken: string;
|
|
6
|
+
cliTokenId: string;
|
|
7
|
+
apiUrl?: string;
|
|
8
|
+
telemetryOptOut?: boolean;
|
|
9
|
+
accountStatus?: "new_dormant" | "existing_active";
|
|
10
|
+
}
|
|
11
|
+
export interface ModeResolution {
|
|
12
|
+
mode: McpMode;
|
|
13
|
+
installToken?: string;
|
|
14
|
+
cliCredentials?: CliCredentials;
|
|
15
|
+
reason: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function loadCliCredentials(env?: NodeJS.ProcessEnv): CliCredentials | null;
|
|
18
|
+
export declare function resolveMcpMode(env?: NodeJS.ProcessEnv): ModeResolution;
|
|
19
|
+
export declare function isTelemetryOptedOut(env?: NodeJS.ProcessEnv, credentials?: CliCredentials | null): boolean;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { credentialsPath, readJsonFile } from "./paths.js";
|
|
3
|
+
function clean(value) {
|
|
4
|
+
return String(value ?? "").trim().replace(/^['"]/, "").replace(/['"]$/, "");
|
|
5
|
+
}
|
|
6
|
+
export function loadCliCredentials(env = process.env) {
|
|
7
|
+
const explicitToken = clean(env.ASKTHEW_CLI_TOKEN);
|
|
8
|
+
if (explicitToken) {
|
|
9
|
+
return {
|
|
10
|
+
userId: clean(env.ASKTHEW_USER_ID) || "local",
|
|
11
|
+
cliToken: explicitToken,
|
|
12
|
+
cliTokenId: clean(env.ASKTHEW_CLI_TOKEN_ID) || "env",
|
|
13
|
+
apiUrl: clean(env.ASKTHEW_API_URL) || undefined,
|
|
14
|
+
telemetryOptOut: env.ASKTHEW_TELEMETRY === "off",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const creds = readJsonFile(credentialsPath(env));
|
|
18
|
+
if (!creds?.cliToken || !creds.userId || !creds.cliTokenId) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return creds;
|
|
22
|
+
}
|
|
23
|
+
export function resolveMcpMode(env = process.env) {
|
|
24
|
+
const installToken = clean(env.ASKTHEW_INSTALL_TOKEN);
|
|
25
|
+
if (installToken) {
|
|
26
|
+
return {
|
|
27
|
+
mode: "paid",
|
|
28
|
+
installToken,
|
|
29
|
+
reason: "workspace_install_token",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const credentials = loadCliCredentials(env);
|
|
33
|
+
if (credentials?.cliToken) {
|
|
34
|
+
return {
|
|
35
|
+
mode: "free",
|
|
36
|
+
cliCredentials: credentials,
|
|
37
|
+
reason: "cli_free_tier_credentials",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (clean(env.ASKTHEW_FREE_MODE) === "1" || clean(env.ASKTHEW_FREE_MODE).toLowerCase() === "true") {
|
|
41
|
+
return {
|
|
42
|
+
mode: "unauthenticated",
|
|
43
|
+
reason: fs.existsSync(credentialsPath(env)) ? "invalid_cli_credentials" : "free_mode_no_credentials",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
mode: "unauthenticated",
|
|
48
|
+
reason: "no_identity",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function isTelemetryOptedOut(env = process.env, credentials = loadCliCredentials(env)) {
|
|
52
|
+
return env.ASKTHEW_TELEMETRY === "off" || credentials?.telemetryOptOut === true;
|
|
53
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export type SignalKind = "setup_complete" | "session_checkpoint" | "direction_change" | "implementation_update" | "verification_result" | "final_summary";
|
|
2
|
+
export type DecisionStatus = "proposed" | "committed" | "shipped" | "abandoned";
|
|
3
|
+
export interface LocalSignalInput {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
sequence: number;
|
|
6
|
+
kind: SignalKind;
|
|
7
|
+
summary: string;
|
|
8
|
+
evidence?: unknown[];
|
|
9
|
+
filesTouched?: string[];
|
|
10
|
+
commandsRun?: string[];
|
|
11
|
+
metadata?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface LocalSignal extends Required<LocalSignalInput> {
|
|
14
|
+
id: number;
|
|
15
|
+
capturedAt: string;
|
|
16
|
+
}
|
|
17
|
+
export interface LocalDecision {
|
|
18
|
+
id: string;
|
|
19
|
+
sessionId: string | null;
|
|
20
|
+
headline: string;
|
|
21
|
+
why: string | null;
|
|
22
|
+
status: DecisionStatus;
|
|
23
|
+
alignment: "aligned" | "orthogonal" | "conflicts" | "ambiguous" | null;
|
|
24
|
+
files: string[];
|
|
25
|
+
sourceSignalIds: number[];
|
|
26
|
+
rawContent: string;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
updatedAt: string;
|
|
29
|
+
uploadedAt: string | null;
|
|
30
|
+
}
|
|
31
|
+
export interface LocalDecisionInput {
|
|
32
|
+
id?: string;
|
|
33
|
+
sessionId?: string | null;
|
|
34
|
+
headline?: string;
|
|
35
|
+
why?: string | null;
|
|
36
|
+
status?: DecisionStatus;
|
|
37
|
+
alignment?: LocalDecision["alignment"];
|
|
38
|
+
files?: string[];
|
|
39
|
+
sourceSignalIds?: number[];
|
|
40
|
+
rawContent: string;
|
|
41
|
+
}
|
|
42
|
+
export interface TelemetryOutboxRow {
|
|
43
|
+
id: number;
|
|
44
|
+
payload: Record<string, unknown>;
|
|
45
|
+
createdAt: string;
|
|
46
|
+
attempts: number;
|
|
47
|
+
lastAttemptAt: string | null;
|
|
48
|
+
deliveredAt: string | null;
|
|
49
|
+
}
|
|
50
|
+
export declare class LocalStore {
|
|
51
|
+
private readonly storePath;
|
|
52
|
+
private data;
|
|
53
|
+
private db;
|
|
54
|
+
private jsonMode;
|
|
55
|
+
private jsonPath;
|
|
56
|
+
private constructor();
|
|
57
|
+
static open(input?: {
|
|
58
|
+
path?: string;
|
|
59
|
+
}): LocalStore;
|
|
60
|
+
get usingJsonFallback(): boolean;
|
|
61
|
+
close(): void;
|
|
62
|
+
migrate(): void;
|
|
63
|
+
getMeta(key: string): string;
|
|
64
|
+
setMeta(key: string, value: string): void;
|
|
65
|
+
insertSignal(input: LocalSignalInput): LocalSignal;
|
|
66
|
+
listSignals(input?: {
|
|
67
|
+
sessionId?: string;
|
|
68
|
+
limit?: number;
|
|
69
|
+
uploaded?: boolean;
|
|
70
|
+
}): LocalSignal[];
|
|
71
|
+
getSignal(id: number): LocalSignal | null;
|
|
72
|
+
mostRecentSessionId(): any;
|
|
73
|
+
createDecision(input: LocalDecisionInput): LocalDecision;
|
|
74
|
+
updateDecision(id: string, patch: Partial<Omit<LocalDecision, "id" | "createdAt">>): LocalDecision | null;
|
|
75
|
+
deleteDecision(id: string): boolean;
|
|
76
|
+
getDecision(id: string): LocalDecision | null;
|
|
77
|
+
listDecisions(input?: {
|
|
78
|
+
status?: DecisionStatus;
|
|
79
|
+
limit?: number;
|
|
80
|
+
since?: string;
|
|
81
|
+
pendingUploadOnly?: boolean;
|
|
82
|
+
}): LocalDecision[];
|
|
83
|
+
enqueueTelemetry(payload: Record<string, unknown>): number;
|
|
84
|
+
listTelemetryOutbox(input?: {
|
|
85
|
+
undeliveredOnly?: boolean;
|
|
86
|
+
limit?: number;
|
|
87
|
+
}): TelemetryOutboxRow[];
|
|
88
|
+
markTelemetryAttempt(id: number, delivered: boolean): void;
|
|
89
|
+
stats(): {
|
|
90
|
+
signals: number;
|
|
91
|
+
decisions: number;
|
|
92
|
+
decisionsByStatus: Record<string, number>;
|
|
93
|
+
lastSessionId: string | null;
|
|
94
|
+
jsonFallback: boolean;
|
|
95
|
+
};
|
|
96
|
+
private openDatabase;
|
|
97
|
+
private persistJson;
|
|
98
|
+
private nextJsonId;
|
|
99
|
+
}
|