@rahularya01/pi-essentials 0.1.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/README.md +324 -0
- package/examples/mcp.json +30 -0
- package/examples/pi-essentials.json +32 -0
- package/examples/pi-settings.json +5 -0
- package/package.json +88 -0
- package/skills/pi-essentials/SKILL.md +50 -0
- package/src/config.ts +351 -0
- package/src/errors.ts +96 -0
- package/src/index.ts +43 -0
- package/src/mcp/commands.ts +390 -0
- package/src/mcp/config.ts +157 -0
- package/src/mcp/credential-store.ts +153 -0
- package/src/mcp/index.ts +67 -0
- package/src/mcp/manager.ts +941 -0
- package/src/mcp/oauth.ts +262 -0
- package/src/mcp/proxy-tool.ts +213 -0
- package/src/mcp/render.ts +164 -0
- package/src/mcp/types.ts +63 -0
- package/src/paths.ts +48 -0
- package/src/questions/ask.ts +134 -0
- package/src/questions/index.ts +72 -0
- package/src/questions/render.ts +69 -0
- package/src/questions/validate.ts +85 -0
- package/src/security/env.ts +132 -0
- package/src/security/limits.ts +20 -0
- package/src/security/ssrf.ts +237 -0
- package/src/subagents/activity.ts +132 -0
- package/src/subagents/builtins/oracle.md +11 -0
- package/src/subagents/builtins/reviewer.md +11 -0
- package/src/subagents/builtins/scout.md +12 -0
- package/src/subagents/builtins/worker.md +11 -0
- package/src/subagents/discover.ts +54 -0
- package/src/subagents/herdr.ts +150 -0
- package/src/subagents/index.ts +642 -0
- package/src/subagents/inspector-tail.d.mts +1 -0
- package/src/subagents/inspector-tail.mjs +140 -0
- package/src/subagents/render.ts +464 -0
- package/src/subagents/runner.ts +468 -0
- package/src/subagents/schema.ts +107 -0
- package/src/subagents/types.ts +131 -0
- package/src/subagents/worktree.ts +131 -0
- package/src/todos/index.ts +170 -0
- package/src/todos/render.ts +198 -0
- package/src/todos/state.ts +310 -0
- package/src/ui/render.ts +215 -0
- package/src/web/activity.ts +91 -0
- package/src/web/cache.ts +153 -0
- package/src/web/extract.ts +75 -0
- package/src/web/fetch.ts +167 -0
- package/src/web/html-to-markdown.ts +284 -0
- package/src/web/http.ts +238 -0
- package/src/web/index.ts +214 -0
- package/src/web/providers/brave.ts +27 -0
- package/src/web/providers/duckduckgo.ts +60 -0
- package/src/web/providers/exa.ts +29 -0
- package/src/web/providers/jina.ts +25 -0
- package/src/web/providers/searxng.ts +29 -0
- package/src/web/providers/tavily.ts +31 -0
- package/src/web/providers/types.ts +75 -0
- package/src/web/render.ts +130 -0
- package/src/web/search.ts +108 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { ensureDir, writePrivateFile } from "../config.ts";
|
|
4
|
+
import { getOAuthStorePath, getPiEssentialsDir } from "../paths.ts";
|
|
5
|
+
|
|
6
|
+
const KEYRING_SERVICE = "pi-essentials-mcp-oauth";
|
|
7
|
+
const PROBE_TIMEOUT_MS = 3_000;
|
|
8
|
+
|
|
9
|
+
export interface CredentialStore {
|
|
10
|
+
readonly backend: "keyring" | "file";
|
|
11
|
+
get(account: string): Promise<string | undefined>;
|
|
12
|
+
set(account: string, value: string): Promise<void>;
|
|
13
|
+
delete(account: string): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function accountId(rawKey: string): string {
|
|
17
|
+
return crypto.createHash("sha256").update(rawKey).digest("hex");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface FileRecords {
|
|
21
|
+
records: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function loadFileRecords(): FileRecords {
|
|
25
|
+
const filePath = getOAuthStorePath();
|
|
26
|
+
if (!fs.existsSync(filePath)) return { records: {} };
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as FileRecords;
|
|
29
|
+
if (!parsed.records || typeof parsed.records !== "object") return { records: {} };
|
|
30
|
+
return parsed;
|
|
31
|
+
} catch {
|
|
32
|
+
return { records: {} };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function saveFileRecords(data: FileRecords): void {
|
|
37
|
+
ensureDir(getPiEssentialsDir());
|
|
38
|
+
writePrivateFile(getOAuthStorePath(), `${JSON.stringify(data, null, 2)}\n`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Plaintext file (mode 0600), keyed by the raw un-hashed storage key -- unchanged on-disk shape. */
|
|
42
|
+
export function createFileStore(): CredentialStore {
|
|
43
|
+
return {
|
|
44
|
+
backend: "file",
|
|
45
|
+
async get(account) {
|
|
46
|
+
const raw = loadFileRecords().records[account];
|
|
47
|
+
if (raw === undefined) return undefined;
|
|
48
|
+
// A record written by the previous (pre credential-store) version of this
|
|
49
|
+
// file stored the object directly rather than a JSON string; tolerate it.
|
|
50
|
+
return typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
51
|
+
},
|
|
52
|
+
async set(account, value) {
|
|
53
|
+
const data = loadFileRecords();
|
|
54
|
+
data.records[account] = value;
|
|
55
|
+
saveFileRecords(data);
|
|
56
|
+
},
|
|
57
|
+
async delete(account) {
|
|
58
|
+
const data = loadFileRecords();
|
|
59
|
+
if (!(account in data.records)) return;
|
|
60
|
+
delete data.records[account];
|
|
61
|
+
saveFileRecords(data);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
67
|
+
let timer: NodeJS.Timeout;
|
|
68
|
+
const timeout = new Promise<never>((_resolve, reject) => {
|
|
69
|
+
timer = setTimeout(() => reject(new Error("timed out")), ms);
|
|
70
|
+
timer.unref?.();
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
return await Promise.race([promise, timeout]);
|
|
74
|
+
} finally {
|
|
75
|
+
clearTimeout(timer!);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** OS credential store (macOS Keychain, Windows Credential Manager, Linux Secret Service). */
|
|
80
|
+
export function createKeyringStore(): CredentialStore {
|
|
81
|
+
return {
|
|
82
|
+
backend: "keyring",
|
|
83
|
+
async get(account) {
|
|
84
|
+
const { AsyncEntry } = await import("@napi-rs/keyring");
|
|
85
|
+
try {
|
|
86
|
+
return (await new AsyncEntry(KEYRING_SERVICE, account).getPassword()) ?? undefined;
|
|
87
|
+
} catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
async set(account, value) {
|
|
92
|
+
const { AsyncEntry } = await import("@napi-rs/keyring");
|
|
93
|
+
await new AsyncEntry(KEYRING_SERVICE, account).setPassword(value);
|
|
94
|
+
},
|
|
95
|
+
async delete(account) {
|
|
96
|
+
const { AsyncEntry } = await import("@napi-rs/keyring");
|
|
97
|
+
try {
|
|
98
|
+
await new AsyncEntry(KEYRING_SERVICE, account).deletePassword();
|
|
99
|
+
} catch {
|
|
100
|
+
// Already absent, or the store rejected the delete; there is nothing more to do.
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Round-trips a canary value; any failure (missing binary, locked/absent daemon) means "unavailable". */
|
|
107
|
+
async function probeKeyring(): Promise<boolean> {
|
|
108
|
+
try {
|
|
109
|
+
const { AsyncEntry } = await import("@napi-rs/keyring");
|
|
110
|
+
const probe = new AsyncEntry(KEYRING_SERVICE, "__pi_essentials_probe__");
|
|
111
|
+
await withTimeout(probe.setPassword("ok"), PROBE_TIMEOUT_MS);
|
|
112
|
+
await withTimeout(probe.deletePassword(), PROBE_TIMEOUT_MS).catch(() => undefined);
|
|
113
|
+
return true;
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ResolvedCredentialStore {
|
|
120
|
+
store: CredentialStore;
|
|
121
|
+
/** Set only when falling back to the plaintext file because no OS credential store is available. */
|
|
122
|
+
warning?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let resolved: Promise<ResolvedCredentialStore> | undefined;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Prefer the OS credential store; fall back to the existing 0600 plaintext file
|
|
129
|
+
* when none is available (no daemon, headless Linux without Secret Service,
|
|
130
|
+
* platform without a supported backend). Unlike a hard fail-closed policy, this
|
|
131
|
+
* keeps OAuth working everywhere pi-essentials already worked, while upgrading
|
|
132
|
+
* to the more secure backend wherever one exists. Resolved once per process.
|
|
133
|
+
*/
|
|
134
|
+
export function resolveCredentialStore(): Promise<ResolvedCredentialStore> {
|
|
135
|
+
if (!resolved) {
|
|
136
|
+
resolved = probeKeyring().then((available) =>
|
|
137
|
+
available
|
|
138
|
+
? { store: createKeyringStore() }
|
|
139
|
+
: {
|
|
140
|
+
store: createFileStore(),
|
|
141
|
+
warning:
|
|
142
|
+
"No OS credential store is available (keychain/Credential Manager/Secret Service); " +
|
|
143
|
+
"MCP OAuth tokens are stored in a local file instead (mode 0600).",
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return resolved;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Test-only: forget the resolved backend so the next call re-probes. */
|
|
151
|
+
export function resetCredentialStoreForTests(): void {
|
|
152
|
+
resolved = undefined;
|
|
153
|
+
}
|
package/src/mcp/index.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ResolvedConfig } from "../config.ts";
|
|
3
|
+
import { registerMcpCommands } from "./commands.ts";
|
|
4
|
+
import { McpManager } from "./manager.ts";
|
|
5
|
+
import { registerMcpTool } from "./proxy-tool.ts";
|
|
6
|
+
import { mcpStatusText } from "./render.ts";
|
|
7
|
+
|
|
8
|
+
/** How often the footer status re-reads server state while a session is open. */
|
|
9
|
+
const STATUS_POLL_MS = 4000;
|
|
10
|
+
|
|
11
|
+
export function registerMcp(pi: ExtensionAPI, config: ResolvedConfig): void {
|
|
12
|
+
const manager = new McpManager(process.cwd(), config);
|
|
13
|
+
let statusTimer: NodeJS.Timeout | undefined;
|
|
14
|
+
let lastStatus: string | undefined;
|
|
15
|
+
let lastCtx: ExtensionContext | undefined;
|
|
16
|
+
|
|
17
|
+
const refreshStatus = (ctx: ExtensionContext) => {
|
|
18
|
+
if (!ctx.hasUI) return;
|
|
19
|
+
const text = mcpStatusText(manager.listServers());
|
|
20
|
+
if (text === lastStatus) return;
|
|
21
|
+
lastStatus = text;
|
|
22
|
+
ctx.ui.setStatus("pi-essentials-mcp", text);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
registerMcpTool(pi, manager, refreshStatus);
|
|
26
|
+
registerMcpCommands(pi, manager, refreshStatus);
|
|
27
|
+
|
|
28
|
+
// authStart() returns immediately and finishes OAuth in the background; surface
|
|
29
|
+
// that completion (or failure) proactively instead of leaving it silent until
|
|
30
|
+
// the next unrelated status check.
|
|
31
|
+
manager.onAuthUpdate((_server, message) => {
|
|
32
|
+
if (!lastCtx?.hasUI) return;
|
|
33
|
+
lastCtx.ui.notify(message, "info");
|
|
34
|
+
refreshStatus(lastCtx);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
let warned = false;
|
|
38
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
39
|
+
lastCtx = ctx;
|
|
40
|
+
manager.reset();
|
|
41
|
+
if (!warned && manager.warnings.length > 0) {
|
|
42
|
+
warned = true;
|
|
43
|
+
for (const warning of manager.warnings) {
|
|
44
|
+
if (ctx.hasUI) ctx.ui.notify(`pi-essentials: ${warning}`, "warning");
|
|
45
|
+
else console.warn(`[pi-essentials] ${warning}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Only eager/keep-alive servers connect here; lazy servers wait for first use.
|
|
49
|
+
await manager.ensureMetadata().catch(() => undefined);
|
|
50
|
+
refreshStatus(ctx);
|
|
51
|
+
|
|
52
|
+
// Idle disconnects and OAuth expiry change status without a tool call, so
|
|
53
|
+
// the footer polls rather than relying only on explicit refreshes.
|
|
54
|
+
if (ctx.hasUI && !statusTimer) {
|
|
55
|
+
statusTimer = setInterval(() => refreshStatus(ctx), STATUS_POLL_MS);
|
|
56
|
+
statusTimer.unref?.();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
61
|
+
if (statusTimer) clearInterval(statusTimer);
|
|
62
|
+
statusTimer = undefined;
|
|
63
|
+
if (ctx.hasUI) ctx.ui.setStatus("pi-essentials-mcp", undefined);
|
|
64
|
+
lastCtx = undefined;
|
|
65
|
+
await manager.shutdown().catch(() => undefined);
|
|
66
|
+
});
|
|
67
|
+
}
|