@temet/cli 0.2.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 +57 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +513 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @temet/cli
|
|
2
|
+
|
|
3
|
+
Temet CLI for MCP config setup.
|
|
4
|
+
|
|
5
|
+
## Install / Run
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm dlx @temet/cli install-handler
|
|
9
|
+
pnpm dlx @temet/cli connect --address <16hex> --token <token>
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Command
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
temet connect --address <16hex> --token <token> [--config-path <path>] [--relay-url <url>] [--name temet] [--dry-run]
|
|
16
|
+
temet connect-url --url "temet://connect?address=<16hex>&token=<token>&relay=<url>&name=temet" [--config-path <path>] [--name temet] [--relay-url <url>] [--dry-run]
|
|
17
|
+
temet install-handler [--dry-run]
|
|
18
|
+
temet uninstall-handler [--dry-run]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What it does
|
|
22
|
+
|
|
23
|
+
- Creates or updates an MCP config file (default: `.mcp.json`)
|
|
24
|
+
- Upserts a `temet` MCP HTTP server entry
|
|
25
|
+
- Preserves existing MCP server entries
|
|
26
|
+
- Accepts `temet://connect?...` deep links via `connect-url`
|
|
27
|
+
- Installs native `temet://` protocol handler (macOS, Linux, Windows)
|
|
28
|
+
|
|
29
|
+
## Native handler quickstart
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm dlx @temet/cli install-handler
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then use a Quick connect button in Temet UI.
|
|
36
|
+
|
|
37
|
+
To remove:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pnpm dlx @temet/cli uninstall-handler
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Defaults
|
|
44
|
+
|
|
45
|
+
- relay URL: `https://temet-relay.ramponneau.workers.dev/mcp`
|
|
46
|
+
- config path: `./.mcp.json`
|
|
47
|
+
- server name: `temet`
|
|
48
|
+
|
|
49
|
+
## Notes
|
|
50
|
+
|
|
51
|
+
V1 does not handle login or token generation. Get `address` and `token` from Temet Agent Hub.
|
|
52
|
+
|
|
53
|
+
## Release automation
|
|
54
|
+
|
|
55
|
+
- Versioning and changelog are managed by `release-please` for `packages/cli`.
|
|
56
|
+
- npm publication is automated from GitHub Actions when a CLI release is created.
|
|
57
|
+
- Required repo secret: `NPM_TOKEN` (npm token with publish rights for `@temet/cli`).
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const DEFAULT_RELAY_URL = "https://temet-relay.ramponneau.workers.dev/mcp";
|
|
9
|
+
const DEFAULT_SERVER_NAME = "temet";
|
|
10
|
+
const DEFAULT_PROTOCOL_APP_NAME = "Temet Handler";
|
|
11
|
+
const LINUX_DESKTOP_ENTRY = "temet-handler.desktop";
|
|
12
|
+
const LINUX_HANDLER_SCRIPT = "temet-protocol-handler";
|
|
13
|
+
const HELP = `Temet CLI
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
\ttemet connect --address <16hex> --token <token> [--config-path <path>] [--relay-url <url>] [--name temet] [--dry-run]
|
|
17
|
+
\ttemet connect-url --url "temet://connect?address=<16hex>&token=<token>&relay=<url>&name=temet" [--config-path <path>] [--name temet] [--relay-url <url>] [--dry-run]
|
|
18
|
+
\ttemet install-handler [--dry-run]
|
|
19
|
+
\ttemet uninstall-handler [--dry-run]
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
\ttemet connect --address a3f8c2d19f0b7e41 --token <token>
|
|
23
|
+
\ttemet connect-url --url "temet://connect?address=a3f8c2d19f0b7e41&token=<token>&relay=https%3A%2F%2Ftemet-relay.ramponneau.workers.dev%2Fmcp"
|
|
24
|
+
\tpnpm dlx @temet/cli install-handler
|
|
25
|
+
\tpnpm dlx @temet/cli connect-url --url "temet://connect?address=a3f8c2d19f0b7e41&token=<token>"
|
|
26
|
+
`;
|
|
27
|
+
function printHelp(exitCode = 0) {
|
|
28
|
+
console.log(HELP);
|
|
29
|
+
process.exit(exitCode);
|
|
30
|
+
}
|
|
31
|
+
function isObject(value) {
|
|
32
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
function normalizeAddress(raw) {
|
|
35
|
+
const address = raw.trim().toLowerCase();
|
|
36
|
+
if (!/^[a-f0-9]{16}$/i.test(address)) {
|
|
37
|
+
throw new Error("Invalid address. Expected 16 hex chars.");
|
|
38
|
+
}
|
|
39
|
+
return address;
|
|
40
|
+
}
|
|
41
|
+
function parseFlagBag(args) {
|
|
42
|
+
const flags = new Map();
|
|
43
|
+
const positionals = [];
|
|
44
|
+
for (let i = 0; i < args.length; i++) {
|
|
45
|
+
const arg = args[i];
|
|
46
|
+
if (!arg.startsWith("--")) {
|
|
47
|
+
positionals.push(arg);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (arg === "--dry-run") {
|
|
51
|
+
flags.set("dry-run", true);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const key = arg.slice(2);
|
|
55
|
+
const value = args[i + 1];
|
|
56
|
+
if (!value || value.startsWith("--")) {
|
|
57
|
+
throw new Error(`Missing value for --${key}`);
|
|
58
|
+
}
|
|
59
|
+
flags.set(key, value);
|
|
60
|
+
i += 1;
|
|
61
|
+
}
|
|
62
|
+
return { flags, positionals };
|
|
63
|
+
}
|
|
64
|
+
function readOptionalString(flags, key) {
|
|
65
|
+
const value = flags.get(key);
|
|
66
|
+
if (typeof value !== "string")
|
|
67
|
+
return null;
|
|
68
|
+
const trimmed = value.trim();
|
|
69
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
70
|
+
}
|
|
71
|
+
function parseConnectOptions(flags) {
|
|
72
|
+
const address = normalizeAddress(String(flags.get("address") ?? ""));
|
|
73
|
+
const token = String(flags.get("token") ?? "").trim();
|
|
74
|
+
const configPath = resolve(String(flags.get("config-path") ?? ".mcp.json").trim());
|
|
75
|
+
const relayUrl = String(flags.get("relay-url") ?? DEFAULT_RELAY_URL).trim();
|
|
76
|
+
const serverName = String(flags.get("name") ?? DEFAULT_SERVER_NAME).trim();
|
|
77
|
+
const dryRun = Boolean(flags.get("dry-run"));
|
|
78
|
+
if (token.length === 0) {
|
|
79
|
+
throw new Error("Missing --token.");
|
|
80
|
+
}
|
|
81
|
+
if (serverName.length === 0) {
|
|
82
|
+
throw new Error("Invalid --name.");
|
|
83
|
+
}
|
|
84
|
+
if (relayUrl.length === 0) {
|
|
85
|
+
throw new Error("Invalid --relay-url.");
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
address,
|
|
89
|
+
token,
|
|
90
|
+
configPath,
|
|
91
|
+
relayUrl,
|
|
92
|
+
serverName,
|
|
93
|
+
dryRun,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function parseTemetDeepLink(rawUrl) {
|
|
97
|
+
let url;
|
|
98
|
+
try {
|
|
99
|
+
url = new URL(rawUrl);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
throw new Error("Invalid --url. Expected a valid temet:// URL.");
|
|
103
|
+
}
|
|
104
|
+
if (url.protocol !== "temet:") {
|
|
105
|
+
throw new Error("Invalid deep link protocol. Expected temet://");
|
|
106
|
+
}
|
|
107
|
+
const route = `${url.hostname}${url.pathname}`
|
|
108
|
+
.replace(/^\/+/, "")
|
|
109
|
+
.replace(/\/+$/, "")
|
|
110
|
+
.toLowerCase();
|
|
111
|
+
if (route !== "connect") {
|
|
112
|
+
throw new Error("Invalid deep link route. Expected temet://connect");
|
|
113
|
+
}
|
|
114
|
+
const address = normalizeAddress(url.searchParams.get("address") ?? "");
|
|
115
|
+
const token = (url.searchParams.get("token") ?? "").trim();
|
|
116
|
+
const relayUrl = (url.searchParams.get("relay") ?? DEFAULT_RELAY_URL).trim();
|
|
117
|
+
const serverName = (url.searchParams.get("name") ?? DEFAULT_SERVER_NAME).trim();
|
|
118
|
+
if (token.length === 0) {
|
|
119
|
+
throw new Error("Deep link missing token parameter.");
|
|
120
|
+
}
|
|
121
|
+
if (relayUrl.length === 0) {
|
|
122
|
+
throw new Error("Deep link has empty relay parameter.");
|
|
123
|
+
}
|
|
124
|
+
if (serverName.length === 0) {
|
|
125
|
+
throw new Error("Deep link has empty name parameter.");
|
|
126
|
+
}
|
|
127
|
+
return { address, token, relayUrl, serverName };
|
|
128
|
+
}
|
|
129
|
+
function parseConnectUrlOptions(flags, positionals) {
|
|
130
|
+
const deeplink = readOptionalString(flags, "url") ?? positionals[0] ?? "";
|
|
131
|
+
if (!deeplink) {
|
|
132
|
+
throw new Error("Missing --url for connect-url command.");
|
|
133
|
+
}
|
|
134
|
+
const parsed = parseTemetDeepLink(deeplink);
|
|
135
|
+
const configPath = resolve(String(flags.get("config-path") ?? ".mcp.json").trim());
|
|
136
|
+
const relayUrl = readOptionalString(flags, "relay-url") ??
|
|
137
|
+
parsed.relayUrl ??
|
|
138
|
+
DEFAULT_RELAY_URL;
|
|
139
|
+
const serverName = readOptionalString(flags, "name") ??
|
|
140
|
+
parsed.serverName ??
|
|
141
|
+
DEFAULT_SERVER_NAME;
|
|
142
|
+
const dryRun = Boolean(flags.get("dry-run"));
|
|
143
|
+
return {
|
|
144
|
+
address: parsed.address,
|
|
145
|
+
token: parsed.token,
|
|
146
|
+
configPath,
|
|
147
|
+
relayUrl,
|
|
148
|
+
serverName,
|
|
149
|
+
dryRun,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function parseArgs(argv) {
|
|
153
|
+
if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) {
|
|
154
|
+
printHelp(0);
|
|
155
|
+
}
|
|
156
|
+
const [command, ...rest] = argv;
|
|
157
|
+
const { flags, positionals } = parseFlagBag(rest);
|
|
158
|
+
const dryRun = Boolean(flags.get("dry-run"));
|
|
159
|
+
if (command === "connect") {
|
|
160
|
+
return {
|
|
161
|
+
command,
|
|
162
|
+
options: parseConnectOptions(flags),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (command === "connect-url") {
|
|
166
|
+
return {
|
|
167
|
+
command,
|
|
168
|
+
options: parseConnectUrlOptions(flags, positionals),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (command === "install-handler" || command === "uninstall-handler") {
|
|
172
|
+
return { command, dryRun };
|
|
173
|
+
}
|
|
174
|
+
console.error(`Unknown command: ${command}`);
|
|
175
|
+
printHelp(1);
|
|
176
|
+
}
|
|
177
|
+
async function readConfig(configPath) {
|
|
178
|
+
try {
|
|
179
|
+
const content = await readFile(configPath, "utf8");
|
|
180
|
+
if (!content.trim())
|
|
181
|
+
return {};
|
|
182
|
+
const parsed = JSON.parse(content);
|
|
183
|
+
if (!isObject(parsed)) {
|
|
184
|
+
throw new Error("MCP config must be a JSON object.");
|
|
185
|
+
}
|
|
186
|
+
return parsed;
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
const e = error;
|
|
190
|
+
if (e.code === "ENOENT") {
|
|
191
|
+
return {};
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function buildServerEntry(opts) {
|
|
197
|
+
return {
|
|
198
|
+
type: "http",
|
|
199
|
+
url: opts.relayUrl,
|
|
200
|
+
headers: {
|
|
201
|
+
"X-Temet-Address": opts.address,
|
|
202
|
+
Authorization: `Bearer ${opts.token}`,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function upsertServer(config, serverName, entry) {
|
|
207
|
+
if (isObject(config.mcpServers)) {
|
|
208
|
+
const servers = config.mcpServers;
|
|
209
|
+
servers[serverName] = entry;
|
|
210
|
+
config.mcpServers = servers;
|
|
211
|
+
return config;
|
|
212
|
+
}
|
|
213
|
+
// Fresh or flat config — always wrap in mcpServers (Claude Code / Cursor expect this)
|
|
214
|
+
config.mcpServers = { [serverName]: entry };
|
|
215
|
+
return config;
|
|
216
|
+
}
|
|
217
|
+
async function writeConfig(configPath, config) {
|
|
218
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
219
|
+
await writeFile(configPath, `${JSON.stringify(config, null, "\t")}\n`, "utf8");
|
|
220
|
+
}
|
|
221
|
+
async function runWriteFlow(command, opts) {
|
|
222
|
+
const current = await readConfig(opts.configPath);
|
|
223
|
+
const next = upsertServer(current, opts.serverName, buildServerEntry(opts));
|
|
224
|
+
if (opts.dryRun) {
|
|
225
|
+
console.log("[temet] dry-run: config preview");
|
|
226
|
+
console.log(JSON.stringify(next, null, "\t"));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
await writeConfig(opts.configPath, next);
|
|
230
|
+
console.log(`[temet] config updated: ${opts.configPath}`);
|
|
231
|
+
console.log(`[temet] server: ${opts.serverName}`);
|
|
232
|
+
console.log(`[temet] source: ${command}`);
|
|
233
|
+
console.log("[temet] next: restart your MCP client (Claude Code/Cursor/Codex).");
|
|
234
|
+
}
|
|
235
|
+
async function resolveBinary(name) {
|
|
236
|
+
try {
|
|
237
|
+
if (process.platform === "win32") {
|
|
238
|
+
const { stdout } = await execFileAsync("where", [name]);
|
|
239
|
+
const line = stdout
|
|
240
|
+
.split(/\r?\n/)
|
|
241
|
+
.map((entry) => entry.trim())
|
|
242
|
+
.find((entry) => entry.length > 0);
|
|
243
|
+
return line ?? null;
|
|
244
|
+
}
|
|
245
|
+
const { stdout } = await execFileAsync("which", [name]);
|
|
246
|
+
const line = stdout
|
|
247
|
+
.split(/\r?\n/)
|
|
248
|
+
.map((entry) => entry.trim())
|
|
249
|
+
.find((entry) => entry.length > 0);
|
|
250
|
+
return line ?? null;
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function shQuote(value) {
|
|
257
|
+
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
258
|
+
}
|
|
259
|
+
function buildProtocolHandlerScript(temetBin, pnpmBin) {
|
|
260
|
+
const temetRunner = temetBin
|
|
261
|
+
? `${shQuote(temetBin)} connect-url --url "$URL"`
|
|
262
|
+
: null;
|
|
263
|
+
const pnpmRunner = pnpmBin
|
|
264
|
+
? `${shQuote(pnpmBin)} dlx @temet/cli connect-url --url "$URL"`
|
|
265
|
+
: null;
|
|
266
|
+
const runLine = temetRunner ??
|
|
267
|
+
pnpmRunner ??
|
|
268
|
+
`echo "[temet] no runner found (need temet or pnpm in PATH)"`;
|
|
269
|
+
return `#!/bin/sh
|
|
270
|
+
URL="$1"
|
|
271
|
+
LOG_DIR="$HOME/.temet"
|
|
272
|
+
LOG_FILE="$LOG_DIR/handler.log"
|
|
273
|
+
|
|
274
|
+
mkdir -p "$LOG_DIR"
|
|
275
|
+
|
|
276
|
+
{
|
|
277
|
+
echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] handler URL: $URL"
|
|
278
|
+
if [ -z "$URL" ]; then
|
|
279
|
+
echo "[temet] empty URL"
|
|
280
|
+
exit 1
|
|
281
|
+
fi
|
|
282
|
+
${runLine}
|
|
283
|
+
STATUS=$?
|
|
284
|
+
if [ "$STATUS" -ne 0 ]; then
|
|
285
|
+
echo "[temet] handler failed with exit $STATUS"
|
|
286
|
+
exit "$STATUS"
|
|
287
|
+
fi
|
|
288
|
+
echo "[temet] handler success"
|
|
289
|
+
} >> "$LOG_FILE" 2>&1
|
|
290
|
+
`;
|
|
291
|
+
}
|
|
292
|
+
async function safeExec(command, args, errorHint) {
|
|
293
|
+
try {
|
|
294
|
+
await execFileAsync(command, args);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
console.warn(`[temet] warning: ${errorHint}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async function installMacHandler(dryRun) {
|
|
301
|
+
const appDir = resolve(homedir(), "Applications", `${DEFAULT_PROTOCOL_APP_NAME}.app`);
|
|
302
|
+
const contentsDir = resolve(appDir, "Contents");
|
|
303
|
+
const macOSDir = resolve(contentsDir, "MacOS");
|
|
304
|
+
const executablePath = resolve(macOSDir, "temet-handler");
|
|
305
|
+
const infoPlistPath = resolve(contentsDir, "Info.plist");
|
|
306
|
+
const lsregisterPath = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
|
|
307
|
+
const temetBin = await resolveBinary("temet");
|
|
308
|
+
const pnpmBin = await resolveBinary("pnpm");
|
|
309
|
+
const script = buildProtocolHandlerScript(temetBin, pnpmBin);
|
|
310
|
+
const infoPlist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
311
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
312
|
+
<plist version="1.0">
|
|
313
|
+
<dict>
|
|
314
|
+
<key>CFBundleName</key>
|
|
315
|
+
<string>${DEFAULT_PROTOCOL_APP_NAME}</string>
|
|
316
|
+
<key>CFBundleDisplayName</key>
|
|
317
|
+
<string>${DEFAULT_PROTOCOL_APP_NAME}</string>
|
|
318
|
+
<key>CFBundleIdentifier</key>
|
|
319
|
+
<string>com.temet.protocol-handler</string>
|
|
320
|
+
<key>CFBundleVersion</key>
|
|
321
|
+
<string>1</string>
|
|
322
|
+
<key>CFBundlePackageType</key>
|
|
323
|
+
<string>APPL</string>
|
|
324
|
+
<key>CFBundleExecutable</key>
|
|
325
|
+
<string>temet-handler</string>
|
|
326
|
+
<key>CFBundleURLTypes</key>
|
|
327
|
+
<array>
|
|
328
|
+
<dict>
|
|
329
|
+
<key>CFBundleURLName</key>
|
|
330
|
+
<string>Temet Protocol</string>
|
|
331
|
+
<key>CFBundleURLSchemes</key>
|
|
332
|
+
<array>
|
|
333
|
+
<string>temet</string>
|
|
334
|
+
</array>
|
|
335
|
+
</dict>
|
|
336
|
+
</array>
|
|
337
|
+
</dict>
|
|
338
|
+
</plist>
|
|
339
|
+
`;
|
|
340
|
+
if (dryRun) {
|
|
341
|
+
console.log(`[temet] dry-run macOS install path: ${appDir}`);
|
|
342
|
+
console.log(`[temet] dry-run executable: ${executablePath}`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
await mkdir(macOSDir, { recursive: true });
|
|
346
|
+
await writeFile(infoPlistPath, infoPlist, "utf8");
|
|
347
|
+
await writeFile(executablePath, script, "utf8");
|
|
348
|
+
await chmod(executablePath, 0o755);
|
|
349
|
+
await safeExec(lsregisterPath, ["-f", appDir], "unable to refresh LaunchServices registration (you may need to open the app once)");
|
|
350
|
+
console.log(`[temet] macOS handler installed: ${appDir}`);
|
|
351
|
+
if (!temetBin && !pnpmBin) {
|
|
352
|
+
console.log("[temet] warning: neither 'temet' nor 'pnpm' was found in PATH during install.");
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async function uninstallMacHandler(dryRun) {
|
|
356
|
+
const appDir = resolve(homedir(), "Applications", `${DEFAULT_PROTOCOL_APP_NAME}.app`);
|
|
357
|
+
const lsregisterPath = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
|
|
358
|
+
if (dryRun) {
|
|
359
|
+
console.log(`[temet] dry-run macOS uninstall path: ${appDir}`);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
await safeExec(lsregisterPath, ["-u", appDir], "unable to unregister LaunchServices entry");
|
|
363
|
+
await rm(appDir, { recursive: true, force: true });
|
|
364
|
+
console.log(`[temet] macOS handler removed: ${appDir}`);
|
|
365
|
+
}
|
|
366
|
+
async function installLinuxHandler(dryRun) {
|
|
367
|
+
const localShare = resolve(homedir(), ".local", "share", "applications");
|
|
368
|
+
const localBin = resolve(homedir(), ".local", "bin");
|
|
369
|
+
const desktopPath = resolve(localShare, LINUX_DESKTOP_ENTRY);
|
|
370
|
+
const scriptPath = resolve(localBin, LINUX_HANDLER_SCRIPT);
|
|
371
|
+
const temetBin = await resolveBinary("temet");
|
|
372
|
+
const pnpmBin = await resolveBinary("pnpm");
|
|
373
|
+
const script = buildProtocolHandlerScript(temetBin, pnpmBin);
|
|
374
|
+
const desktopEntry = `[Desktop Entry]
|
|
375
|
+
Name=Temet Protocol Handler
|
|
376
|
+
Exec=${scriptPath} %u
|
|
377
|
+
Type=Application
|
|
378
|
+
Terminal=false
|
|
379
|
+
NoDisplay=true
|
|
380
|
+
MimeType=x-scheme-handler/temet;
|
|
381
|
+
`;
|
|
382
|
+
if (dryRun) {
|
|
383
|
+
console.log(`[temet] dry-run linux desktop file: ${desktopPath}`);
|
|
384
|
+
console.log(`[temet] dry-run linux script path: ${scriptPath}`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
await mkdir(localShare, { recursive: true });
|
|
388
|
+
await mkdir(localBin, { recursive: true });
|
|
389
|
+
await writeFile(scriptPath, script, "utf8");
|
|
390
|
+
await chmod(scriptPath, 0o755);
|
|
391
|
+
await writeFile(desktopPath, desktopEntry, "utf8");
|
|
392
|
+
await safeExec("xdg-mime", ["default", LINUX_DESKTOP_ENTRY, "x-scheme-handler/temet"], "xdg-mime registration failed. Try running it manually.");
|
|
393
|
+
await safeExec("update-desktop-database", [localShare], "update-desktop-database not available");
|
|
394
|
+
console.log(`[temet] linux handler installed: ${desktopPath}`);
|
|
395
|
+
}
|
|
396
|
+
async function uninstallLinuxHandler(dryRun) {
|
|
397
|
+
const localShare = resolve(homedir(), ".local", "share", "applications");
|
|
398
|
+
const localBin = resolve(homedir(), ".local", "bin");
|
|
399
|
+
const desktopPath = resolve(localShare, LINUX_DESKTOP_ENTRY);
|
|
400
|
+
const scriptPath = resolve(localBin, LINUX_HANDLER_SCRIPT);
|
|
401
|
+
if (dryRun) {
|
|
402
|
+
console.log(`[temet] dry-run linux uninstall desktop file: ${desktopPath}`);
|
|
403
|
+
console.log(`[temet] dry-run linux uninstall script path: ${scriptPath}`);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
await rm(desktopPath, { force: true });
|
|
407
|
+
await rm(scriptPath, { force: true });
|
|
408
|
+
console.log(`[temet] linux handler removed`);
|
|
409
|
+
}
|
|
410
|
+
async function installWindowsHandler(dryRun) {
|
|
411
|
+
const temetBin = await resolveBinary("temet");
|
|
412
|
+
const pnpmBin = await resolveBinary("pnpm");
|
|
413
|
+
const runner = temetBin
|
|
414
|
+
? `"${temetBin}" connect-url --url "%1"`
|
|
415
|
+
: pnpmBin
|
|
416
|
+
? `"${pnpmBin}" dlx @temet/cli connect-url --url "%1"`
|
|
417
|
+
: `temet connect-url --url "%1"`;
|
|
418
|
+
const commandValue = `cmd.exe /d /s /c ${runner}`;
|
|
419
|
+
if (dryRun) {
|
|
420
|
+
console.log(`[temet] dry-run windows command: ${commandValue}`);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
await execFileAsync("reg", [
|
|
424
|
+
"add",
|
|
425
|
+
"HKCU\\Software\\Classes\\temet",
|
|
426
|
+
"/ve",
|
|
427
|
+
"/d",
|
|
428
|
+
"URL:Temet Protocol",
|
|
429
|
+
"/f",
|
|
430
|
+
]);
|
|
431
|
+
await execFileAsync("reg", [
|
|
432
|
+
"add",
|
|
433
|
+
"HKCU\\Software\\Classes\\temet",
|
|
434
|
+
"/v",
|
|
435
|
+
"URL Protocol",
|
|
436
|
+
"/d",
|
|
437
|
+
"",
|
|
438
|
+
"/f",
|
|
439
|
+
]);
|
|
440
|
+
await execFileAsync("reg", [
|
|
441
|
+
"add",
|
|
442
|
+
"HKCU\\Software\\Classes\\temet\\shell\\open\\command",
|
|
443
|
+
"/ve",
|
|
444
|
+
"/d",
|
|
445
|
+
commandValue,
|
|
446
|
+
"/f",
|
|
447
|
+
]);
|
|
448
|
+
console.log("[temet] windows handler installed (HKCU\\Software\\Classes\\temet)");
|
|
449
|
+
}
|
|
450
|
+
async function uninstallWindowsHandler(dryRun) {
|
|
451
|
+
if (dryRun) {
|
|
452
|
+
console.log("[temet] dry-run windows uninstall key: HKCU\\Software\\Classes\\temet");
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
await execFileAsync("reg", [
|
|
456
|
+
"delete",
|
|
457
|
+
"HKCU\\Software\\Classes\\temet",
|
|
458
|
+
"/f",
|
|
459
|
+
]);
|
|
460
|
+
console.log("[temet] windows handler removed (HKCU\\Software\\Classes\\temet)");
|
|
461
|
+
}
|
|
462
|
+
async function installProtocolHandler(dryRun) {
|
|
463
|
+
if (process.platform === "darwin") {
|
|
464
|
+
await installMacHandler(dryRun);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (process.platform === "linux") {
|
|
468
|
+
await installLinuxHandler(dryRun);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (process.platform === "win32") {
|
|
472
|
+
await installWindowsHandler(dryRun);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
476
|
+
}
|
|
477
|
+
async function uninstallProtocolHandler(dryRun) {
|
|
478
|
+
if (process.platform === "darwin") {
|
|
479
|
+
await uninstallMacHandler(dryRun);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (process.platform === "linux") {
|
|
483
|
+
await uninstallLinuxHandler(dryRun);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (process.platform === "win32") {
|
|
487
|
+
await uninstallWindowsHandler(dryRun);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
491
|
+
}
|
|
492
|
+
async function run() {
|
|
493
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
494
|
+
if (parsed.command === "connect" || parsed.command === "connect-url") {
|
|
495
|
+
await runWriteFlow(parsed.command, parsed.options);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (parsed.command === "install-handler") {
|
|
499
|
+
await installProtocolHandler(parsed.dryRun);
|
|
500
|
+
console.log("[temet] next: click a Quick connect button in Temet.");
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (parsed.command === "uninstall-handler") {
|
|
504
|
+
await uninstallProtocolHandler(parsed.dryRun);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
throw new Error(`Unhandled command: ${parsed.command}`);
|
|
508
|
+
}
|
|
509
|
+
run().catch((error) => {
|
|
510
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
511
|
+
console.error(`[temet] error: ${message}`);
|
|
512
|
+
process.exit(1);
|
|
513
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@temet/cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Temet CLI for MCP configuration",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"temet",
|
|
7
|
+
"mcp",
|
|
8
|
+
"byoa",
|
|
9
|
+
"cli",
|
|
10
|
+
"agent"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"homepage": "https://temetapp.com",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/ramponneau/temet.git",
|
|
17
|
+
"directory": "packages/cli"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/ramponneau/temet/issues"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"bin": {
|
|
24
|
+
"temet": "dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc -p tsconfig.json",
|
|
31
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
}
|
|
36
|
+
}
|