epismo 1.4.0 → 1.6.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 CHANGED
@@ -1,7 +1,6 @@
1
1
  # Epismo CLI
2
2
 
3
- The CLI for [Epismo](https://epismo.ai), implemented in Go and distributed
4
- as a native executable.
3
+ Native command-line interface for [Epismo](https://epismo.ai). It is written in Go and returns JSON, making it useful in terminals, scripts, and agent workflows.
5
4
 
6
5
  ## Install
7
6
 
@@ -17,35 +16,41 @@ Windows PowerShell:
17
16
  irm https://epismo.ai/install.ps1 | iex
18
17
  ```
19
18
 
20
- With Go:
19
+ Or install with Go or npm:
21
20
 
22
21
  ```sh
23
22
  go install github.com/epismoai/cli/cmd/epismo@latest
23
+ npm install -g epismo
24
24
  ```
25
25
 
26
- With npm:
26
+ Prebuilt archives, standalone executables, and `checksums.txt` are available from [GitHub Releases](https://github.com/epismoai/cli/releases).
27
+
28
+ ## Updating
29
+
30
+ The CLI checks GitHub Releases for a newer version at most once every 24 hours in interactive terminals. When an update is available, run:
27
31
 
28
32
  ```sh
29
- npm install -g epismo
33
+ epismo update
30
34
  ```
31
35
 
32
- You can also download a platform archive, a raw executable, and `checksums.txt` from
33
- [GitHub Releases](https://github.com/epismoai/cli/releases).
36
+ The command identifies how the active executable was installed and returns the appropriate installation command; it never modifies the executable itself. Default shell installations receive the short command shown above. A custom installation directory is included only when needed. Go and global Node installations receive a manager-specific command when the package manager can be identified. Yarn's global command is returned only for Yarn Classic because modern Yarn does not support that workflow. Set `EPISMO_UPDATE_CHECK=0` to disable automatic checks.
34
37
 
35
- ## Usage
38
+ The npm package contains a dependency-free JavaScript launcher and all supported native binaries. It uses no lifecycle installation scripts and performs no additional downloads during installation. The launcher selects the binary for the current platform and passes its package-manager context to the native CLI. Shell and PowerShell installers write an installation receipt next to the executable; Go installations are identified from embedded build metadata. If the method cannot be determined safely, `epismo update` links to the update instructions without modifying the executable.
39
+
40
+ ## Quick start
36
41
 
37
42
  ```sh
38
43
  epismo login
39
- epismo whoami
40
44
  epismo workspace list
45
+ epismo workspace use WORKSPACE_ID # optional: save a default workspace
41
46
  epismo playbook search --query onboarding
42
- epismo case list --status open
43
- epismo task list
44
- epismo suggestion list
45
47
  ```
46
48
 
47
- Every successful command writes JSON to stdout. Warnings and errors are JSON on
48
- stderr, making the CLI suitable for both people and agents.
49
+ Run `epismo --help` for command groups, or append `--help` to any group or command for its options.
50
+
51
+ ## Output and input
52
+
53
+ Successful commands write JSON to stdout; warnings and errors write JSON to stderr. This keeps output easy to use from scripts and agents.
49
54
 
50
55
  ```json
51
56
  {
@@ -57,54 +62,41 @@ stderr, making the CLI suitable for both people and agents.
57
62
  }
58
63
  ```
59
64
 
60
- Use `epismo --help`, `epismo <group> --help`, or
61
- `epismo <group> <command> --help` for the complete command reference.
62
-
63
- ### Structured input
64
-
65
- Commands that accept request payloads support inline JSON, files, and stdin:
65
+ Commands that accept a request body support inline JSON, a file, or stdin:
66
66
 
67
67
  ```sh
68
68
  epismo playbook create --input @playbook.json
69
69
  epismo case record append CASE_ID --input - < record.json
70
70
  ```
71
71
 
72
- Explicit command-line options override fields from `--input`. Mutations generate
73
- an idempotency key unless `--idempotency-key` or an `idempotencyKey` in input is
74
- provided.
72
+ Explicit flags override fields supplied through `--input`. Mutations create an idempotency key automatically unless you provide `--idempotency-key` or `idempotencyKey` in the input.
75
73
 
76
- ### Authentication and configuration
74
+ ## Authentication and configuration
77
75
 
78
- Interactive login uses OAuth 2.0 Authorization Code with PKCE. Email login
79
- automatically selects organization SSO or a terminal email-code flow.
76
+ `epismo login` opens a browser-based OAuth login. With `--email`, it automatically uses your organization SSO when available, otherwise it prompts for an email code.
80
77
 
81
- For CI, issue a token and set it as an environment variable:
78
+ For CI or other non-interactive use, create a workspace-scoped token and pass it with `EPISMO_TOKEN`:
82
79
 
83
80
  ```sh
84
81
  epismo token create --workspace-id WORKSPACE_ID
85
82
  EPISMO_TOKEN=... epismo playbook search
86
83
  ```
87
84
 
88
- Configuration remains compatible with the original npm CLI and is stored under
89
- `~/.epismo`. Set `EPISMO_CONFIG_DIR` to use another location. For local API
90
- development, set `APP_ENV=dev`, or override endpoints directly with
91
- `EPISMO_API_URL` and `EPISMO_WEB_URL`.
85
+ Configuration is stored in `~/.epismo` (compatible with the original npm CLI). Set `EPISMO_CONFIG_DIR` to use a different directory. For local API development, set `APP_ENV=dev`, or override `EPISMO_API_URL` and `EPISMO_WEB_URL`.
92
86
 
93
87
  ## API contract
94
88
 
95
- The versioned OpenAPI 3.1 contract used by the CLI is checked in at
96
- `contracts/openapi.json` as a generated snapshot of the public contract served
97
- at `https://api.epismo.ai/openapi.json`. After deploying an API change, refresh
98
- the snapshot with `sh scripts/sync-openapi.sh`; do not edit it manually. Its
99
- `info.version` identifies the API compatibility generation (`1.0.0` for `/v1`)
100
- and is independent of the CLI release version. Contract tests verify that every
101
- remote CLI command uses a documented operation and that its query options remain
102
- compatible with the API. Releases run the same compatibility checks against the
103
- deployed contract before publishing.
89
+ [`contracts/openapi.json`](contracts/openapi.json) is a generated snapshot of Epismo's public OpenAPI 3.1 contract. Refresh it after an API deployment:
90
+
91
+ ```sh
92
+ sh scripts/sync-openapi.sh
93
+ ```
94
+
95
+ Do not edit the snapshot manually. Its `info.version` represents API compatibility (for example, `1.0.0` for `/v1`), not the CLI release version. Contract tests ensure remote commands and query options remain compatible with the API.
104
96
 
105
97
  ## Develop
106
98
 
107
- The runtime uses only the Go standard library.
99
+ The CLI runtime uses only the Go standard library.
108
100
 
109
101
  ```sh
110
102
  go test -race ./...
@@ -112,11 +104,8 @@ go vet ./...
112
104
  go build ./cmd/epismo
113
105
  ```
114
106
 
115
- Releases are built for macOS, Linux, and Windows from semantic-version tags such
116
- as `v1.3.1`. The tag is the sole source of the release version: the workflow
117
- injects it into Go binaries and sets the npm package version immediately before
118
- publishing. The development `package.json` version is intentionally not bumped.
107
+ Releases are built for macOS, Linux, and Windows from semantic-version tags such as `v1.3.1`. The tag is the release version source; the development `package.json` version is intentionally not updated.
119
108
 
120
109
  ## License
121
110
 
122
- Apache License 2.0.
111
+ [Apache License 2.0](LICENSE).
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { launch } from "../launcher.js";
4
+
5
+ try {
6
+ launch();
7
+ } catch (error) {
8
+ console.error(`epismo: ${error.message}`);
9
+ process.exitCode = 1;
10
+ }
@@ -0,0 +1,104 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, realpathSync } from "node:fs";
3
+ import { dirname, join, parse, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
7
+
8
+ export function targetFor(platform = process.platform, architecture = process.arch) {
9
+ const os = { darwin: "darwin", linux: "linux", win32: "windows" }[platform];
10
+ const arch = { x64: "amd64", arm64: "arm64" }[architecture];
11
+ return os && arch ? `${os}-${arch}` : null;
12
+ }
13
+
14
+ export function binaryFor(platform = process.platform, architecture = process.arch, root = packageRoot) {
15
+ const target = targetFor(platform, architecture);
16
+ if (!target) return null;
17
+ return join(root, "npm", "vendor", target, platform === "win32" ? "epismo.exe" : "epismo");
18
+ }
19
+
20
+ export function detectPackageManager({
21
+ root = packageRoot,
22
+ entrypoint = process.argv[1] || "",
23
+ userAgent = process.env.npm_config_user_agent || "",
24
+ execPath = process.env.npm_execpath || ""
25
+ } = {}) {
26
+ if (pnpmOwnsPackage(root, entrypoint)) return { name: "pnpm", version: versionFromUserAgent(userAgent, "pnpm") };
27
+ const normalizedRoot = root.toLowerCase().replaceAll("\\", "/");
28
+ if (/\bbun\//i.test(userAgent) || /(?:^|[\\/])bun(?:\.exe)?$/i.test(execPath) || normalizedRoot.includes("/.bun/install/global/")) {
29
+ return { name: "bun", version: versionFromUserAgent(userAgent, "bun") };
30
+ }
31
+ if (/\byarn\//i.test(userAgent) || /(?:^|[\\/])yarn(?:\.c?js|\.cmd)?$/i.test(execPath) || normalizedRoot.includes("/.config/yarn/global/")) {
32
+ return { name: "yarn", version: versionFromUserAgent(userAgent, "yarn") || "1" };
33
+ }
34
+ return { name: "npm", version: versionFromUserAgent(userAgent, "npm") };
35
+ }
36
+
37
+ export function installationScope(environment = process.env) {
38
+ return environment.npm_command === "exec" || environment.npm_lifecycle_event === "npx" ? "ephemeral" : "global";
39
+ }
40
+
41
+ export function launch() {
42
+ const binary = binaryFor();
43
+ if (!binary) {
44
+ throw new Error(`Unsupported platform: ${process.platform}-${process.arch}`);
45
+ }
46
+ if (!existsSync(binary)) {
47
+ throw new Error(`Epismo native binary is missing for ${process.platform}-${process.arch}. Reinstall the epismo package.`);
48
+ }
49
+ const manager = detectPackageManager();
50
+ const env = {
51
+ ...process.env,
52
+ EPISMO_DISTRIBUTION: "node",
53
+ EPISMO_NODE_MANAGER: manager.name,
54
+ EPISMO_NODE_MANAGER_VERSION: manager.version || "",
55
+ EPISMO_NODE_SCOPE: installationScope()
56
+ };
57
+ const child = spawn(binary, process.argv.slice(2), { stdio: "inherit", env });
58
+ const signalHandlers = new Map();
59
+ const removeSignalHandlers = () => {
60
+ for (const [signal, handler] of signalHandlers) process.off(signal, handler);
61
+ };
62
+ child.on("error", (error) => {
63
+ removeSignalHandlers();
64
+ console.error(error.message);
65
+ process.exitCode = 1;
66
+ });
67
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
68
+ const handler = () => {
69
+ if (!child.killed) child.kill(signal);
70
+ };
71
+ signalHandlers.set(signal, handler);
72
+ process.on(signal, handler);
73
+ }
74
+ child.on("exit", (code, signal) => {
75
+ removeSignalHandlers();
76
+ if (signal && process.platform !== "win32") {
77
+ process.kill(process.pid, signal);
78
+ return;
79
+ }
80
+ process.exitCode = code ?? 1;
81
+ });
82
+ }
83
+
84
+ function versionFromUserAgent(userAgent, manager) {
85
+ return userAgent.match(new RegExp(`(?:^|\\s)${manager}/([^\\s]+)`, "i"))?.[1] || "";
86
+ }
87
+
88
+ function pnpmOwnsPackage(root, entrypoint) {
89
+ for (const start of new Set([root, dirname(resolve(entrypoint || root))])) {
90
+ const filesystemRoot = parse(start).root;
91
+ for (let current = start; ; current = dirname(current)) {
92
+ const nodeModules = join(current, "node_modules");
93
+ if (existsSync(join(nodeModules, ".modules.yaml"))) {
94
+ try {
95
+ if (realpathSync(join(nodeModules, "epismo")) === realpathSync(root)) return true;
96
+ } catch {
97
+ // Keep looking for the owning pnpm node_modules directory.
98
+ }
99
+ }
100
+ if (current === filesystemRoot) break;
101
+ }
102
+ }
103
+ return false;
104
+ }
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "epismo",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Dependency-free CLI for discovering, authoring, and coordinating reusable AI Playbooks.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
- "homepage": "https://github.com/epismoai/cli",
7
+ "homepage": "https://epismo.ai",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/epismoai/cli.git"
@@ -17,16 +17,25 @@
17
17
  "agent",
18
18
  "cli"
19
19
  ],
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
20
23
  "bin": {
21
- "epismo": "npm/bin/epismo.exe"
24
+ "epismo": "npm/bin/epismo.js"
22
25
  },
