octiqflow 0.1.52

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 ADDED
@@ -0,0 +1,39 @@
1
+ # OctiqFlow
2
+
3
+ OctiqFlow is an agent workflow orchestrator: real terminals, Claude Code,
4
+ Codex, and pi.dev in one browser control plane.
5
+
6
+ ## Run now
7
+
8
+ ```bash
9
+ npx octiqflow
10
+ ```
11
+
12
+ This starts OctiqFlow in the foreground and prints the local URL. Node.js 18+
13
+ is the only build/runtime prerequisite; the matching native server and browser
14
+ client are installed automatically for macOS arm64/x64, Linux x64, or Windows
15
+ x64.
16
+
17
+ ## Install on macOS
18
+
19
+ ```bash
20
+ npm install -g octiqflow
21
+ octiqflow install
22
+ ```
23
+
24
+ The second command installs and starts a launchd user service, then opens the
25
+ authenticated local URL. It keeps running after the terminal closes and starts
26
+ again at login.
27
+
28
+ ```bash
29
+ octiqflow status
30
+ octiqflow restart
31
+ octiqflow open
32
+ octiqflow uninstall
33
+ ```
34
+
35
+ Uninstalling the service keeps profiles and transcripts under `~/.octiqflow`.
36
+ Agent CLIs are separate: install any of Claude Code, Codex, or pi.dev that you
37
+ want OctiqFlow to drive.
38
+
39
+ Project and source: <https://github.com/kyson8101/octiq-flow>
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { main } = require('../lib/cli.cjs');
4
+
5
+ main(process.argv.slice(2))
6
+ .then((code) => {
7
+ process.exitCode = code;
8
+ })
9
+ .catch((error) => {
10
+ console.error(`octiqflow: ${error.message}`);
11
+ process.exitCode = 1;
12
+ });
package/lib/cli.cjs ADDED
@@ -0,0 +1,171 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const { spawn } = require('node:child_process');
4
+ const { resolveRuntime, runForeground } = require('./runtime.cjs');
5
+ const {
6
+ installMacService,
7
+ restartMacService,
8
+ serviceStatus,
9
+ uninstallMacService,
10
+ } = require('./service.cjs');
11
+
12
+ const HELP = `OctiqFlow — agent workflow orchestration in your browser
13
+
14
+ Usage:
15
+ octiqflow Run in the foreground
16
+ octiqflow install [options] Install and start the macOS background service
17
+ octiqflow uninstall Remove the macOS background service
18
+ octiqflow status [options] Check the service and HTTP endpoint
19
+ octiqflow restart Restart the macOS background service
20
+ octiqflow open [options] Open the local browser client
21
+
22
+ Options:
23
+ --port <number> HTTP port (default: 1421)
24
+ --bind <address> Bind address (default: 127.0.0.1)
25
+ --no-open Do not open a browser after installation
26
+ -h, --help Show this help
27
+ -v, --version Show the package version
28
+
29
+ Node.js 18 or newer is required. Claude Code, Codex, and pi.dev are detected
30
+ separately; install whichever agent CLIs you want OctiqFlow to drive.
31
+ `;
32
+
33
+ function packageVersion() {
34
+ const manifest = path.join(__dirname, '..', 'package.json');
35
+ return JSON.parse(fs.readFileSync(manifest, 'utf8')).version;
36
+ }
37
+
38
+ function parseOptions(args) {
39
+ const options = {
40
+ port: Number(process.env.OCTIQ_WEB_PORT || 1421),
41
+ bind: process.env.OCTIQ_WEB_BIND || '127.0.0.1',
42
+ open: true,
43
+ };
44
+ const rest = [];
45
+ for (let index = 0; index < args.length; index += 1) {
46
+ const value = args[index];
47
+ if (value === '--port') {
48
+ const raw = args[++index];
49
+ if (raw == null) throw new Error('--port needs a value');
50
+ options.port = Number(raw);
51
+ } else if (value === '--bind') {
52
+ const raw = args[++index];
53
+ if (raw == null) throw new Error('--bind needs a value');
54
+ options.bind = raw;
55
+ } else if (value === '--no-open') {
56
+ options.open = false;
57
+ } else {
58
+ rest.push(value);
59
+ }
60
+ }
61
+ if (!Number.isInteger(options.port) || options.port < 1 || options.port > 65535) {
62
+ throw new Error('--port must be an integer from 1 to 65535');
63
+ }
64
+ return { options, rest };
65
+ }
66
+
67
+ async function localUrl(port, fetchImpl = fetch) {
68
+ const origin = `http://127.0.0.1:${port}`;
69
+ const tokenResponse = await fetchImpl(`${origin}/token`);
70
+ if (!tokenResponse.ok) throw new Error(`server answered ${tokenResponse.status} at /token`);
71
+ const token = (await tokenResponse.text()).trim();
72
+ return `${origin}/?token=${encodeURIComponent(token)}`;
73
+ }
74
+
75
+ async function waitForUrl(port, options = {}) {
76
+ const attempts = options.attempts || 30;
77
+ const delay = options.delay || 250;
78
+ const fetchImpl = options.fetch || fetch;
79
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
80
+ try {
81
+ const response = await fetchImpl(`http://127.0.0.1:${port}/healthz`);
82
+ if (response.ok) return localUrl(port, fetchImpl);
83
+ } catch (_error) {
84
+ // launchd has accepted the job but the socket is not ready yet.
85
+ }
86
+ await new Promise((resolve) => setTimeout(resolve, delay));
87
+ }
88
+ throw new Error(`the service started but did not listen on port ${port}`);
89
+ }
90
+
91
+ function openBrowser(url, platform = process.platform) {
92
+ const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
93
+ const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
94
+ const child = spawn(command, args, { detached: true, stdio: 'ignore' });
95
+ child.once('error', () => {});
96
+ child.unref();
97
+ }
98
+
99
+ async function main(args, dependencies = {}) {
100
+ const output = dependencies.output || console;
101
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
102
+ output.log(HELP.trimEnd());
103
+ return 0;
104
+ }
105
+ if (args.includes('--version') || args.includes('-v') || args[0] === 'version') {
106
+ output.log(packageVersion());
107
+ return 0;
108
+ }
109
+
110
+ const command = args[0] && !args[0].startsWith('-') ? args[0] : 'run';
111
+ const commandArgs = command === 'run' && args[0] !== 'run' ? args : args.slice(1);
112
+ const { options, rest } = parseOptions(commandArgs);
113
+ if (rest.length) throw new Error(`unknown argument: ${rest[0]}`);
114
+
115
+ if (command === 'run') {
116
+ const runtime = (dependencies.resolveRuntime || resolveRuntime)();
117
+ return (dependencies.runForeground || runForeground)(runtime, [], {
118
+ env: {
119
+ OCTIQ_WEB_BIND: options.bind,
120
+ OCTIQ_WEB_PORT: String(options.port),
121
+ },
122
+ });
123
+ }
124
+ if (command === 'install') {
125
+ const runtime = (dependencies.resolveRuntime || resolveRuntime)();
126
+ const details = (dependencies.installMacService || installMacService)(runtime, {
127
+ version: packageVersion(),
128
+ bind: options.bind,
129
+ port: options.port,
130
+ });
131
+ const url = await (dependencies.waitForUrl || waitForUrl)(options.port);
132
+ output.log(`OctiqFlow ${packageVersion()} is running as a background service.`);
133
+ output.log(url);
134
+ output.log(`Logs: ${details.logs}`);
135
+ if (options.open) (dependencies.openBrowser || openBrowser)(url);
136
+ return 0;
137
+ }
138
+ if (command === 'uninstall') {
139
+ (dependencies.uninstallMacService || uninstallMacService)();
140
+ output.log('OctiqFlow background service removed. Your profiles and transcripts were kept.');
141
+ return 0;
142
+ }
143
+ if (command === 'restart') {
144
+ (dependencies.restartMacService || restartMacService)();
145
+ const url = await (dependencies.waitForUrl || waitForUrl)(options.port);
146
+ output.log(`OctiqFlow restarted: ${url}`);
147
+ return 0;
148
+ }
149
+ if (command === 'status') {
150
+ const installed = (dependencies.serviceStatus || serviceStatus)();
151
+ let url = null;
152
+ try {
153
+ url = await (dependencies.localUrl || localUrl)(options.port);
154
+ } catch (_error) {
155
+ // Status reports both facts instead of turning an unavailable endpoint
156
+ // into an exception that hides whether launchd owns the service.
157
+ }
158
+ output.log(`Service: ${installed ? 'installed' : 'not installed'}`);
159
+ output.log(`Endpoint: ${url || 'not responding'}`);
160
+ return installed && url ? 0 : 1;
161
+ }
162
+ if (command === 'open') {
163
+ const url = await (dependencies.localUrl || localUrl)(options.port);
164
+ (dependencies.openBrowser || openBrowser)(url);
165
+ output.log(url);
166
+ return 0;
167
+ }
168
+ throw new Error(`unknown command: ${command}; run octiqflow --help`);
169
+ }
170
+
171
+ module.exports = { HELP, localUrl, main, openBrowser, packageVersion, parseOptions, waitForUrl };
@@ -0,0 +1,66 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const { spawn } = require('node:child_process');
4
+
5
+ const PLATFORM_PACKAGES = Object.freeze({
6
+ 'darwin-arm64': 'octiqflow-darwin-arm64',
7
+ 'darwin-x64': 'octiqflow-darwin-x64',
8
+ 'linux-x64': 'octiqflow-linux-x64',
9
+ 'win32-x64': '@kyson8101/octiqflow-win32-x64',
10
+ });
11
+
12
+ function packageFor(platform = process.platform, arch = process.arch) {
13
+ const key = `${platform}-${arch}`;
14
+ const packageName = PLATFORM_PACKAGES[key];
15
+ if (!packageName) {
16
+ const supported = Object.keys(PLATFORM_PACKAGES).join(', ');
17
+ throw new Error(`unsupported platform ${key}; supported platforms: ${supported}`);
18
+ }
19
+ return packageName;
20
+ }
21
+
22
+ function resolveRuntime(options = {}) {
23
+ const platform = options.platform || process.platform;
24
+ const arch = options.arch || process.arch;
25
+ const resolve = options.resolve || require.resolve;
26
+ const exists = options.exists || fs.existsSync;
27
+ const packageName = packageFor(platform, arch);
28
+ let manifest;
29
+ try {
30
+ manifest = resolve(`${packageName}/package.json`);
31
+ } catch (_error) {
32
+ throw new Error(
33
+ `the runtime package ${packageName} is missing; reinstall without --omit=optional`,
34
+ );
35
+ }
36
+
37
+ const root = path.dirname(manifest);
38
+ const binaryName = platform === 'win32' ? 'octiq-server.exe' : 'octiq-server';
39
+ const binary = path.join(root, 'bin', binaryName);
40
+ const client = path.join(root, 'bin', 'v2');
41
+ if (!exists(binary) || !exists(path.join(client, 'index.html'))) {
42
+ throw new Error(`${packageName} is incomplete; reinstall octiqflow`);
43
+ }
44
+ return { packageName, root, binary, client, platform, arch };
45
+ }
46
+
47
+ function runForeground(runtime, args = [], options = {}) {
48
+ const spawnImpl = options.spawn || spawn;
49
+ return new Promise((resolve, reject) => {
50
+ const child = spawnImpl(runtime.binary, args, {
51
+ cwd: process.cwd(),
52
+ env: { ...process.env, OCTIQ_WEB: '1', ...(options.env || {}) },
53
+ stdio: 'inherit',
54
+ });
55
+ child.once('error', reject);
56
+ child.once('exit', (code, signal) => {
57
+ if (signal) {
58
+ process.kill(process.pid, signal);
59
+ return;
60
+ }
61
+ resolve(code == null ? 1 : code);
62
+ });
63
+ });
64
+ }
65
+
66
+ module.exports = { PLATFORM_PACKAGES, packageFor, resolveRuntime, runForeground };
@@ -0,0 +1,177 @@
1
+ const fs = require('node:fs');
2
+ const os = require('node:os');
3
+ const path = require('node:path');
4
+ const { spawnSync } = require('node:child_process');
5
+
6
+ const LABEL = 'com.kyson.octiqflow.server';
7
+
8
+ function xml(value) {
9
+ return String(value)
10
+ .replaceAll('&', '&amp;')
11
+ .replaceAll('<', '&lt;')
12
+ .replaceAll('>', '&gt;')
13
+ .replaceAll('"', '&quot;')
14
+ .replaceAll("'", '&apos;');
15
+ }
16
+
17
+ function servicePaths(home = os.homedir(), version = 'development') {
18
+ const base = path.join(home, '.octiqflow');
19
+ return {
20
+ base,
21
+ logs: path.join(base, 'logs'),
22
+ runtime: path.join(base, 'runtimes', version),
23
+ plist: path.join(home, 'Library', 'LaunchAgents', `${LABEL}.plist`),
24
+ };
25
+ }
26
+
27
+ function plist({ binary, bind, port, home, shell }) {
28
+ const logs = path.join(home, '.octiqflow', 'logs');
29
+ const searchPath = [
30
+ path.join(home, '.local', 'bin'),
31
+ '/opt/homebrew/bin',
32
+ '/usr/local/bin',
33
+ '/usr/bin',
34
+ '/bin',
35
+ '/usr/sbin',
36
+ '/sbin',
37
+ ].join(':');
38
+ return `<?xml version="1.0" encoding="UTF-8"?>
39
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
40
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
41
+ <plist version="1.0">
42
+ <dict>
43
+ <key>Label</key><string>${LABEL}</string>
44
+ <key>ProgramArguments</key>
45
+ <array><string>${xml(binary)}</string></array>
46
+ <key>EnvironmentVariables</key>
47
+ <dict>
48
+ <key>OCTIQ_WEB</key><string>1</string>
49
+ <key>OCTIQ_WEB_BIND</key><string>${xml(bind)}</string>
50
+ <key>OCTIQ_WEB_PORT</key><string>${xml(port)}</string>
51
+ <key>PATH</key><string>${xml(searchPath)}</string>
52
+ <key>SHELL</key><string>${xml(shell)}</string>
53
+ </dict>
54
+ <key>RunAtLoad</key><true/>
55
+ <key>KeepAlive</key><true/>
56
+ <key>ThrottleInterval</key><integer>10</integer>
57
+ <key>ProcessType</key><string>Background</string>
58
+ <key>StandardOutPath</key><string>${xml(path.join(logs, 'server.log'))}</string>
59
+ <key>StandardErrorPath</key><string>${xml(path.join(logs, 'server.err.log'))}</string>
60
+ </dict>
61
+ </plist>
62
+ `;
63
+ }
64
+
65
+ function runLaunchctl(args, options = {}) {
66
+ const run = options.run || spawnSync;
67
+ return run('launchctl', args, { encoding: 'utf8', stdio: options.stdio || 'pipe' });
68
+ }
69
+
70
+ function sleep(milliseconds) {
71
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
72
+ }
73
+
74
+ function replaceDirectory(source, destination) {
75
+ const parent = path.dirname(destination);
76
+ const temporary = `${destination}.install-${process.pid}-${Date.now()}`;
77
+ fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
78
+ fs.cpSync(source, temporary, { recursive: true });
79
+ fs.rmSync(destination, { recursive: true, force: true });
80
+ fs.renameSync(temporary, destination);
81
+ }
82
+
83
+ function installMacService(runtime, options = {}) {
84
+ if ((options.platform || process.platform) !== 'darwin') {
85
+ throw new Error('background service installation is currently supported on macOS only');
86
+ }
87
+ const home = options.home || os.homedir();
88
+ const version = options.version || 'development';
89
+ const bind = options.bind || '127.0.0.1';
90
+ const port = Number(options.port || 1421);
91
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
92
+ throw new Error('port must be an integer from 1 to 65535');
93
+ }
94
+
95
+ const paths = servicePaths(home, version);
96
+ const binDir = path.join(paths.runtime, 'bin');
97
+ fs.mkdirSync(paths.logs, { recursive: true, mode: 0o700 });
98
+ fs.mkdirSync(binDir, { recursive: true, mode: 0o700 });
99
+ const installedBinary = path.join(binDir, 'octiq-server');
100
+ fs.copyFileSync(runtime.binary, installedBinary);
101
+ fs.chmodSync(installedBinary, 0o755);
102
+ replaceDirectory(runtime.client, path.join(binDir, 'v2'));
103
+
104
+ fs.mkdirSync(path.dirname(paths.plist), { recursive: true });
105
+ const body = plist({
106
+ binary: installedBinary,
107
+ bind,
108
+ port,
109
+ home,
110
+ shell: process.env.SHELL || '/bin/zsh',
111
+ });
112
+ const temporaryPlist = `${paths.plist}.install-${process.pid}`;
113
+ fs.writeFileSync(temporaryPlist, body, { mode: 0o600 });
114
+ fs.renameSync(temporaryPlist, paths.plist);
115
+
116
+ const domain = `gui/${options.uid == null ? process.getuid() : options.uid}`;
117
+ runLaunchctl(['bootout', `${domain}/${LABEL}`], options);
118
+ for (let attempt = 0; attempt < 20; attempt += 1) {
119
+ if (runLaunchctl(['print', `${domain}/${LABEL}`], options).status !== 0) break;
120
+ sleep(250);
121
+ }
122
+ let started = false;
123
+ let lastError = '';
124
+ for (let attempt = 0; attempt < 5; attempt += 1) {
125
+ const result = runLaunchctl(['bootstrap', domain, paths.plist], options);
126
+ if (result.status === 0) {
127
+ started = true;
128
+ break;
129
+ }
130
+ lastError = (result.stderr || result.stdout || '').trim();
131
+ sleep(500);
132
+ }
133
+ if (!started) {
134
+ throw new Error(`launchd could not start OctiqFlow${lastError ? `: ${lastError}` : ''}`);
135
+ }
136
+ return { ...paths, binary: installedBinary, bind, port };
137
+ }
138
+
139
+ function uninstallMacService(options = {}) {
140
+ if ((options.platform || process.platform) !== 'darwin') {
141
+ throw new Error('background service installation is currently supported on macOS only');
142
+ }
143
+ const home = options.home || os.homedir();
144
+ const paths = servicePaths(home);
145
+ const domain = `gui/${options.uid == null ? process.getuid() : options.uid}`;
146
+ runLaunchctl(['bootout', `${domain}/${LABEL}`], options);
147
+ fs.rmSync(paths.plist, { force: true });
148
+ return paths.plist;
149
+ }
150
+
151
+ function serviceStatus(options = {}) {
152
+ if ((options.platform || process.platform) !== 'darwin') return false;
153
+ const domain = `gui/${options.uid == null ? process.getuid() : options.uid}`;
154
+ return runLaunchctl(['print', `${domain}/${LABEL}`], options).status === 0;
155
+ }
156
+
157
+ function restartMacService(options = {}) {
158
+ if ((options.platform || process.platform) !== 'darwin') {
159
+ throw new Error('background service installation is currently supported on macOS only');
160
+ }
161
+ const domain = `gui/${options.uid == null ? process.getuid() : options.uid}`;
162
+ const result = runLaunchctl(['kickstart', '-k', `${domain}/${LABEL}`], options);
163
+ if (result.status !== 0) {
164
+ throw new Error('OctiqFlow service is not installed');
165
+ }
166
+ }
167
+
168
+ module.exports = {
169
+ LABEL,
170
+ installMacService,
171
+ plist,
172
+ restartMacService,
173
+ servicePaths,
174
+ serviceStatus,
175
+ uninstallMacService,
176
+ xml,
177
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "octiqflow",
3
+ "version": "0.1.52",
4
+ "description": "Agent workflow orchestrator with real browser terminals",
5
+ "bin": {
6
+ "octiqflow": "bin/octiqflow.cjs"
7
+ },
8
+ "files": [
9
+ "bin",
10
+ "lib",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "optionalDependencies": {
17
+ "octiqflow-darwin-arm64": "0.1.52",
18
+ "octiqflow-darwin-x64": "0.1.52",
19
+ "octiqflow-linux-x64": "0.1.52",
20
+ "@kyson8101/octiqflow-win32-x64": "0.1.52"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/kyson8101/octiq-flow.git"
25
+ },
26
+ "homepage": "https://github.com/kyson8101/octiq-flow#readme",
27
+ "bugs": "https://github.com/kyson8101/octiq-flow/issues",
28
+ "license": "UNLICENSED",
29
+ "publishConfig": {
30
+ "access": "public"
31
+ }
32
+ }