paseo-smart-session 1.2.5 → 1.2.6
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 +2 -2
- package/mcp.mjs +34 -12
- package/package.json +1 -1
- package/server/install.ts +46 -10
package/README.md
CHANGED
|
@@ -37,13 +37,13 @@ That history remains useful after an individual session ends or a provider cache
|
|
|
37
37
|
Install from npm on Paseo 0.9.0 or newer:
|
|
38
38
|
|
|
39
39
|
```sh
|
|
40
|
-
paseo plugin install npm:paseo-smart-session@1.2.
|
|
40
|
+
paseo plugin install npm:paseo-smart-session@1.2.6
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
Paseo 0.8 can install the same plugin from Git:
|
|
44
44
|
|
|
45
45
|
```sh
|
|
46
|
-
paseo plugin add tomgrin10/paseo-smart-session --ref v1.2.
|
|
46
|
+
paseo plugin add tomgrin10/paseo-smart-session --ref v1.2.6
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
```sh
|
package/mcp.mjs
CHANGED
|
@@ -71,6 +71,12 @@ const log = (...args) => console.error("[smart-session mcp]", ...args);
|
|
|
71
71
|
* installed. On a managed Git install the fallback instead finds the `paseo` CLI
|
|
72
72
|
* on `PATH` and resolves the client from its own installed dependencies — the
|
|
73
73
|
* same package the running daemon was built with.
|
|
74
|
+
*
|
|
75
|
+
* The desktop app is the exception. Its client lives inside `app.asar`, which
|
|
76
|
+
* only Electron can read, and its `paseo` is a shell wrapper with no
|
|
77
|
+
* `node_modules` beside it. `server/install.ts` therefore registers this server
|
|
78
|
+
* to run on the app's own Electron runtime in node mode, and from there the
|
|
79
|
+
* client resolves through `process.resourcesPath`.
|
|
74
80
|
*/
|
|
75
81
|
let daemonClientModule = null;
|
|
76
82
|
|
|
@@ -78,22 +84,38 @@ const CLIENT_SPECIFIER = ["@getpaseo", "client", "internal", "daemon-client"].jo
|
|
|
78
84
|
|
|
79
85
|
async function loadDaemonClient() {
|
|
80
86
|
if (daemonClientModule !== null) return daemonClientModule;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return daemonClientModule;
|
|
84
|
-
} catch (directError) {
|
|
85
|
-
const cliPath = findPaseoCli();
|
|
86
|
-
if (cliPath === null) throw directError;
|
|
87
|
+
const failures = [];
|
|
88
|
+
for (const { label, resolve } of clientCandidates()) {
|
|
87
89
|
try {
|
|
88
|
-
|
|
89
|
-
daemonClientModule = await import(pathToFileURL(resolved).href);
|
|
90
|
+
daemonClientModule = await import(resolve());
|
|
90
91
|
return daemonClientModule;
|
|
91
|
-
} catch (
|
|
92
|
-
|
|
93
|
-
`Cannot resolve ${CLIENT_SPECIFIER} directly (${directError.message}) or via the paseo CLI at ${cliPath} (${viaCliError.message})`,
|
|
94
|
-
);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
failures.push(`${label} (${error.message})`);
|
|
95
94
|
}
|
|
96
95
|
}
|
|
96
|
+
throw new Error(`Cannot resolve ${CLIENT_SPECIFIER} ${failures.join(" or ")}${desktopHint()}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Every place the client can come from, cheapest first. */
|
|
100
|
+
function clientCandidates() {
|
|
101
|
+
const fromEntry = (entry) => () => pathToFileURL(createRequire(entry).resolve(CLIENT_SPECIFIER)).href;
|
|
102
|
+
const candidates = [{ label: "directly", resolve: () => CLIENT_SPECIFIER }];
|
|
103
|
+
// Set only under Electron, where the bundled CLI inside app.asar is readable.
|
|
104
|
+
if (typeof process.resourcesPath === "string") {
|
|
105
|
+
const entry = join(process.resourcesPath, "app.asar", "node_modules", "@getpaseo", "cli", "dist", "index.js");
|
|
106
|
+
candidates.push({ label: `from the Paseo app bundle at ${process.resourcesPath}`, resolve: fromEntry(entry) });
|
|
107
|
+
}
|
|
108
|
+
const cliPath = findPaseoCli();
|
|
109
|
+
if (cliPath !== null) candidates.push({ label: `via the paseo CLI at ${cliPath}`, resolve: fromEntry(cliPath) });
|
|
110
|
+
return candidates;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Explains the one failure a person can fix without reading the code. */
|
|
114
|
+
function desktopHint() {
|
|
115
|
+
if (process.versions.electron !== undefined) return "";
|
|
116
|
+
const cliPath = findPaseoCli();
|
|
117
|
+
if (cliPath === null || !existsSync(join(dirname(cliPath), "..", "app.asar"))) return "";
|
|
118
|
+
return ". The paseo CLI is the desktop app's, whose client only its own runtime can load: reload the smart-session plugin so it re-registers this MCP server on that runtime, then restart the Claude Code session";
|
|
97
119
|
}
|
|
98
120
|
|
|
99
121
|
/** Finds `paseo` on PATH and resolves symlinks, the way a shell's `which` would. */
|
package/package.json
CHANGED
package/server/install.ts
CHANGED
|
@@ -198,6 +198,45 @@ async function writeAtomic(path: string, text: string): Promise<void> {
|
|
|
198
198
|
await rename(temp, path);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
export interface McpDefinition {
|
|
202
|
+
type: "stdio";
|
|
203
|
+
command: string;
|
|
204
|
+
args: string[];
|
|
205
|
+
env?: Record<string, string>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface McpRuntime {
|
|
209
|
+
electron: string | undefined;
|
|
210
|
+
execPath: string;
|
|
211
|
+
appImage: string | undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const currentRuntime = (): McpRuntime => ({
|
|
215
|
+
electron: process.versions.electron,
|
|
216
|
+
execPath: process.execPath,
|
|
217
|
+
appImage: process.env.APPIMAGE,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* How Claude Code should launch `mcp.mjs`.
|
|
222
|
+
*
|
|
223
|
+
* `node` from `PATH`, except under the desktop app. There the daemon client lives
|
|
224
|
+
* inside `app.asar`, which plain Node cannot read, so the server runs on the
|
|
225
|
+
* runtime this plugin is itself running on — Electron in node mode, the same way
|
|
226
|
+
* the app launches its bundled `paseo` CLI. An AppImage mounts at a fresh path on
|
|
227
|
+
* every launch, so its stable `$APPIMAGE` stands in for the mounted executable.
|
|
228
|
+
*/
|
|
229
|
+
export function mcpDefinition(pluginDir: string, runtime: McpRuntime = currentRuntime()): McpDefinition {
|
|
230
|
+
const args = [join(pluginDir, "mcp.mjs")];
|
|
231
|
+
if (runtime.electron === undefined) return { type: "stdio", command: "node", args };
|
|
232
|
+
return {
|
|
233
|
+
type: "stdio",
|
|
234
|
+
command: runtime.appImage ?? runtime.execPath,
|
|
235
|
+
args,
|
|
236
|
+
env: { ELECTRON_RUN_AS_NODE: "1" },
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
201
240
|
/**
|
|
202
241
|
* Registers the agent-facing MCP server, through Claude Code's own CLI.
|
|
203
242
|
*
|
|
@@ -208,34 +247,31 @@ async function writeAtomic(path: string, text: string): Promise<void> {
|
|
|
208
247
|
* install into a new directory, so an entry can exist while its script no longer
|
|
209
248
|
* does.
|
|
210
249
|
*/
|
|
211
|
-
export function mcpPointsAtPlugin(output: string,
|
|
250
|
+
export function mcpPointsAtPlugin(output: string, definition: McpDefinition): boolean {
|
|
212
251
|
const lines = output.split(/\r?\n/).map((line) => line.trim());
|
|
213
252
|
return lines.includes("Scope: User config (available in all your projects)")
|
|
214
|
-
&& lines.includes(
|
|
215
|
-
&& lines.includes(`Args: ${join(
|
|
253
|
+
&& lines.includes(`Command: ${definition.command}`)
|
|
254
|
+
&& lines.includes(`Args: ${definition.args.join(" ")}`)
|
|
255
|
+
&& Object.entries(definition.env ?? {}).every(([key, value]) => lines.includes(`${key}=${value}`));
|
|
216
256
|
}
|
|
217
257
|
|
|
218
258
|
async function ensureMcp(pluginDir: string): Promise<InstallReport["mcp"]> {
|
|
219
259
|
const run = (args: string[]): Promise<{ stdout: string }> =>
|
|
220
260
|
execFileAsync("claude", args, { timeout: 20_000, maxBuffer: 1024 * 1024 });
|
|
261
|
+
const definition = mcpDefinition(pluginDir);
|
|
221
262
|
|
|
222
263
|
let existing = false;
|
|
223
264
|
try {
|
|
224
265
|
const { stdout } = await run(["mcp", "get", "smart-session"]);
|
|
225
|
-
if (mcpPointsAtPlugin(stdout,
|
|
266
|
+
if (mcpPointsAtPlugin(stdout, definition)) return "present";
|
|
226
267
|
existing = true;
|
|
227
268
|
} catch {
|
|
228
269
|
// Absent, or no `claude` on PATH. The add below tells the two apart.
|
|
229
270
|
}
|
|
230
271
|
|
|
231
|
-
const definition = JSON.stringify({
|
|
232
|
-
type: "stdio",
|
|
233
|
-
command: "node",
|
|
234
|
-
args: [join(pluginDir, "mcp.mjs")],
|
|
235
|
-
});
|
|
236
272
|
try {
|
|
237
273
|
if (existing) await run(["mcp", "remove", "smart-session", "--scope", "user"]);
|
|
238
|
-
await run(["mcp", "add-json", "--scope", "user", "smart-session", definition]);
|
|
274
|
+
await run(["mcp", "add-json", "--scope", "user", "smart-session", JSON.stringify(definition)]);
|
|
239
275
|
return existing ? "updated" : "added";
|
|
240
276
|
} catch {
|
|
241
277
|
// No `claude` binary on the daemon's PATH is the usual reason, and it is not
|