Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,30 @@ jobs:
path: packages/cli/build/sessionforge-${{ matrix.target }}*
if-no-files-found: error

# The Paseo plugin (this repo's root — index.ts, main.client.tsx, src/server/*) isn't OS/arch-specific
# like the CLI binaries above, so it only needs one job. `sessionforge wire-paseo` downloads this exact
# asset and points a local `paseo plugin install` at it.
package-plugin:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm

- run: npm install
- run: npm run package:plugin

- uses: actions/upload-artifact@v4
with:
name: sessionforge-paseo-plugin
path: build/sessionforge-paseo-plugin.tar.gz
if-no-files-found: error

release:
needs: build-binaries
needs: [build-binaries, package-plugin]
runs-on: ubuntu-latest
permissions:
contents: write
Expand Down
12 changes: 9 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,15 @@ with nvm, not by reading release notes, so re-verify empirically before ever low

`.github/workflows/release.yml` is manual-trigger only (`workflow_dispatch` from the Actions tab) — it
never runs on a push. Builds a standalone `sessionforge` binary (Node SEA) natively per platform — Linux,
macOS Intel, macOS Apple Silicon, Windows — and attaches them to a new GitHub Release tagged from
`packages/cli/package.json`'s current version. This is the only distribution channel; there is no npm
publish step.
macOS Intel, macOS Apple Silicon, Windows — and also packages the Paseo plugin (`scripts/package-plugin.mjs`
— a self-contained directory with a real, non-symlinked copy of the built `@aadaa88/sessionforge` package
baked into `node_modules`, since npm workspace symlinks don't survive being extracted on someone else's
machine; verified by actually installing the packaged output via `paseo plugin install`, not just built).
All of these are attached to a new GitHub Release tagged from `packages/cli/package.json`'s current
version. This is the only distribution channel; there is no npm publish step. `sessionforge wire-paseo`
downloads that same plugin asset and installs it — keep its version-matched download URL (see
`packages/cli/src/core/paseo-wire.server.ts`) in sync with this workflow's release tag format if either
changes.

## Making changes