23
26
  "files": [
24
- "npm/bin/epismo.exe",
25
- "npm/install.js",
27
+ "npm/bin/epismo.js",
28
+ "npm/launcher.js",
29
+ "npm/vendor/darwin-amd64/epismo",
30
+ "npm/vendor/darwin-arm64/epismo",
31
+ "npm/vendor/linux-amd64/epismo",
32
+ "npm/vendor/linux-arm64/epismo",
33
+ "npm/vendor/windows-amd64/epismo.exe",
34
+ "npm/vendor/windows-arm64/epismo.exe",
26
35
  "README.md",
27
36
  "LICENSE"
28
37
  ],
29
38
  "scripts": {
30
- "postinstall": "node npm/install.js"
39
+ "test": "node --test npm/launcher.test.js"
31
40
  }
32
41
  }
@@ -1 +0,0 @@
1
- Epismo native binary placeholder. The npm postinstall script replaces this file.
package/npm/install.js DELETED
@@ -1,82 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { chmod, readFile, rename, rm, writeFile } from "node:fs/promises";
3
- import { get } from "node:https";
4
- import { dirname, join } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
-
7
- const repository = "epismoai/cli";
8
- const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
9
- const target = join(packageRoot, "npm", "bin", "epismo.exe");
10
- const packageJson = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
11
- const version = packageJson.version;
12
- if (version === "0.0.0-development") {
13
- console.log("Skipping Epismo binary download for a development checkout.");
14
- process.exit(0);
15
- }
16
- if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
17
- throw new Error(`Invalid published package version: ${JSON.stringify(version)}`);
18
- }
19
-
20
- const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[process.platform];
21
- const architecture = { x64: "amd64", arm64: "arm64" }[process.arch];
22
- if (!platform || !architecture) {
23
- throw new Error(`Unsupported platform: ${process.platform}-${process.arch}`);
24
- }
25
-
26
- const extension = platform === "windows" ? ".exe" : "";
27
- const asset = `epismo_${platform}_${architecture}${extension}`;
28
- const releaseBase = process.env.EPISMO_RELEASE_BASE_URL || `https://github.com/${repository}/releases/download/v${version}`;
29
- const [binary, checksumFile] = await Promise.all([
30
- download(`${releaseBase}/${asset}`),
31
- download(`${releaseBase}/checksums.txt`)
32
- ]);
33
-
34
- const expected = checksumFile
35
- .toString("utf8")
36
- .split(/\r?\n/)
37
- .map((line) => line.trim().split(/\s+/))
38
- .find((parts) => parts[1] === asset)?.[0];
39
- if (!expected) {
40
- throw new Error(`Checksum not found for ${asset}`);
41
- }
42
- const actual = createHash("sha256").update(binary).digest("hex");
43
- if (actual !== expected.toLowerCase()) {
44
- throw new Error(`Checksum verification failed for ${asset}`);
45
- }
46
-
47
- const temporary = `${target}.${process.pid}.tmp`;
48
- await writeFile(temporary, binary, { mode: 0o755 });
49
- await chmod(temporary, 0o755).catch(() => {});
50
- await rm(target, { force: true });
51
- await rename(temporary, target);
52
-
53
- function download(url, redirects = 0) {
54
- if (redirects > 5) {
55
- return Promise.reject(new Error(`Too many redirects while downloading ${url}`));
56
- }
57
- return new Promise((resolve, reject) => {
58
- const request = get(url, { headers: { "user-agent": `epismo-npm/${version}` } }, (response) => {
59
- if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400) {
60
- const location = response.headers.location;
61
- response.resume();
62
- if (!location) {
63
- reject(new Error(`Redirect without Location from ${url}`));
64
- return;
65
- }
66
- resolve(download(new URL(location, url).toString(), redirects + 1));
67
- return;
68
- }
69
- if (response.statusCode !== 200) {
70
- response.resume();
71
- reject(new Error(`Download failed (${response.statusCode}) for ${url}`));
72
- return;
73
- }
74
- const chunks = [];
75
- response.on("data", (chunk) => chunks.push(chunk));
76
- response.on("end", () => resolve(Buffer.concat(chunks)));
77
- response.on("error", reject);
78
- });
79
- request.setTimeout(30_000, () => request.destroy(new Error(`Download timed out: ${url}`)));
80
- request.on("error", reject);
81
- });
82
- }