@love-moon/conductor-cli 0.2.19 → 0.2.21

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/bin/conductor.js CHANGED
@@ -10,18 +10,16 @@
10
10
  * update - Update the CLI to the latest version
11
11
  * diagnose - Diagnose a task in production/backend
12
12
  * send-file - Upload a local file into a task session
13
+ * channel - Connect user-owned chat channel providers
13
14
  */
14
15
 
15
- import { fileURLToPath } from "node:url";
16
+ import { fileURLToPath, pathToFileURL } from "node:url";
16
17
  import path from "node:path";
17
- import { createRequire } from "node:module";
18
18
  import fs from "node:fs";
19
- import yargs from "yargs/yargs";
20
- import { hideBin } from "yargs/helpers";
19
+ import { maybeCheckForUpdates } from "../src/cli-update-notifier.js";
21
20
 
22
21
  const __filename = fileURLToPath(import.meta.url);
23
22
  const __dirname = path.dirname(__filename);
24
- const require = createRequire(import.meta.url);
25
23
  const PKG_ROOT = path.join(__dirname, "..");
26
24
 
27
25
  const pkgJson = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8"));
@@ -29,57 +27,81 @@ const pkgJson = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"),
29
27
  // Parse command line arguments
30
28
  const argv = process.argv.slice(2);
31
29
 
32
- // If no arguments or help flag, show help
33
- if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
34
- showHelp();
35
- process.exit(0);
30
+ export function runConductorCli(args = argv, deps = {}) {
31
+ const consoleImpl = deps.console || console;
32
+ const importModule = deps.importModule || ((subcommandPath) => import(subcommandPath));
33
+ const env = deps.env || process.env;
34
+ const processArgv = deps.processArgv || process.argv;
35
+ const fsExistsSync = deps.existsSync || fs.existsSync;
36
+ const checkForUpdates = deps.maybeCheckForUpdates || maybeCheckForUpdates;
37
+ const validSubcommands = ["fire", "daemon", "config", "update", "diagnose", "send-file", "channel"];
38
+
39
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
40
+ showHelp(consoleImpl);
41
+ return { shouldExit: true, exitCode: 0 };
42
+ }
43
+
44
+ if (args[0] === "--version" || args[0] === "-v") {
45
+ const commitId = pkgJson.gitCommitId || "unknown";
46
+ consoleImpl.log(`conductor version ${pkgJson.version} (${commitId})`);
47
+ return { shouldExit: true, exitCode: 0 };
48
+ }
49
+
50
+ const subcommand = args[0];
51
+ if (!validSubcommands.includes(subcommand)) {
52
+ consoleImpl.error(`Error: Unknown subcommand '${subcommand}'`);
53
+ consoleImpl.error(`Valid subcommands: ${validSubcommands.join(", ")}`);
54
+ consoleImpl.error(`Run 'conductor --help' for usage information.`);
55
+ return { shouldExit: true, exitCode: 1 };
56
+ }
57
+
58
+ void checkForUpdates({
59
+ currentVersion: pkgJson.version,
60
+ subcommand,
61
+ env,
62
+ }).catch(() => {});
63
+
64
+ const subcommandArgs = args.slice(1);
65
+ env.CONDUCTOR_CLI_NAME = `conductor ${subcommand}`;
66
+ env.CONDUCTOR_LAUNCHER_SCRIPT = processArgv[1] || "";
67
+ env.CONDUCTOR_SUBCOMMAND = subcommand;
68
+ env.CONDUCTOR_SUBCOMMAND_ARGS_JSON = JSON.stringify(subcommandArgs);
69
+
70
+ const subcommandPath = path.join(__dirname, `conductor-${subcommand}.js`);
71
+ if (!fsExistsSync(subcommandPath)) {
72
+ consoleImpl.error(`Error: Subcommand implementation not found: ${subcommandPath}`);
73
+ return { shouldExit: true, exitCode: 1 };
74
+ }
75
+
76
+ process.argv = [processArgv[0], subcommandPath, ...subcommandArgs];
77
+ importModule(subcommandPath).catch((error) => {
78
+ consoleImpl.error(`Error loading subcommand '${subcommand}': ${error.message}`);
79
+ process.exit(1);
80
+ });
81
+ return { shouldExit: false, exitCode: 0 };
36
82
  }
