@frumu/tandem 0.3.3
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 +121 -0
- package/package.json +34 -0
- package/scripts/install.js +178 -0
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Tandem Engine CLI (npm Wrapper)
|
|
2
|
+
|
|
3
|
+
```text
|
|
4
|
+
TTTTT A N N DDDD EEEEE M M
|
|
5
|
+
T A A NN N D D E MM MM
|
|
6
|
+
T AAAAA N N N D D EEEE M M M
|
|
7
|
+
T A A N NN D D E M M
|
|
8
|
+
T A A N N DDDD EEEEE M M
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What This Is
|
|
12
|
+
|
|
13
|
+
Prebuilt npm distribution of the Tandem engine for macOS, Linux, and Windows.
|
|
14
|
+
Installing this package gives you the `tandem-engine` CLI binary without compiling Rust locally.
|
|
15
|
+
|
|
16
|
+
If you want to build from Rust source instead, use the crate docs in `engine/README.md`.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g @frumu/tandem
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The installer downloads the release asset that matches this package version. Tags and package versions are expected to match (for example, `v0.3.3`).
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
Start the engine server:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
tandem-engine serve --hostname 127.0.0.1 --port 39731
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
In a second terminal, you can start the TUI:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
tandem-tui
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
### Serve
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
tandem-engine serve --hostname 127.0.0.1 --port 39731
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Options include:
|
|
49
|
+
|
|
50
|
+
- `--hostname` or `--host`
|
|
51
|
+
- `--port`
|
|
52
|
+
- `--state-dir`
|
|
53
|
+
- `--provider`
|
|
54
|
+
- `--model`
|
|
55
|
+
- `--api-key`
|
|
56
|
+
- `--config`
|
|
57
|
+
- `--api-token`
|
|
58
|
+
|
|
59
|
+
### Run a Single Prompt
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
tandem-engine run "What is the capital of France?"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Run a Concurrent Batch
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
cat > tasks.json << 'JSON'
|
|
69
|
+
{
|
|
70
|
+
"tasks": [
|
|
71
|
+
{ "id": "plan", "prompt": "Create a 3-step rollout plan." },
|
|
72
|
+
{ "id": "risks", "prompt": "List top 5 rollout risks." },
|
|
73
|
+
{ "id": "comms", "prompt": "Draft a short launch update." }
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
JSON
|
|
77
|
+
|
|
78
|
+
tandem-engine parallel --json @tasks.json --concurrency 3
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Execute a Tool Directly
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
tandem-engine tool --json '{"tool":"workspace_list_files","args":{"path":"."}}'
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### List Providers
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
tandem-engine providers
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
|
|
95
|
+
Tandem Engine merges config from:
|
|
96
|
+
|
|
97
|
+
1. Environment variables
|
|
98
|
+
2. `managed_config.json`
|
|
99
|
+
3. Project config at `.tandem/config.json`
|
|
100
|
+
4. Global config:
|
|
101
|
+
- macOS/Linux: `~/.config/tandem/config.json`
|
|
102
|
+
- Windows: `%APPDATA%\tandem\config.json`
|
|
103
|
+
|
|
104
|
+
Common provider keys:
|
|
105
|
+
|
|
106
|
+
- `OPENAI_API_KEY`
|
|
107
|
+
- `ANTHROPIC_API_KEY`
|
|
108
|
+
- `OPENROUTER_API_KEY`
|
|
109
|
+
- `GROQ_API_KEY`
|
|
110
|
+
- `MISTRAL_API_KEY`
|
|
111
|
+
- `TOGETHER_API_KEY`
|
|
112
|
+
- `COHERE_API_KEY`
|
|
113
|
+
- `GITHUB_TOKEN` (Copilot)
|
|
114
|
+
- `AZURE_OPENAI_API_KEY`
|
|
115
|
+
- `VERTEX_API_KEY`
|
|
116
|
+
- `BEDROCK_API_KEY`
|
|
117
|
+
|
|
118
|
+
## Documentation
|
|
119
|
+
|
|
120
|
+
- Project docs: https://tandem.frumu.ai/docs
|
|
121
|
+
- GitHub releases: https://github.com/frumu-ai/tandem/releases
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@frumu/tandem",
|
|
3
|
+
"version": "0.3.3",
|
|
4
|
+
"description": "Tandem Engine binary distribution",
|
|
5
|
+
"homepage": "https://tandem.frumu.ai",
|
|
6
|
+
"bin": {
|
|
7
|
+
"tandem-engine": "bin/tandem-engine"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"postinstall": "node scripts/install.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"scripts"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/frumu-ai/tandem.git"
|
|
19
|
+
},
|
|
20
|
+
"author": "Frumu Ltd",
|
|
21
|
+
"license": "MIT OR Apache-2.0",
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"os": [
|
|
26
|
+
"darwin",
|
|
27
|
+
"linux",
|
|
28
|
+
"win32"
|
|
29
|
+
],
|
|
30
|
+
"cpu": [
|
|
31
|
+
"x64",
|
|
32
|
+
"arm64"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
// Configuration
|
|
7
|
+
const REPO = "frumu-ai/tandem";
|
|
8
|
+
const MIN_SIZE = 1024 * 1024; // 1MB
|
|
9
|
+
|
|
10
|
+
// Platform mapping
|
|
11
|
+
const PLATFORM_MAP = {
|
|
12
|
+
'win32': { os: 'windows', ext: '.exe' },
|
|
13
|
+
'darwin': { os: 'darwin', ext: '' },
|
|
14
|
+
'linux': { os: 'linux', ext: '' }
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const ARCH_MAP = {
|
|
18
|
+
'x64': 'x64',
|
|
19
|
+
'arm64': 'arm64'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function getArtifactInfo() {
|
|
23
|
+
const platform = PLATFORM_MAP[process.platform];
|
|
24
|
+
const arch = ARCH_MAP[process.arch];
|
|
25
|
+
|
|
26
|
+
if (!platform || !arch) {
|
|
27
|
+
throw new Error(`Unsupported platform: ${process.platform}-${process.arch}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let artifactName = `tandem-engine-${platform.os}-${arch}`;
|
|
31
|
+
// Handle specific artifact naming conventions (zip vs tar.gz)
|
|
32
|
+
if (platform.os === 'windows') {
|
|
33
|
+
artifactName += '.zip';
|
|
34
|
+
} else if (platform.os === 'darwin') {
|
|
35
|
+
artifactName += '.zip';
|
|
36
|
+
} else {
|
|
37
|
+
artifactName += '.tar.gz';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
artifactName,
|
|
42
|
+
binaryName: `tandem-engine${platform.ext}`,
|
|
43
|
+
isWindows: platform.os === 'windows'
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { artifactName, binaryName, isWindows } = getArtifactInfo();
|
|
48
|
+
const binDir = path.join(__dirname, '..', 'bin');
|
|
49
|
+
const destPath = path.join(binDir, binaryName);
|
|
50
|
+
|
|
51
|
+
if (!fs.existsSync(binDir)) {
|
|
52
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (fs.existsSync(destPath)) {
|
|
56
|
+
console.log("Binary already present.");
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Helper to fetch JSON from GitHub API
|
|
61
|
+
function fetchJson(url) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
https.get(url, { headers: { 'User-Agent': 'tandem-engine-installer' } }, (res) => {
|
|
64
|
+
if (res.statusCode !== 200) {
|
|
65
|
+
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
66
|
+
return fetchJson(res.headers.location).then(resolve).catch(reject);
|
|
67
|
+
}
|
|
68
|
+
return reject(new Error(`GitHub API HTTP ${res.statusCode}`));
|
|
69
|
+
}
|
|
70
|
+
let data = '';
|
|
71
|
+
res.on('data', chunk => data += chunk);
|
|
72
|
+
res.on('end', () => {
|
|
73
|
+
try { resolve(JSON.parse(data)); } catch (e) { reject(e); }
|
|
74
|
+
});
|
|
75
|
+
}).on('error', reject);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Simplified download
|
|
80
|
+
async function download() {
|
|
81
|
+
console.log(`Checking releases for ${REPO}...`);
|
|
82
|
+
const releases = await fetchJson(`https://api.github.com/repos/${REPO}/releases`);
|
|
83
|
+
|
|
84
|
+
// Get the version from package.json
|
|
85
|
+
const packageVersion = require('../package.json').version;
|
|
86
|
+
const targetTag = `v${packageVersion}`;
|
|
87
|
+
|
|
88
|
+
console.log(`Filtering releases for ${REPO} (Target: ${targetTag})...`);
|
|
89
|
+
// const releases = await fetchJson... <--- REMOVED DUPLICATE
|
|
90
|
+
|
|
91
|
+
// 1. Try to find the exact release for this package version
|
|
92
|
+
let release = releases.find(r => r.tag_name === targetTag);
|
|
93
|
+
|
|
94
|
+
if (!release) {
|
|
95
|
+
console.warn(`Warning: No release found for tag ${targetTag}. Checking for latest compatible assets...`);
|
|
96
|
+
// 2. Fallback: Find LATEST release that contains our asset (useful for nightly/beta where tags might differ)
|
|
97
|
+
release = releases.find(r => r.assets.some(a => a.name === artifactName));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!release) {
|
|
101
|
+
// Fallback: Check prereleases explicitly if strict filtering was on (it wasn't here)
|
|
102
|
+
// If not found, maybe name changed?
|
|
103
|
+
console.error(`Status: No release found with asset ${artifactName}`);
|
|
104
|
+
console.error("Available assets in latest:", releases[0]?.assets?.map(a => a.name));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const asset = release.assets.find(a => a.name === artifactName);
|
|
109
|
+
console.log(`Downloading ${asset.name} from ${release.tag_name}...`);
|
|
110
|
+
|
|
111
|
+
const file = fs.createWriteStream(path.join(binDir, artifactName));
|
|
112
|
+
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
const downloadUrl = asset.browser_download_url;
|
|
115
|
+
|
|
116
|
+
const request = (url) => {
|
|
117
|
+
https.get(url, { headers: { 'User-Agent': 'tandem-installer' } }, (res) => {
|
|
118
|
+
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
119
|
+
return request(res.headers.location);
|
|
120
|
+
}
|
|
121
|
+
if (res.statusCode !== 200) return reject(new Error(`Download failed: HTTP ${res.statusCode}`));
|
|
122
|
+
res.pipe(file);
|
|
123
|
+
file.on('finish', () => {
|
|
124
|
+
file.close();
|
|
125
|
+
resolve(path.join(binDir, artifactName));
|
|
126
|
+
});
|
|
127
|
+
}).on('error', err => {
|
|
128
|
+
fs.unlink(path.join(binDir, artifactName), () => { }); // cleanup
|
|
129
|
+
reject(err);
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
request(downloadUrl);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Extract
|
|
137
|
+
async function extract(archivePath) {
|
|
138
|
+
console.log("Extracting...");
|
|
139
|
+
if (isWindows) {
|
|
140
|
+
execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${binDir}' -Force"`);
|
|
141
|
+
} else {
|
|
142
|
+
if (artifactName.endsWith('.zip')) {
|
|
143
|
+
execSync(`unzip -o "${archivePath}" -d "${binDir}"`);
|
|
144
|
+
} else {
|
|
145
|
+
execSync(`tar -xzf "${archivePath}" -C "${binDir}"`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Cleanup archive
|
|
150
|
+
fs.unlinkSync(archivePath);
|
|
151
|
+
|
|
152
|
+
// Locate binary (it might be inside a folder?)
|
|
153
|
+
// Our release workflow:
|
|
154
|
+
// Windows: dist/tandem-engine.exe -> zipped -> dist/tandem-engine.exe
|
|
155
|
+
// Linux: dist/tandem-engine -> tar -> dist/tandem-engine
|
|
156
|
+
// So on extraction, it might extract 'dist/tandem-engine' or just 'tandem-engine'.
|
|
157
|
+
// We should check.
|
|
158
|
+
|
|
159
|
+
// If extraction creates a folder (common behavior), we need to find it.
|
|
160
|
+
// Assuming root extraction for now based on GHA inspect.
|
|
161
|
+
// 'dist' folder? Yes. The GHA zips "dist/*". So it extracts "tandem-engine.exe" directly if zip didn't preserve root?
|
|
162
|
+
// "Compress-Archive -Path "dist/*" ... " -> This usually puts files at root of zip.
|
|
163
|
+
|
|
164
|
+
if (fs.existsSync(destPath)) {
|
|
165
|
+
console.log("Verified binary extracted.");
|
|
166
|
+
if (!isWindows) fs.chmodSync(destPath, 0o755);
|
|
167
|
+
} else {
|
|
168
|
+
console.error("Binary not found at expected path:", destPath);
|
|
169
|
+
// List files
|
|
170
|
+
console.log("Files in bin:", fs.readdirSync(binDir));
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
download().then(extract).catch(err => {
|
|
176
|
+
console.error("Install failed:", err);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
});
|