Sign In

@xmemo/client

Package Overview
Dependencies
Maintainers
1
Versions
51
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@xmemo/client - npm Package Compare versions

Comparing version
0.4.180
to
0.4.181
+46
skills/xmemo/install.ps1
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Net.Http
$baseUrl = if ($env:XMEMO_BASE_URL) { $env:XMEMO_BASE_URL.TrimEnd('/') } else { 'https://xmemo.dev' }
$packageUrl = [Uri]"$baseUrl/v1/skill/package"
$installDir = if ($env:XMEMO_SKILL_DIR) { $env:XMEMO_SKILL_DIR } else { 'xmemo-skill' }
$tempDir = "$installDir.tmp.$PID"
if ($packageUrl.Scheme -ne 'https') { throw 'XMemo Skill installer requires an HTTPS XMEMO_BASE_URL.' }
if (Test-Path -LiteralPath $installDir) { throw "Destination already exists: $installDir" }
$handler = [System.Net.Http.HttpClientHandler]::new()
$handler.AllowAutoRedirect = $false
$client = [System.Net.Http.HttpClient]::new($handler)
try {
New-Item -ItemType Directory -Path $tempDir, "$tempDir\extract" | Out-Null
$archivePath = "$tempDir\xmemo-skill.tar.gz"
$uri = $packageUrl
$downloaded = $false
for ($redirects = 0; $redirects -lt 6; $redirects++) {
$response = $client.GetAsync($uri, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult()
if ([int]$response.StatusCode -ge 300 -and [int]$response.StatusCode -lt 400) {
if (-not $response.Headers.Location) { throw 'HTTPS redirect is missing a location.' }
$nextUri = [Uri]::new($uri, $response.Headers.Location)
$response.Dispose()
if ($nextUri.Scheme -ne 'https') { throw 'Refusing a non-HTTPS redirect.' }
$uri = $nextUri
continue
}
if (-not $response.IsSuccessStatusCode) { throw "Download failed: HTTP $([int]$response.StatusCode)" }
$stream = [System.IO.File]::Create($archivePath)
try { $response.Content.CopyToAsync($stream).GetAwaiter().GetResult() } finally { $stream.Dispose(); $response.Dispose() }
$downloaded = $true
break
}
if (-not $downloaded) { throw 'Too many redirects.' }
& tar.exe -xzf $archivePath -C "$tempDir\extract"
if ($LASTEXITCODE -ne 0) { throw 'Archive extraction failed.' }
if (-not (Test-Path -LiteralPath "$tempDir\extract\scripts\xmemo-skill.mjs" -PathType Leaf)) {
throw 'Archive does not contain xmemo-skill.'
}
Move-Item -LiteralPath "$tempDir\extract" -Destination $installDir
Write-Output "Installed XMemo Skill to $installDir"
} finally {
$client.Dispose()
if (Test-Path -LiteralPath $tempDir) { Remove-Item -LiteralPath $tempDir -Recurse -Force }
}
#!/bin/sh
# Install the XMemo standalone Skill with only curl and tar available.
set -eu
base_url="${XMEMO_BASE_URL:-https://xmemo.dev}"
case "$base_url" in https://*) ;; *) printf '%s\n' 'XMemo Skill installer requires an HTTPS XMEMO_BASE_URL.' >&2; exit 1 ;; esac
package_url="${base_url%/}/v1/skill/package"
install_dir="${XMEMO_SKILL_DIR:-xmemo-skill}"
tmp_dir="${install_dir}.tmp.$$"
fail() { printf '%s\n' "XMemo Skill installer: $1" >&2; exit 1; }
[ ! -e "$install_dir" ] || fail "destination already exists: $install_dir"
cleanup() { rm -rf "$tmp_dir"; }
trap cleanup 0 HUP INT TERM
mkdir "$tmp_dir" "$tmp_dir/extract" || fail "cannot create temporary directory"
curl --fail --show-error --silent --location --proto '=https' --proto-redir '=https' \
"$package_url" -o "$tmp_dir/xmemo-skill.tar.gz" || fail "download failed"
tar -xzf "$tmp_dir/xmemo-skill.tar.gz" -C "$tmp_dir/extract" || fail "archive extraction failed"
[ -f "$tmp_dir/extract/scripts/xmemo-skill.mjs" ] || fail "archive does not contain xmemo-skill"
mv "$tmp_dir/extract" "$install_dir" || fail "could not finalize installation"
printf '%s\n' "Installed XMemo Skill to $install_dir"
import fs from 'node:fs/promises';
import path from 'node:path';
import { randomUUID } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { hasFlag, optionValue } from '../core/args.js';
import {
CLI_VERSION,
COMMAND_NAME,
PACKAGE_NAME
} from '../core/constants.js';
import { UsageError } from '../core/errors.js';
import { writeLine } from '../core/io.js';
const BUNDLED_SKILL_DIR = fileURLToPath(new URL('../../skills/xmemo/', import.meta.url));
const DEFAULT_INSTALL_DIR = 'xmemo-skill';
const REQUIRED_SKILL_FILES = [
'SKILL.md',
path.join('scripts', 'xmemo-skill.mjs')
];
export async function skillCommand(args, io) {
const subcommand = args[0] ?? 'help';
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
writeSkillHelp(io);
return 0;
}
if (subcommand !== 'install') {
throw new UsageError(`Unknown skill command: ${subcommand}`);
}
const optionArgs = args.slice(1);
if (hasFlag(optionArgs, '--help') || hasFlag(optionArgs, '-h')) {
writeSkillHelp(io);
return 0;
}
validateInstallArgs(optionArgs);
const dryRun = hasFlag(optionArgs, '--dry-run');
const force = hasFlag(optionArgs, '--force');
const outputJson = hasFlag(optionArgs, '--json');
const cwd = io.cwd ?? process.cwd();
const configuredTarget = optionValue(optionArgs, '--target')
?? io.env?.XMEMO_SKILL_DIR
?? DEFAULT_INSTALL_DIR;
const targetDir = path.resolve(cwd, configuredTarget);
validateTarget(BUNDLED_SKILL_DIR, targetDir);
const skillVersion = await validateBundledSkill(BUNDLED_SKILL_DIR);
const targetExists = await pathExists(targetDir);
if (targetExists && !force) {
throw new UsageError(`Skill destination already exists: ${targetDir}. Use --force to replace it.`);
}
const report = {
package: PACKAGE_NAME,
cliVersion: CLI_VERSION,
skillVersion,
source: BUNDLED_SKILL_DIR,
target: targetDir,
dryRun,
force,
replaced: targetExists && !dryRun,
installed: false,
networkUsed: false,
tokenSent: false
};
if (!dryRun) {
await installBundledSkill(BUNDLED_SKILL_DIR, targetDir, { replace: targetExists });
report.installed = true;
}
if (outputJson) {
writeLine(io.stdout, JSON.stringify(report, null, 2));
return 0;
}
const action = dryRun ? 'Would install' : 'Installed';
writeLine(io.stdout, `${action} bundled XMemo Skill ${skillVersion} to ${targetDir}`);
writeLine(io.stdout, `Source: ${PACKAGE_NAME} ${CLI_VERSION} (offline; no credential used)`);
if (dryRun) {
writeLine(io.stdout, 'Dry run only; no files were changed.');
}
return 0;
}
function writeSkillHelp(io) {
writeLine(io.stdout, 'Skill commands:');
writeLine(io.stdout, ` ${COMMAND_NAME} skill install [--target <directory>] [--dry-run] [--force] [--json]`);
writeLine(io.stdout, '');
writeLine(io.stdout, `Installs the XMemo Skill bundled with the current ${PACKAGE_NAME} package.`);
writeLine(io.stdout, `The default destination is ./${DEFAULT_INSTALL_DIR}; XMEMO_SKILL_DIR can override it.`);
writeLine(io.stdout, 'Installation is offline and never reads or sends XMemo credentials.');
}
function validateInstallArgs(args) {
const flags = new Set(['--dry-run', '--force', '--json', '--help', '-h']);
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === '--target') {
if (!args[index + 1] || args[index + 1].startsWith('--')) {
throw new UsageError('Option --target requires a value.');
}
index += 1;
continue;
}
if (!flags.has(arg)) {
throw new UsageError(`Unknown skill install option: ${arg}`);
}
}
}
function validateTarget(sourceDir, targetDir) {
const root = path.parse(targetDir).root;
if (targetDir === root) {
throw new UsageError('Refusing to install a Skill into a filesystem root.');
}
const relative = path.relative(sourceDir, targetDir);
if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) {
throw new UsageError('Skill destination cannot be the bundled source or a directory inside it.');
}
}
async function validateBundledSkill(sourceDir) {
for (const relativePath of REQUIRED_SKILL_FILES) {
const sourcePath = path.join(sourceDir, relativePath);
const stat = await fs.stat(sourcePath).catch(() => null);
if (!stat?.isFile()) {
throw new UsageError(`The npm package is missing bundled Skill file: ${relativePath}`);
}
}
await rejectSymlinks(sourceDir);
const runtimeSource = await fs.readFile(path.join(sourceDir, 'scripts', 'xmemo-skill.mjs'), 'utf8');
const skillVersion = runtimeSource.match(/const SKILL_VERSION = '([^']+)'/)?.[1];
if (!skillVersion) {
throw new UsageError('The bundled XMemo Skill version could not be determined.');
}
return skillVersion;
}
async function rejectSymlinks(directory) {
const entries = await fs.readdir(directory, { withFileTypes: true });
for (const entry of entries) {
const entryPath = path.join(directory, entry.name);
if (entry.isSymbolicLink()) {
throw new UsageError(`The bundled XMemo Skill contains a symbolic link: ${entry.name}`);
}
if (entry.isDirectory()) {
await rejectSymlinks(entryPath);
}
}
}
async function installBundledSkill(sourceDir, targetDir, { replace }) {
const parentDir = path.dirname(targetDir);
const baseName = path.basename(targetDir);
const nonce = `${process.pid}-${randomUUID()}`;
const stagingDir = path.join(parentDir, `.${baseName}.xmemo-tmp-${nonce}`);
const backupDir = path.join(parentDir, `.${baseName}.xmemo-backup-${nonce}`);
let backupCreated = false;
await fs.mkdir(parentDir, { recursive: true });
try {
await fs.cp(sourceDir, stagingDir, { recursive: true, errorOnExist: true, force: false });
if (replace) {
await fs.rename(targetDir, backupDir);
backupCreated = true;
}
await fs.rename(stagingDir, targetDir);
if (backupCreated) {
await fs.rm(backupDir, { recursive: true, force: true });
backupCreated = false;
}
} catch (error) {
if (backupCreated && !await pathExists(targetDir)) {
await fs.rename(backupDir, targetDir).catch(() => {});
backupCreated = false;
}
throw error;
} finally {
await fs.rm(stagingDir, { recursive: true, force: true });
if (backupCreated && await pathExists(targetDir)) {
await fs.rm(backupDir, { recursive: true, force: true });
}
}
}
async function pathExists(targetPath) {
try {
await fs.access(targetPath);
return true;
} catch (error) {
if (error.code === 'ENOENT') {
return false;
}
throw error;
}
}
+1
-1
{
"name": "@xmemo/client",
"version": "0.4.180",
"version": "0.4.181",
"description": "Privacy-first CLI and MCP setup helper for XMemo.",

@@ -5,0 +5,0 @@ "mcpName": "io.github.yonro/xmemo",

@@ -320,2 +320,22 @@ <div align="center">

<details>
<summary><strong>Bundled XMemo Skill</strong></summary>
```bash
xmemo skill install --dry-run
xmemo skill install
xmemo skill install --target ~/.codex/skills/xmemo-memory
xmemo skill install --target ~/.claude/skills/xmemo-memory
```
The command copies the Skill bundled in the current `@xmemo/client` package, so
it also works through `npx @xmemo/client skill install`. It is offline, never
uses XMemo credentials, refuses to overwrite an existing destination by
default, and supports explicit atomic replacement with `--force`.
The default destination is `./xmemo-skill`. Use `--target` (or
`XMEMO_SKILL_DIR`) for an Agent-specific user or project Skill directory.
</details>
<details>
<summary><strong>Safe removal</strong></summary>

@@ -322,0 +342,0 @@

# XMemo Skill Change Log
## 1.1.5
- Add zero-dependency POSIX and PowerShell installers for the published
standalone Skill archive. Both enforce HTTPS-only download paths, reject
non-HTTPS redirects, verify the bundled runtime entrypoint, and never accept
or send XMemo credentials.
- Document the installer commands and their destination/origin boundaries;
installation remains separate from explicit login and credential setup.
- Regression coverage pins the HTTPS, redirect, entrypoint, and no-token
guarantees for both installer scripts.
## 1.1.4

@@ -4,0 +15,0 @@

@@ -16,3 +16,3 @@ #!/usr/bin/env node

const SKILL_VERSION = '1.1.4';
const SKILL_VERSION = '1.1.5';
const credentialsPath = path.join(os.homedir(), '.xmemo', 'skill-credentials.json');

@@ -19,0 +19,0 @@ const registrationPath = path.join(os.homedir(), '.xmemo', 'skill-registration.json');

@@ -19,2 +19,21 @@ ---

## Install The Standalone Skill
For a fresh standalone installation, download the currently published Skill
archive using the installer appropriate to the host:
```text
curl -fsSL https://xmemo.dev/v1/skill/package/install.sh | sh
```
```powershell
irm https://xmemo.dev/v1/skill/package/install.ps1 | iex
```
Both installers require HTTPS, follow HTTPS-only redirects, and refuse to
replace an existing destination. By default they create `xmemo-skill` in the
current directory. Set `XMEMO_SKILL_DIR` to choose a new destination, or set
`XMEMO_BASE_URL` only to a trusted HTTPS XMemo origin. They download and unpack
the archive only; login and credential configuration remain explicit steps.
## Hosted Discovery Boundary

@@ -21,0 +40,0 @@

@@ -19,2 +19,3 @@ import {

import { setupCommand } from './commands/setup.js';
import { skillCommand } from './commands/skill.js';
import { uninstallCommand } from './commands/uninstall.js';

@@ -61,2 +62,6 @@ import { updateCommand } from './commands/update.js';

if (command === 'skill') {
return await skillCommand(args.slice(1), io);
}
if (command === 'uninstall') {

@@ -63,0 +68,0 @@ return await uninstallCommand(args.slice(1), io);

@@ -45,2 +45,4 @@ import {

writeLine(io.stdout, ' Check or apply the latest npm package update.');
writeLine(io.stdout, ` ${COMMAND_NAME} skill install [--target <directory>] [--dry-run] [--force] [--json]`);
writeLine(io.stdout, ' Install the XMemo Skill bundled in this npm package without network access.');
writeLine(io.stdout, '');

@@ -47,0 +49,0 @@ writeLine(io.stdout, 'MCP And Profiles');