@resolflow/cli 0.0.0-stage → 0.1.0-beta.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/dist/log.js ADDED
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ /**
3
+ * Logging for the CLI. Rules (CA-08): never log a prompt, an answer or a secret. Call sites only pass short
4
+ * operational messages (ids, counts, durations); `redact` is a second line of defence for tokens and keys.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.redact = redact;
8
+ exports.createLogger = createLogger;
9
+ const TOKEN_PATTERNS = [
10
+ /cli_[0-9a-f]{32}_[0-9a-f]{64}/gi, // device tokens
11
+ /cli_[0-9a-f]{64}/gi, // legacy tokens
12
+ /\b(sk|rk|pk)-[A-Za-z0-9_-]{12,}/g, // provider keys (OpenAI sk-…, Anthropic sk-ant-…)
13
+ /\bAIza[0-9A-Za-z_-]{20,}/g, // Google API keys
14
+ /(authorization:\s*bearer\s+)\S+/gi,
15
+ /([?&#](token|t|key|secret)=)[^&\s]+/gi,
16
+ ];
17
+ function redact(text) {
18
+ let out = String(text);
19
+ for (const re of TOKEN_PATTERNS)
20
+ out = out.replace(re, (m, p1) => (typeof p1 === 'string' && m.startsWith(p1) ? `${p1}[redacted]` : '[redacted]'));
21
+ return out;
22
+ }
23
+ function createLogger(opts = {}) {
24
+ const sink = opts.sink ?? ((line) => process.stderr.write(line + '\n'));
25
+ const at = () => new Date().toISOString().slice(11, 19);
26
+ return {
27
+ info: (m) => sink(`[${at()}] ${redact(m)}`),
28
+ warn: (m) => sink(`[${at()}] warn: ${redact(m)}`),
29
+ error: (m) => sink(`[${at()}] error: ${redact(m)}`),
30
+ debug: (m) => {
31
+ if (opts.verbose)
32
+ sink(`[${at()}] debug: ${redact(m)}`);
33
+ },
34
+ };
35
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PROTOCOL_VERSION = void 0;
4
+ exports.isJob = isJob;
5
+ /**
6
+ * Device protocol v2 (mirror of backend/src/cli-bridge/device-protocol.ts — keep the two in step).
7
+ *
8
+ * server → device job { jobId, kind, adapter?, payload }
9
+ * server → device job_cancel { jobId }
10
+ * server → device job_input { jobId, questionId, answer } (answer to ask_user; P4 Mode B)
11
+ * device → server job_event { jobId, seq, event: JobEvent }
12
+ */
13
+ exports.PROTOCOL_VERSION = 2;
14
+ function isJob(v) {
15
+ return (!!v &&
16
+ typeof v.jobId === 'string' &&
17
+ v.jobId.length > 0 &&
18
+ v.jobId.length <= 100 &&
19
+ (v.kind === 'llm_request' || v.kind === 'agent_job') &&
20
+ (v.adapter === undefined || typeof v.adapter === 'string') &&
21
+ !!v.payload &&
22
+ typeof v.payload === 'object');
23
+ }
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.RunLog = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const config_1 = require("./config");
40
+ const MAX_ENTRIES = 1000;
41
+ const ALLOWED_KEYS = ['at', 'requestId', 'model', 'status', 'errorKind', 'ms', 'promptTokens', 'completionTokens'];
42
+ class RunLog {
43
+ file;
44
+ constructor(dir) {
45
+ this.file = path.join(dir, 'runs.log');
46
+ }
47
+ append(entry) {
48
+ try {
49
+ // Whitelist the fields: nothing else (e.g. a payload) can ever end up in the file
50
+ const clean = {};
51
+ for (const k of ALLOWED_KEYS)
52
+ if (entry[k] !== undefined)
53
+ clean[k] = entry[k];
54
+ (0, config_1.ensurePrivateDir)(path.dirname(this.file));
55
+ fs.appendFileSync(this.file, JSON.stringify(clean) + '\n', { mode: 0o600 });
56
+ const lines = fs.readFileSync(this.file, 'utf8').split('\n').filter(Boolean);
57
+ if (lines.length > MAX_ENTRIES * 1.2)
58
+ (0, config_1.writePrivateFile)(this.file, lines.slice(-MAX_ENTRIES).join('\n') + '\n');
59
+ }
60
+ catch {
61
+ /* the log never breaks serving */
62
+ }
63
+ }
64
+ recent(n = 10) {
65
+ try {
66
+ return fs
67
+ .readFileSync(this.file, 'utf8')
68
+ .split('\n')
69
+ .filter(Boolean)
70
+ .slice(-n)
71
+ .map((l) => JSON.parse(l));
72
+ }
73
+ catch {
74
+ return [];
75
+ }
76
+ }
77
+ }
78
+ exports.RunLog = RunLog;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ /** How this process was started and how it talks to the terminal (RF-94502d0b). */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.NPX_PREFIX = void 0;
5
+ exports.skipUpdateCheck = skipUpdateCheck;
6
+ exports.invokedViaNpx = invokedViaNpx;
7
+ exports.cliCommand = cliCommand;
8
+ exports.guardPipes = guardPipes;
9
+ exports.NPX_PREFIX = 'npx @resolflow/cli';
10
+ /** `--version` / `--help` / no command: answered and exited by commander at once, so no update check (it could not finish). */
11
+ function skipUpdateCheck(argv = process.argv) {
12
+ const args = argv.slice(2);
13
+ return args.length === 0 || args[0] === 'help' || args.some((a) => a === '--version' || a === '-V' || a === '--help' || a === '-h');
14
+ }
15
+ /** Started through `npx @resolflow/cli …` (npm exec sets npm_command=exec; npx installs into the npm cache's _npx). */
16
+ function invokedViaNpx(env = process.env, argv = process.argv) {
17
+ return env.npm_command === 'exec' || /[\\/]_npx[\\/]/.test(argv[1] ?? '');
18
+ }
19
+ /** How the user runs a command of this CLI: `resolflow start` or `npx @resolflow/cli start`. */
20
+ function cliCommand(sub, env = process.env, argv = process.argv) {
21
+ return `${invokedViaNpx(env, argv) ? exports.NPX_PREFIX : 'resolflow'} ${sub}`;
22
+ }
23
+ /**
24
+ * `resolflow status | head -1`: the reader closes the pipe and the next write fails with EPIPE. That is not an
25
+ * error of ours — exit quietly instead of printing a stack trace. Other stream errors still surface.
26
+ */
27
+ function guardPipes(streams = [process.stdout, process.stderr], exit = (c) => process.exit(c)) {
28
+ for (const s of streams) {
29
+ s.on('error', (err) => {
30
+ if (err?.code === 'EPIPE')
31
+ exit(0);
32
+ else
33
+ setImmediate(() => {
34
+ throw err;
35
+ });
36
+ });
37
+ }
38
+ }
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SAFE_SECRET = exports.FileSecretStore = void 0;
37
+ exports.keychainAccount = keychainAccount;
38
+ exports.agentKeyEntry = agentKeyEntry;
39
+ exports.chooseSecretStore = chooseSecretStore;
40
+ const child_process_1 = require("child_process");
41
+ const crypto = __importStar(require("crypto"));
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const config_1 = require("./config");
45
+ const SERVICE = 'resolflow-cli';
46
+ /**
47
+ * The keychain account of a config directory: one entry per config dir (RESOLFLOW_CONFIG_DIR), so two CLIs on one
48
+ * machine (another user profile, staging next to production) never share or overwrite each other's token.
49
+ */
50
+ function keychainAccount(dir) {
51
+ return `device-token:${crypto.createHash('sha256').update(path.resolve(dir)).digest('hex').slice(0, 16)}`;
52
+ }
53
+ function run(cmd, args, stdin) {
54
+ return new Promise((resolve) => {
55
+ const child = (0, child_process_1.spawn)(cmd, args, { stdio: ['pipe', 'pipe', 'ignore'], shell: false });
56
+ let stdout = '';
57
+ child.stdout.on('data', (d) => (stdout += d.toString()));
58
+ child.on('error', () => resolve({ code: -1, stdout: '' }));
59
+ child.on('close', (code) => resolve({ code: code ?? -1, stdout }));
60
+ if (stdin !== undefined)
61
+ child.stdin.write(stdin);
62
+ child.stdin.end();
63
+ });
64
+ }
65
+ class FileSecretStore {
66
+ kind = 'file';
67
+ file;
68
+ constructor(dir, fileName = 'device-token') {
69
+ this.file = path.join(dir, fileName);
70
+ }
71
+ async get() {
72
+ try {
73
+ if (process.platform !== 'win32') {
74
+ const mode = fs.statSync(this.file).mode & 0o777;
75
+ if (mode & 0o077)
76
+ fs.chmodSync(this.file, 0o600); // tighten a loosened file
77
+ }
78
+ return fs.readFileSync(this.file, 'utf8').trim() || null;
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ async set(secret) {
85
+ (0, config_1.writePrivateFile)(this.file, secret + '\n');
86
+ }
87
+ async delete() {
88
+ try {
89
+ fs.unlinkSync(this.file);
90
+ }
91
+ catch {
92
+ /* not there */
93
+ }
94
+ }
95
+ }
96
+ exports.FileSecretStore = FileSecretStore;
97
+ /** Characters a secret may use (device tokens, vendor API keys): nothing the `security -i` command parser treats specially. */
98
+ exports.SAFE_SECRET = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,511}$/;
99
+ class MacKeychainStore {
100
+ account;
101
+ label;
102
+ kind = 'macos-keychain';
103
+ static BIN = '/usr/bin/security';
104
+ constructor(account, label = 'Resolflow CLI device') {
105
+ this.account = account;
106
+ this.label = label;
107
+ }
108
+ async get() {
109
+ return new Promise((resolve) => {
110
+ (0, child_process_1.execFile)(MacKeychainStore.BIN, ['find-generic-password', '-a', this.account, '-s', SERVICE, '-w'], (err, stdout) => resolve(err ? null : stdout.trim() || null));
111
+ });
112
+ }
113
+ async set(secret) {
114
+ if (!exports.SAFE_SECRET.test(secret))
115
+ throw new Error('Unexpected secret format');
116
+ // `security -i` reads commands from stdin: the secret never appears in a process list
117
+ const r = await run(MacKeychainStore.BIN, ['-i'], `add-generic-password -U -a ${this.account} -s ${SERVICE} -l "${this.label}" -w ${secret}\n`);
118
+ if (r.code !== 0)
119
+ throw new Error('Could not write to the macOS keychain');
120
+ }
121
+ async delete() {
122
+ await run(MacKeychainStore.BIN, ['delete-generic-password', '-a', this.account, '-s', SERVICE]);
123
+ }
124
+ }
125
+ class LibsecretStore {
126
+ bin;
127
+ account;
128
+ label;
129
+ kind = 'libsecret';
130
+ constructor(bin, account, label = 'Resolflow CLI device') {
131
+ this.bin = bin;
132
+ this.account = account;
133
+ this.label = label;
134
+ }
135
+ async get() {
136
+ const r = await run(this.bin, ['lookup', 'service', SERVICE, 'account', this.account]);
137
+ return r.code === 0 ? r.stdout.trim() || null : null;
138
+ }
139
+ async set(secret) {
140
+ const r = await run(this.bin, ['store', `--label=${this.label}`, 'service', SERVICE, 'account', this.account], secret);
141
+ if (r.code !== 0)
142
+ throw new Error('Could not write to the Secret Service (libsecret)');
143
+ }
144
+ async delete() {
145
+ await run(this.bin, ['clear', 'service', SERVICE, 'account', this.account]);
146
+ }
147
+ }
148
+ class KeytarStore {
149
+ keytar;
150
+ account;
151
+ kind = 'keytar';
152
+ constructor(keytar, account) {
153
+ this.keytar = keytar;
154
+ this.account = account;
155
+ }
156
+ async get() {
157
+ return (await this.keytar.getPassword(SERVICE, this.account)) ?? null;
158
+ }
159
+ async set(secret) {
160
+ await this.keytar.setPassword(SERVICE, this.account, secret);
161
+ }
162
+ async delete() {
163
+ await this.keytar.deletePassword(SERVICE, this.account);
164
+ }
165
+ }
166
+ function loadKeytar() {
167
+ try {
168
+ // Optional dependency: only when the user installed it
169
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
170
+ return require('keytar');
171
+ }
172
+ catch {
173
+ return null;
174
+ }
175
+ }
176
+ const LIBSECRET_BINS = ['/usr/bin/secret-tool', '/usr/local/bin/secret-tool'];
177
+ function agentKeyEntry(dir, tool) {
178
+ if (!/^[a-z0-9-]{1,40}$/.test(tool))
179
+ throw new Error('Unknown tool');
180
+ const hash = crypto.createHash('sha256').update(path.resolve(dir)).digest('hex').slice(0, 16);
181
+ return { account: `agent-key:${tool}:${hash}`, fileName: `agent-key-${tool}`, label: `Resolflow CLI ${tool} API key`, what: `the ${tool} API key` };
182
+ }
183
+ /** The best store available here (see the header). `warning` is set when only the file store is left. */
184
+ function chooseSecretStore(dir, env = process.env, entry) {
185
+ const forced = env.RESOLFLOW_SECRET_STORE;
186
+ const account = entry?.account ?? keychainAccount(dir);
187
+ const label = entry?.label ?? 'Resolflow CLI device';
188
+ const file = new FileSecretStore(dir, entry?.fileName);
189
+ const fileWarning = `No OS keychain available: ${entry?.what ?? 'the device token'} is stored in ${file.file} (readable by your user only). Install "keytar" or libsecret's secret-tool for keychain storage.`;
190
+ if (forced === 'file')
191
+ return { store: file, warning: fileWarning };
192
+ if (forced !== 'keychain') {
193
+ const keytar = loadKeytar();
194
+ if (keytar)
195
+ return { store: new KeytarStore(keytar, account) };
196
+ if (forced === 'keytar')
197
+ return { store: file, warning: fileWarning };
198
+ }
199
+ if (process.platform === 'darwin' && fs.existsSync(MacKeychainStore.BIN))
200
+ return { store: new MacKeychainStore(account, label) };
201
+ if (process.platform === 'linux') {
202
+ const bin = LIBSECRET_BINS.find((b) => fs.existsSync(b));
203
+ if (bin && (env.DBUS_SESSION_BUS_ADDRESS || env.DISPLAY || env.WAYLAND_DISPLAY))
204
+ return { store: new LibsecretStore(bin, account, label) };
205
+ }
206
+ return { store: file, warning: fileWarning };
207
+ }
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CHECK_INTERVAL_MS = exports.DEFAULT_REGISTRY = exports.PACKAGE_NAME = void 0;
37
+ exports.updateCheckDisabled = updateCheckDisabled;
38
+ exports.compareVersions = compareVersions;
39
+ exports.readUpdateCache = readUpdateCache;
40
+ exports.writeUpdateCache = writeUpdateCache;
41
+ exports.fetchLatestVersion = fetchLatestVersion;
42
+ exports.isStandaloneBinary = isStandaloneBinary;
43
+ exports.updateHint = updateHint;
44
+ exports.startUpdateCheck = startUpdateCheck;
45
+ const fs = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
47
+ /**
48
+ * Non-blocking "a newer version is available" hint (RF-94502d0b). Asks the npm registry for the `latest` dist-tag of
49
+ * @resolflow/cli at most once a day (result cached in the config directory) and prints one line on stderr after the
50
+ * command. Never installs anything. Off with RESOLFLOW_NO_UPDATE_CHECK=1 (also NO_UPDATE_NOTIFIER, CI, or when stderr
51
+ * is not a terminal). Only the package name goes to the registry — no device, token or usage data.
52
+ */
53
+ exports.PACKAGE_NAME = '@resolflow/cli';
54
+ exports.DEFAULT_REGISTRY = 'https://registry.npmjs.org';
55
+ exports.CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
56
+ const CACHE_FILE = 'update-check.json';
57
+ function updateCheckDisabled(env = process.env, isTTY = !!process.stderr.isTTY) {
58
+ const on = (v) => !!v && v !== '0' && v.toLowerCase() !== 'false';
59
+ return on(env.RESOLFLOW_NO_UPDATE_CHECK) || on(env.NO_UPDATE_NOTIFIER) || on(env.CI) || !isTTY;
60
+ }
61
+ function parse(v) {
62
+ const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(v.trim());
63
+ if (!m)
64
+ return null;
65
+ return { core: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ? m[4].split('.') : [] };
66
+ }
67
+ /** Semver precedence: <0 when a < b, 0 when equal, >0 when a > b; NaN when either is not a version. */
68
+ function compareVersions(a, b) {
69
+ const pa = parse(a);
70
+ const pb = parse(b);
71
+ if (!pa || !pb)
72
+ return NaN;
73
+ for (let i = 0; i < 3; i++)
74
+ if (pa.core[i] !== pb.core[i])
75
+ return pa.core[i] - pb.core[i];
76
+ if (!pa.pre.length || !pb.pre.length)
77
+ return pb.pre.length - pa.pre.length; // a release beats its pre-releases
78
+ for (let i = 0; i < Math.max(pa.pre.length, pb.pre.length); i++) {
79
+ const x = pa.pre[i];
80
+ const y = pb.pre[i];
81
+ if (x === undefined)
82
+ return -1;
83
+ if (y === undefined)
84
+ return 1;
85
+ if (x === y)
86
+ continue;
87
+ const nx = /^\d+$/.test(x);
88
+ const ny = /^\d+$/.test(y);
89
+ if (nx && ny)
90
+ return Number(x) - Number(y);
91
+ if (nx !== ny)
92
+ return nx ? -1 : 1;
93
+ return x < y ? -1 : 1;
94
+ }
95
+ return 0;
96
+ }
97
+ function readUpdateCache(dir) {
98
+ try {
99
+ const data = JSON.parse(fs.readFileSync(path.join(dir, CACHE_FILE), 'utf8'));
100
+ return typeof data?.checkedAt === 'number' ? { checkedAt: data.checkedAt, latest: typeof data.latest === 'string' ? data.latest : undefined } : null;
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ }
106
+ function writeUpdateCache(dir, cache) {
107
+ try {
108
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
109
+ fs.writeFileSync(path.join(dir, CACHE_FILE), JSON.stringify(cache) + '\n', { mode: 0o600 });
110
+ }
111
+ catch {
112
+ /* best effort */
113
+ }
114
+ }
115
+ /** The registry's `latest` dist-tag, or null on any problem. */
116
+ async function fetchLatestVersion(registry, signal) {
117
+ try {
118
+ const res = await fetch(`${registry.replace(/\/+$/, '')}/-/package/${exports.PACKAGE_NAME.replace('/', '%2F')}/dist-tags`, {
119
+ headers: { accept: 'application/json' },
120
+ redirect: 'error',
121
+ signal,
122
+ });
123
+ if (!res.ok)
124
+ return null;
125
+ const tags = (await res.json());
126
+ return typeof tags.latest === 'string' && parse(tags.latest) ? tags.latest : null;
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ }
132
+ /** True when this process is a standalone binary (Node single executable application). */
133
+ function isStandaloneBinary() {
134
+ try {
135
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
136
+ return !!require('node:sea').isSea();
137
+ }
138
+ catch {
139
+ return false;
140
+ }
141
+ }
142
+ function updateHint(current, latest, binary = isStandaloneBinary()) {
143
+ if (!latest || !(compareVersions(latest, current) > 0))
144
+ return null;
145
+ const how = binary ? 'download it from the Resolflow CLI releases page' : `run: npm i -g ${exports.PACKAGE_NAME}@latest`;
146
+ return `resolflow ${latest} is available (you have ${current}) — ${how}. Hide this with RESOLFLOW_NO_UPDATE_CHECK=1.`;
147
+ }
148
+ function startUpdateCheck(opts) {
149
+ const env = opts.env ?? process.env;
150
+ if (updateCheckDisabled(env, opts.isTTY ?? !!process.stderr.isTTY))
151
+ return { finish: async () => null };
152
+ const now = opts.now ?? Date.now();
153
+ const cache = readUpdateCache(opts.dir);
154
+ const hintFrom = (latest) => updateHint(opts.current, latest, opts.binary);
155
+ if (cache && now - cache.checkedAt < exports.CHECK_INTERVAL_MS && now >= cache.checkedAt) {
156
+ return { finish: async () => hintFrom(cache.latest) };
157
+ }
158
+ const ctrl = new AbortController();
159
+ const registry = env.RESOLFLOW_UPDATE_REGISTRY || exports.DEFAULT_REGISTRY;
160
+ const fetchLatest = opts.fetchLatest ?? ((signal) => fetchLatestVersion(registry, signal));
161
+ const pending = fetchLatest(ctrl.signal).then((latest) => {
162
+ // Remember the attempt even when it failed, so an offline machine asks at most once a day
163
+ writeUpdateCache(opts.dir, { checkedAt: now, latest: latest ?? cache?.latest });
164
+ return latest ?? cache?.latest;
165
+ });
166
+ return {
167
+ async finish(maxMs = 800) {
168
+ let timer;
169
+ const timeout = new Promise((resolve) => {
170
+ timer = setTimeout(() => {
171
+ ctrl.abort();
172
+ resolve(undefined);
173
+ }, maxMs);
174
+ });
175
+ const latest = await Promise.race([pending, timeout]);
176
+ clearTimeout(timer);
177
+ return hintFrom(latest ?? cache?.latest);
178
+ },
179
+ };
180
+ }
package/dist/urls.js ADDED
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ /**
3
+ * URL policy of the CLI (CA-08):
4
+ * - the Resolflow API: https only (so the socket is wss://); plain http only to a loopback host (local development);
5
+ * - the local model server: loopback only (127.0.0.1, ::1, localhost) — the device never forwards prompts to
6
+ * another machine, and nothing else on the network can be reached through it.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.UrlPolicyError = exports.DEFAULT_LOCAL_ENDPOINT = exports.DEFAULT_API_URL = void 0;
10
+ exports.isLoopbackHost = isLoopbackHost;
11
+ exports.normaliseApiUrl = normaliseApiUrl;
12
+ exports.normaliseLocalEndpoint = normaliseLocalEndpoint;
13
+ exports.DEFAULT_API_URL = 'https://api.resolflow.ai';
14
+ exports.DEFAULT_LOCAL_ENDPOINT = 'http://127.0.0.1:11434/v1';
15
+ const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]']);
16
+ function isLoopbackHost(hostname) {
17
+ const h = hostname.toLowerCase();
18
+ return LOOPBACK_HOSTS.has(h) || /^127\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.test(h);
19
+ }
20
+ class UrlPolicyError extends Error {
21
+ }
22
+ exports.UrlPolicyError = UrlPolicyError;
23
+ /**
24
+ * Normalise the Resolflow API base (no trailing slash, no `/api` suffix). The REST API is `<base>/api/...`, the
25
+ * device socket is socket.io namespace `/cli` on `<base>`.
26
+ */
27
+ function normaliseApiUrl(raw) {
28
+ let u;
29
+ try {
30
+ u = new URL(raw.trim());
31
+ }
32
+ catch {
33
+ throw new UrlPolicyError(`Not a URL: ${raw}`);
34
+ }
35
+ if (u.username || u.password)
36
+ throw new UrlPolicyError('The API URL must not contain credentials');
37
+ if (u.protocol !== 'https:' && !(u.protocol === 'http:' && isLoopbackHost(u.hostname))) {
38
+ throw new UrlPolicyError('The Resolflow API must use https:// (http:// is allowed only for localhost development)');
39
+ }
40
+ u.search = '';
41
+ u.hash = '';
42
+ let path = u.pathname.replace(/\/+$/, '');
43
+ if (path.endsWith('/api'))
44
+ path = path.slice(0, -4);
45
+ return `${u.protocol}//${u.host}${path}`;
46
+ }
47
+ /** The OpenAI-compatible base of the local model server, e.g. http://127.0.0.1:11434/v1. Loopback only. */
48
+ function normaliseLocalEndpoint(raw) {
49
+ let u;
50
+ try {
51
+ u = new URL(raw.trim());
52
+ }
53
+ catch {
54
+ throw new UrlPolicyError(`Not a URL: ${raw}`);
55
+ }
56
+ if (u.protocol !== 'http:' && u.protocol !== 'https:')
57
+ throw new UrlPolicyError('The local endpoint must be http(s)://');
58
+ if (!isLoopbackHost(u.hostname)) {
59
+ throw new UrlPolicyError('The local model endpoint must be on this machine (127.0.0.1, ::1 or localhost)');
60
+ }
61
+ if (u.username || u.password)
62
+ throw new UrlPolicyError('The local endpoint must not contain credentials');
63
+ u.search = '';
64
+ u.hash = '';
65
+ let path = u.pathname.replace(/\/+$/, '');
66
+ if (!/\/v\d+$/.test(path))
67
+ path = `${path}/v1`;
68
+ return `${u.protocol}//${u.host}${path}`;
69
+ }