Expand Down
12 changes: 9 additions & 3 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,15 @@ irm https://raw.githubusercontent.com/4mGLn/sessionforge/main/install.ps1 | iex
`sessionforge` 바이너리를 내려받아 PATH에 등록합니다. 지원 대상과 설치 경로를 바꾸는 방법은
[`docs/MANUAL.md`](docs/MANUAL.md#installing-the-standalone-binary)를 참고하세요.

**Paseo 플러그인**으로도 사용할 수 있습니다: 클론한 뒤 `paseo plugin install /path/to/sessionforge` —
자세한 내용과 알려진 환경 이슈는 [`docs/MANUAL.md`](docs/MANUAL.md#installing-the-paseo-plugin) 참고
(이 경로는 Node.js 22.16 이상이 필요합니다; 독립 바이너리는 Node.js가 전혀 필요 없습니다).
**Paseo 플러그인**으로도 사용할 수 있습니다 — 위 바이너리가 있다면 다음으로 연결하세요:

```bash
sessionforge wire-paseo
```

자세한 내용, 알려진 환경 이슈, 그리고 (플러그인 개발 시에만 필요한) 수동 설치 방법
`paseo plugin install /path/to/sessionforge`(저장소 클론과 Node.js 22.16 이상 필요)는
[`docs/MANUAL.md`](docs/MANUAL.md#installing-the-paseo-plugin)를 참고하세요.

npm에는 배포하지 않습니다 — 독립 바이너리와 Paseo 플러그인이 유일한 배포 경로입니다.

Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,15 @@ Downloads a prebuilt `sessionforge` binary for your platform from the latest
[GitHub Release](https://github.com/4mGLn/sessionforge/releases) and puts it on your PATH. See
[`docs/MANUAL.md`](docs/MANUAL.md#installing-the-standalone-binary) for supported targets and override options.

Also available as a **Paseo plugin**: `paseo plugin install /path/to/sessionforge` after cloning — see
[`docs/MANUAL.md`](docs/MANUAL.md#installing-the-paseo-plugin) for details and a known environment quirk
(this route needs Node.js 22.16+; the standalone binary needs no Node.js at all).
Also available as a **Paseo plugin** — once you have the binary above, wire it up with:

```bash
sessionforge wire-paseo
```

See [`docs/MANUAL.md`](docs/MANUAL.md#installing-the-paseo-plugin) for details, a known environment quirk,
and the manual `paseo plugin install /path/to/sessionforge` alternative (only needed for plugin
development — it requires cloning the repo and Node.js 22.16+, unlike `wire-paseo`).

Not published to npm — the standalone binary and the Paseo plugin are the only distribution channels.

Expand Down
19 changes: 19 additions & 0 deletions docs/MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ sessionforge cleanup [--apply] [--json] # preview (default) or apply junk cle
sessionforge archive <id> [--reason ...] # move a session out of the active view
sessionforge restore <id> [--reason ...] # bring an archived/trashed session back
sessionforge audit [id] [--json] # show the audit trail for destructive operations
sessionforge wire-paseo [--version ...] # download and install the Paseo plugin (see below)
sessionforge paseo-status # check whether the Paseo plugin is installed and running
```

`list` flags: `--agent`, `--project`, `--status`, `--lifecycle`, `--category`, `--older-than 30d`,
Expand Down Expand Up @@ -84,6 +86,23 @@ no default whole-filesystem scan.

## Installing the Paseo plugin

The easy way, if you already have the standalone `sessionforge` binary or CLI ([Install](#installing-the-standalone-binary)):

```bash
sessionforge wire-paseo # downloads and installs the version-matched plugin, via `paseo plugin install`
sessionforge paseo-status # check whether it's installed and running
```

This automates exactly the manual flow below: it downloads the Paseo-plugin release asset matching this
CLI's own version (not `releases/latest` — an older CLI always wires up the plugin build it actually
shipped with), extracts it to `~/.sessionforge/paseo-plugin`, and runs `paseo plugin install` against that
directory. It never touches the daemon's `pluginsEnabled` switch itself — plugins are trusted, unsandboxed
code, so if plugins aren't enabled on your daemon, `wire-paseo` stops and tells you to enable them yourself
first (Settings → Plugins → Enable plugins in the Paseo app).

The manual way — needed if you're developing the plugin itself, since it points `paseo` straight at your
working tree instead of a downloaded release snapshot:

```bash
paseo plugin install /path/to/sessionforge
paseo plugin reload sessionforge # after any source change — never auto-reloaded
Expand Down
8 changes: 7 additions & 1 deletion docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ packages/cli/ @aadaa88/sessionforge — the standalone pack
relationships.server.ts local heuristic DUPLICATE/SUPERSEDED cross-session detection (same workspace, topic word-overlap, timing)
trash.server.ts OS-dispatched move-to-trash for delete (Linux XDG Trash / macOS ~/.Trash / Windows Recycle Bin)
store.server.ts SQLite persistence + FTS5 ranked search (node:sqlite, ~/.sessionforge/sessionforge.db)
paseo-wire.server.ts downloads + installs the Paseo plugin via `paseo plugin install` — powers `wire-paseo`/
`paseo-status`; only reads the daemon's pluginsEnabled setting, never writes it
discover.server.ts orchestrates adapters -> activity -> classify -> summarize -> store -> relationships
lifecycle-actions.server.ts archive/restore/delete/cleanup + audit log
src/cli/ the `sessionforge` CLI itself, imports ../core directly — no daemon needed
Expand All @@ -54,8 +56,12 @@ main.client.tsx session browser UI (sidebar panel): search/filter/a
checkboxes + bulk actions, per-provider logo icons, click-to-preview dialog with related
sessions, per-session and per-provider file size

scripts/package-plugin.mjs packages the Paseo plugin (this repo's root — see above) into a self-contained
directory, then tars it — what `sessionforge wire-paseo` downloads and installs

.github/workflows/ CI (typecheck/test/build on Linux/macOS/Windows) + a manual-trigger release workflow
(builds standalone binaries per platform, attaches them to a GitHub Release)
(builds standalone binaries per platform, packages the Paseo plugin, attaches
both to a GitHub Release)
```

## Why a plugin *and* a CLI — and why they're separate packages
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"build": "npm run build --workspace packages/cli",
"test": "npm test --workspace packages/cli",
"cli": "npm run cli --workspace packages/cli --",
"postinstall": "npm run build --workspace packages/cli"
"postinstall": "npm run build --workspace packages/cli",
"package:plugin": "node scripts/package-plugin.mjs"
},
"dependencies": {
"@aadaa88/sessionforge": "*"
Expand Down
8 changes: 7 additions & 1 deletion packages/cli/scripts/build-binary.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// — SEA binaries aren't cross-compilable from one machine — see .github/workflows/release.yml, which runs
// this once per matrix OS to cover Linux, both macOS architectures, and Windows.
import { execFileSync } from "node:child_process";
import { chmodSync, copyFileSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { chmodSync, copyFileSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { arch, platform } from "node:os";
import { dirname, join } from "node:path";
Expand Down Expand Up @@ -51,6 +51,8 @@ async function main() {
rmSync(buildDir, { recursive: true, force: true });
mkdirSync(buildDir, { recursive: true });

const { version } = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));

console.log(`Bundling CLI entry point for ${triple}...`);
await esbuild.build({
entryPoints: [join(packageRoot, "src", "cli", "bin.ts")],
Expand All @@ -63,6 +65,10 @@ async function main() {
// platform:"node" already treats bare node builtins as external automatically, this just makes it
// explicit for both the unprefixed and "node:"-prefixed spellings used across the source.
external: ["node:sqlite"],
// Baked in at build time since the binary has no package.json on disk to read at runtime once
// distributed standalone — `wire-paseo` needs its own version to pick the matching GitHub Release
// asset.
define: { __SESSIONFORGE_VERSION__: JSON.stringify(version) },
logLevel: "info",
});

Expand Down
93 changes: 93 additions & 0 deletions packages/cli/src/cli/bin.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,43 @@
#!/usr/bin/env -S npx tsx
import { readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { AiderAdapter } from "../core/aider-adapter.server.js";
import { ClaudeCodeAdapter } from "../core/claude-adapter.server.js";
import { CodexAdapter } from "../core/codex-adapter.server.js";
import { runDiscovery } from "../core/discover.server.js";
import { GeminiCliAdapter } from "../core/gemini-adapter.server.js";
import { archiveSession, restoreSession, runCleanup, SessionNotFoundError } from "../core/lifecycle-actions.server.js";
import { OpenCodeAdapter } from "../core/opencode-adapter.server.js";
import {
arePluginsEnabled,
DEFAULT_PLUGIN_ID,
downloadPluginArchive,
extractPluginArchive,
getPluginStatus,
installPluginDirectory,
isPaseoCliAvailable,
PLUGIN_ARCHIVE_NAME,
pluginInstallDir,
} from "../core/paseo-wire.server.js";
import { SessionStore } from "../core/store.server.js";
import type { AgentId, ClassificationCategory, SessionLifecycle, SessionStatus } from "../core/types.server.js";
import { formatSessionDetail, formatSessionTable, parseOlderThan } from "./format.js";

const ADAPTERS = [new ClaudeCodeAdapter(), new CodexAdapter(), new GeminiCliAdapter(), new OpenCodeAdapter(), new AiderAdapter()];
const ACTOR = "cli";

// Baked in by esbuild's `define` when built into the standalone binary (see build-binary.mjs) — the
// binary has no package.json on disk at runtime to read its own version from otherwise. Falls back to
// reading packages/cli/package.json directly when running from raw source (tsx, no esbuild pass).
declare const __SESSIONFORGE_VERSION__: string | undefined;

function getVersion(): string {
if (typeof __SESSIONFORGE_VERSION__ !== "undefined") return __SESSIONFORGE_VERSION__;
const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")) as { version: string };
return pkg.version;
}

interface ParsedArgs {
positional: string[];
flags: Map<string, string | boolean>;
Expand Down Expand Up @@ -212,6 +237,68 @@ async function cmdAudit(args: ParsedArgs): Promise<void> {
}
}

/**
* Automates the manual "clone the repo, `paseo plugin install /path/to/it`" flow: downloads the
* version-matched Paseo plugin bundle from this CLI's own GitHub Release, extracts it to a stable local
* directory, and installs it via the real `paseo plugin install`. Never touches the daemon's
* `pluginsEnabled` switch itself — plugins are trusted, unsandboxed code, so enabling that is left to the
* user, in the Paseo app.
*/
async function cmdWirePaseo(args: ParsedArgs): Promise<void> {
if (!(await isPaseoCliAvailable())) {
console.error("The `paseo` CLI was not found on PATH. Install Paseo first, then re-run this command.");
process.exitCode = 1;
return;
}
console.log("paseo CLI found.");

if (!(await arePluginsEnabled())) {
console.error(
"\nPlugins are disabled on this Paseo daemon. Plugins are trusted, unsandboxed code — backend " +
"plugin code can access your daemon machine, including files, processes, credentials, and " +
"network services. Client plugin code runs inside the Paseo app.\n\n" +
"Enable plugins yourself first (Settings → Plugins → Enable plugins in the Paseo app), " +
"then re-run this command.",
);
process.exitCode = 1;
return;
}
console.log("Plugins are enabled on this daemon.");

const version = flagString(args.flags, "version") ?? getVersion();
const archivePath = join(tmpdir(), PLUGIN_ARCHIVE_NAME);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Create a private temporary archive

On a shared system, another local user can pre-create the predictable sessionforge-paseo-plugin.tar.gz path in the temp directory as a symlink to a victim-writable file. downloadPluginArchive opens this path with createWriteStream, which follows the symlink and truncates/writes the downloaded archive before extraction, so running wire-paseo can overwrite an arbitrary file owned by the caller. Use a uniquely created private temp directory or file (for example, via mkdtemp) and clean it up afterward.

Useful? React with 👍 / 👎.

console.log(`Downloading the v${version} Paseo plugin release asset...`);
await downloadPluginArchive(version, archivePath);

const installDir = pluginInstallDir();
console.log(`Extracting to ${installDir}...`);
await extractPluginArchive(archivePath, installDir);

const id = flagString(args.flags, "id") ?? DEFAULT_PLUGIN_ID;
console.log("Installing via `paseo plugin install`...");
const result = await installPluginDirectory(installDir, id);

if (result.status !== "running") {
console.error(`\nPlugin installed but is not running (status: ${result.status}).`);
if (result.error) console.error(result.error);
console.error(`Check \`paseo plugin logs ${id}\` for details.`);
process.exitCode = 1;
return;
}

console.log(`\nSessionForge is wired into Paseo (plugin id: ${id}, status: running).`);
}

async function cmdPaseoStatus(): Promise<void> {
const status = await getPluginStatus(DEFAULT_PLUGIN_ID);
if (!status) {
console.log(`Not installed. Run \`sessionforge wire-paseo\` to install it.`);
return;
}
console.log(`${status.id}: ${status.status}${status.enabled ? "" : " (disabled)"} — ${status.path}`);
if (status.error) console.log(`Error: ${status.error}`);
}

function printHelp(): void {
console.log(`sessionforge — unified agent session inventory (SessionForge)

Expand All @@ -224,6 +311,8 @@ Usage:
sessionforge archive <id> [--reason ...] move a session out of the active view
sessionforge restore <id> [--reason ...] bring an archived/trashed session back
sessionforge audit [id] [--json] show the audit trail for destructive operations
sessionforge wire-paseo [--version ...] download and install the Paseo plugin for this CLI's version
sessionforge paseo-status show whether the Paseo plugin is installed and running
`);
}

Expand All @@ -248,6 +337,10 @@ async function main(): Promise<void> {
return cmdRestore(args);
case "audit":
return cmdAudit(args);
case "wire-paseo":
return cmdWirePaseo(args);
case "paseo-status":
return cmdPaseoStatus();
case undefined:
case "help":
case "--help":
Expand Down
Loading
Loading