37
83
 
38
- // If version flag, show version
39
- if (argv[0] === "--version" || argv[0] === "-v") {
40
- const commitId = pkgJson.gitCommitId || "unknown";
41
- console.log(`conductor version ${pkgJson.version} (${commitId})`);
42
- process.exit(0);
84
+ const isDirectExecution = (() => {
85
+ const entryPath = process.argv[1];
86
+ if (!entryPath) {
87
+ return false;
88
+ }
89
+ try {
90
+ return pathToFileURL(entryPath).href === import.meta.url;
91
+ } catch {
92
+ return false;
93
+ }
94
+ })();
95
+
96
+ if (isDirectExecution) {
97
+ const result = runConductorCli();
98
+ if (result.shouldExit) {
99
+ process.exit(result.exitCode);
100
+ }
43
101
  }
44
102
 
45
- // Get the subcommand
46
- const subcommand = argv[0];
47
-
48
- // Valid subcommands
49
- const validSubcommands = ["fire", "daemon", "config", "update", "diagnose", "send-file"];
50
-
51
- if (!validSubcommands.includes(subcommand)) {
52
- console.error(`Error: Unknown subcommand '${subcommand}'`);
53
- console.error(`Valid subcommands: ${validSubcommands.join(", ")}`);
54
- console.error(`Run 'conductor --help' for usage information.`);
55
- process.exit(1);
56
- }
57
-
58
- // Route to the appropriate subcommand
59
- const subcommandArgs = argv.slice(1);
60
-
61
- // Set environment variable to track the CLI name for logging
62
- process.env.CONDUCTOR_CLI_NAME = `conductor ${subcommand}`;
63
-
64
- // Import and execute the subcommand
65
- const subcommandPath = path.join(__dirname, `conductor-${subcommand}.js`);
66
-
67
- if (!fs.existsSync(subcommandPath)) {
68
- console.error(`Error: Subcommand implementation not found: ${subcommandPath}`);
69
- process.exit(1);
70
- }
71
-
72
- // Replace process.argv to pass the correct arguments to the subcommand
73
- process.argv = [process.argv[0], subcommandPath, ...subcommandArgs];
74
-
75
- // Dynamically import and execute the subcommand
76
- import(subcommandPath).catch((error) => {
77
- console.error(`Error loading subcommand '${subcommand}': ${error.message}`);
78
- process.exit(1);
79
- });
80
-
81
- function showHelp() {
82
- console.log(`conductor - Conductor CLI tool
103
+ function showHelp(consoleImpl = console) {
104
+ consoleImpl.log(`conductor - Conductor CLI tool
83
105
 
84
106
  Usage: conductor <subcommand> [options]
85
107
 
@@ -90,6 +112,7 @@ Subcommands:
90
112
  update Update the CLI to the latest version
91
113
  diagnose Diagnose a task and print likely root cause
92
114
  send-file Upload a local file into a task session
115
+ channel Connect user-owned chat channel providers
93
116
 
94
117
  Options:
95
118
  -h, --help Show this help message
@@ -101,6 +124,7 @@ Examples:
101
124
  conductor daemon --config-file ~/.conductor/config.yaml
102
125
  conductor diagnose <task-id>
103
126
  conductor send-file ./screenshot.png
127
+ conductor channel connect feishu
104
128
  conductor config
105
129
  conductor update
106
130
 
@@ -111,6 +135,7 @@ For subcommand-specific help:
111
135
  conductor update --help
112
136
  conductor diagnose --help
113
137
  conductor send-file --help
138
+ conductor channel --help
114
139
 
115
140
  Version: ${pkgJson.version}
116
141
  `);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.2.19",
4
- "gitCommitId": "346e048",
3
+ "version": "0.2.21",
4
+ "gitCommitId": "fa11085",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "conductor": "bin/conductor.js"
@@ -17,8 +17,8 @@
17
17
  "test": "node --test test/*.test.js"
18
18
  },
19
19
  "dependencies": {
20
- "@love-moon/ai-sdk": "0.2.19",
21
- "@love-moon/conductor-sdk": "0.2.19",
20
+ "@love-moon/ai-sdk": "0.2.21",
21
+ "@love-moon/conductor-sdk": "0.2.21",
22
22
  "dotenv": "^16.4.5",
23
23
  "enquirer": "^2.4.1",
24
24
  "js-yaml": "^4.1.1",
@@ -28,7 +28,14 @@
28
28
  "chrome-launcher": "^1.2.1",
29
29
  "chrome-remote-interface": "^0.33.0"
30
30
  },
31
+ "optionalDependencies": {
32
+ "@roamhq/wrtc": "^0.10.0"
33
+ },
31
34
  "pnpm": {
35
+ "onlyBuiltDependencies": [
36
+ "node-pty",
37
+ "@roamhq/wrtc"
38
+ ],
32
39
  "overrides": {
33
40
  "@love-moon/ai-sdk": "file:../modules/ai-sdk",
34
41
  "@love-moon/conductor-sdk": "file:../modules/conductor-sdk"
@@ -0,0 +1,241 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ import { fetchLatestVersion, isNewerVersion } from "./version-check.js";
6
+
7
+ export const DEFAULT_VERSION_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
8
+ export const DEFAULT_VERSION_NOTIFY_INTERVAL_MS = 24 * 60 * 60 * 1000;
9
+ export const DEFAULT_VERSION_CHECK_TIMEOUT_MS = 800;
10
+ const DEFAULT_CACHE_FILE = "version-check.json";
11
+
12
+ function normalizeOptionalString(value) {
13
+ if (typeof value !== "string") {
14
+ return null;
15
+ }
16
+ const normalized = value.trim();
17
+ return normalized || null;
18
+ }
19
+
20
+ function parseBooleanLike(value) {
21
+ if (typeof value !== "string") {
22
+ return false;
23
+ }
24
+ return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
25
+ }
26
+
27
+ function parseTimestamp(value) {
28
+ if (typeof value !== "string" || !value.trim()) {
29
+ return null;
30
+ }
31
+ const parsed = Date.parse(value);
32
+ if (Number.isNaN(parsed)) {
33
+ return null;
34
+ }
35
+ return new Date(parsed).toISOString();
36
+ }
37
+
38
+ function isTimestampOlderThan(value, ageMs, nowMs) {
39
+ const parsed = value ? Date.parse(value) : NaN;
40
+ if (Number.isNaN(parsed)) {
41
+ return true;
42
+ }
43
+ return nowMs - parsed >= ageMs;
44
+ }
45
+
46
+ export function resolveVersionCheckCachePath(options = {}) {
47
+ const homeDir = options.homeDir || process.env.HOME || os.homedir() || "/tmp";
48
+ return path.join(homeDir, ".conductor", DEFAULT_CACHE_FILE);
49
+ }
50
+
51
+ export function normalizeVersionCheckCache(value) {
52
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
53
+ return null;
54
+ }
55
+ const latestVersion = normalizeOptionalString(value.latestVersion);
56
+ const lastNotifiedVersion = normalizeOptionalString(value.lastNotifiedVersion);
57
+ const normalized = {
58
+ lastCheckedAt: parseTimestamp(value.lastCheckedAt),
59
+ latestVersion,
60
+ latestCheckedAt: parseTimestamp(value.latestCheckedAt),
61
+ lastNotifiedVersion,
62
+ lastNotifiedAt: parseTimestamp(value.lastNotifiedAt),
63
+ };
64
+ if (
65
+ !normalized.lastCheckedAt &&
66
+ !normalized.latestVersion &&
67
+ !normalized.latestCheckedAt &&
68
+ !normalized.lastNotifiedVersion &&
69
+ !normalized.lastNotifiedAt
70
+ ) {
71
+ return null;
72
+ }
73
+ return normalized;
74
+ }
75
+
76
+ export async function readVersionCheckCache(options = {}) {
77
+ const readFileFn = options.readFile || fs.readFile;
78
+ const cachePath = options.cachePath || resolveVersionCheckCachePath(options);
79
+ try {
80
+ const content = await readFileFn(cachePath, "utf8");
81
+ return normalizeVersionCheckCache(JSON.parse(content));
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ export async function writeVersionCheckCache(cache, options = {}) {
88
+ const writeFileFn = options.writeFile || fs.writeFile;
89
+ const mkdirFn = options.mkdir || fs.mkdir;
90
+ const cachePath = options.cachePath || resolveVersionCheckCachePath(options);
91
+ await mkdirFn(path.dirname(cachePath), { recursive: true });
92
+ await writeFileFn(cachePath, JSON.stringify(cache, null, 2), "utf8");
93
+ }
94
+
95
+ export function shouldSkipVersionCheck(options = {}) {
96
+ const env = options.env || process.env;
97
+ const subcommand = normalizeOptionalString(options.subcommand);
98
+ const stdoutIsTTY = options.stdoutIsTTY ?? Boolean(process.stdout?.isTTY);
99
+
100
+ if (parseBooleanLike(env.CONDUCTOR_SKIP_UPDATE_CHECK)) {
101
+ return { skip: true, reason: "disabled_by_env" };
102
+ }
103
+ if (subcommand === "update") {
104
+ return { skip: true, reason: "update_subcommand" };
105
+ }
106
+ if (!stdoutIsTTY) {
107
+ return { skip: true, reason: "non_tty" };
108
+ }
109
+ if (parseBooleanLike(env.CI)) {
110
+ return { skip: true, reason: "ci" };
111
+ }
112
+ if (normalizeOptionalString(env.CONDUCTOR_CLI_COMMAND)) {
113
+ return { skip: true, reason: "nested_cli" };
114
+ }
115
+ return { skip: false, reason: null };
116
+ }
117
+
118
+ export function buildUpdateNotice({ currentVersion, latestVersion }) {
119
+ return `New conductor version available: ${currentVersion} -> ${latestVersion}. Run: conductor update`;
120
+ }
121
+
122
+ function shouldNotifyVersion({ latestVersion, currentVersion, cache, nowMs, notifyIntervalMs }) {
123
+ if (!latestVersion || !isNewerVersion(latestVersion, currentVersion)) {
124
+ return false;
125
+ }
126
+ if (!cache?.lastNotifiedVersion || cache.lastNotifiedVersion !== latestVersion) {
127
+ return true;
128
+ }
129
+ return isTimestampOlderThan(cache.lastNotifiedAt, notifyIntervalMs, nowMs);
130
+ }
131
+
132
+ function shouldRefreshVersion({ cache, nowMs, checkIntervalMs }) {
133
+ if (!cache?.lastCheckedAt) {
134
+ return true;
135
+ }
136
+ return isTimestampOlderThan(cache.lastCheckedAt, checkIntervalMs, nowMs);
137
+ }
138
+
139
+ function createUpdatedCache(previousCache, updates = {}) {
140
+ return normalizeVersionCheckCache({
141
+ ...previousCache,
142
+ ...updates,
143
+ });
144
+ }
145
+
146
+ export async function maybeCheckForUpdates(options = {}) {
147
+ const env = options.env || process.env;
148
+ const currentVersion = normalizeOptionalString(options.currentVersion);
149
+ const subcommand = normalizeOptionalString(options.subcommand);
150
+ const nowMs = options.nowMs ?? Date.now();
151
+ const checkIntervalMs = options.checkIntervalMs ?? DEFAULT_VERSION_CHECK_INTERVAL_MS;
152
+ const notifyIntervalMs = options.notifyIntervalMs ?? DEFAULT_VERSION_NOTIFY_INTERVAL_MS;
153
+ const timeoutMs = options.timeoutMs ?? DEFAULT_VERSION_CHECK_TIMEOUT_MS;
154
+ const skip = shouldSkipVersionCheck({
155
+ env,
156
+ subcommand,
157
+ stdoutIsTTY: options.stdoutIsTTY,
158
+ });
159
+
160
+ if (skip.skip || !currentVersion) {
161
+ return { skipped: true, reason: skip.reason || "missing_current_version" };
162
+ }
163
+
164
+ const writeNotice = options.writeNotice || ((message) => process.stderr.write(`${message}\n`));
165
+ const fetchLatestVersionFn = options.fetchLatestVersion || fetchLatestVersion;
166
+ const cacheOptions = {
167
+ cachePath: options.cachePath,
168
+ homeDir: options.homeDir || env.HOME,
169
+ readFile: options.readFile,
170
+ writeFile: options.writeFile,
171
+ mkdir: options.mkdir,
172
+ };
173
+
174
+ let cache = await readVersionCheckCache(cacheOptions);
175
+ const needsRefresh = shouldRefreshVersion({ cache, nowMs, checkIntervalMs });
176
+
177
+ if (!needsRefresh) {
178
+ if (cache?.latestVersion && shouldNotifyVersion({
179
+ latestVersion: cache.latestVersion,
180
+ currentVersion,
181
+ cache,
182
+ nowMs,
183
+ notifyIntervalMs,
184
+ })) {
185
+ writeNotice(buildUpdateNotice({ currentVersion, latestVersion: cache.latestVersion }));
186
+ cache = createUpdatedCache(cache, {
187
+ lastNotifiedVersion: cache.latestVersion,
188
+ lastNotifiedAt: new Date(nowMs).toISOString(),
189
+ });
190
+ if (cache) {
191
+ await writeVersionCheckCache(cache, cacheOptions);
192
+ }
193
+ }
194
+ return { skipped: false, refreshed: false, latestVersion: cache?.latestVersion || null };
195
+ }
196
+
197
+ let latestVersion = null;
198
+ try {
199
+ latestVersion = await fetchLatestVersionFn(undefined, { timeoutMs });
200
+ } catch {
201
+ latestVersion = null;
202
+ }
203
+
204
+ cache = createUpdatedCache(cache, {
205
+ lastCheckedAt: new Date(nowMs).toISOString(),
206
+ ...(latestVersion
207
+ ? {
208
+ latestVersion,
209
+ latestCheckedAt: new Date(nowMs).toISOString(),
210
+ }
211
+ : {}),
212
+ });
213
+
214
+ if (cache) {
215
+ await writeVersionCheckCache(cache, cacheOptions);
216
+ }
217
+
218
+ const versionToNotify = latestVersion || cache?.latestVersion || null;
219
+ if (versionToNotify && shouldNotifyVersion({
220
+ latestVersion: versionToNotify,
221
+ currentVersion,
222
+ cache,
223
+ nowMs,
224
+ notifyIntervalMs,
225
+ })) {
226
+ writeNotice(buildUpdateNotice({ currentVersion, latestVersion: versionToNotify }));
227
+ cache = createUpdatedCache(cache, {
228
+ lastNotifiedVersion: versionToNotify,
229
+ lastNotifiedAt: new Date(nowMs).toISOString(),
230
+ });
231
+ if (cache) {
232
+ await writeVersionCheckCache(cache, cacheOptions);
233
+ }
234
+ }
235
+
236
+ return {
237
+ skipped: false,
238
+ refreshed: true,
239
+ latestVersion,
240
+ };
241
+ }