@cordfuse/crosstalk 7.0.0-alpha.16 → 7.0.0-alpha.18

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.
Files changed (75) hide show
  1. package/GUIDE-CLI.md +322 -0
  2. package/GUIDE-PROMPTS.md +112 -0
  3. package/LICENSE +21 -0
  4. package/README.md +337 -62
  5. package/bin/crosstalk.js +88 -54
  6. package/commands/agent.js +69 -0
  7. package/commands/auth.js +273 -0
  8. package/commands/channel.js +54 -44
  9. package/commands/chat.js +107 -71
  10. package/commands/daemon.js +121 -0
  11. package/commands/logs.js +108 -19
  12. package/commands/message.js +125 -0
  13. package/commands/server.js +153 -0
  14. package/commands/settings.js +49 -0
  15. package/commands/token.js +136 -0
  16. package/commands/transport.js +272 -0
  17. package/commands/version.js +3 -3
  18. package/commands/workflow.js +234 -0
  19. package/deploy/crosstalk@.service +62 -0
  20. package/deploy/install.sh +82 -0
  21. package/lib/api-client.js +77 -22
  22. package/lib/credentials.js +207 -0
  23. package/lib/nativeServer.js +174 -0
  24. package/lib/resolve.js +95 -43
  25. package/package.json +27 -4
  26. package/src/activation.ts +104 -0
  27. package/src/api.ts +1716 -0
  28. package/src/auth/enforce.ts +68 -0
  29. package/src/auth/handlers.ts +266 -0
  30. package/src/auth/middleware.ts +132 -0
  31. package/src/auth/setup.ts +263 -0
  32. package/src/auth/tokens.ts +285 -0
  33. package/src/auth/users.ts +267 -0
  34. package/src/channel.ts +202 -0
  35. package/src/dispatch.ts +492 -0
  36. package/src/dispatchers.ts +91 -0
  37. package/src/filenames.ts +28 -0
  38. package/src/frontmatter.ts +26 -0
  39. package/src/init.ts +116 -0
  40. package/src/invoke.ts +201 -0
  41. package/src/log-buffer.ts +67 -0
  42. package/src/models.ts +283 -0
  43. package/src/replies.ts +73 -0
  44. package/src/resolve.ts +100 -0
  45. package/src/run.ts +250 -0
  46. package/src/state.ts +190 -0
  47. package/src/status.ts +84 -0
  48. package/src/stop.ts +37 -0
  49. package/src/transport.ts +243 -0
  50. package/src/web/auth-pages.ts +160 -0
  51. package/src/web/channels.ts +395 -0
  52. package/src/web/chat-page.ts +636 -0
  53. package/src/web/chat-pty.ts +238 -0
  54. package/src/web/dashboard.ts +129 -0
  55. package/src/web/layout.ts +237 -0
  56. package/src/web/stubs.ts +510 -0
  57. package/src/web/workflows.ts +490 -0
  58. package/src/workflow.ts +470 -0
  59. package/template/CLAUDE.md +10 -0
  60. package/template/CROSSTALK-VERSION +1 -0
  61. package/template/CROSSTALK.md +258 -0
  62. package/template/PROTOCOL.md +66 -0
  63. package/template/README.md +64 -0
  64. package/template/auth/.gitkeep +0 -0
  65. package/template/auth/README.md +224 -0
  66. package/template/data/crosstalk.yaml +196 -0
  67. package/template/gitignore +4 -0
  68. package/commands/down.js +0 -40
  69. package/commands/init.js +0 -241
  70. package/commands/pull.js +0 -22
  71. package/commands/replies.js +0 -40
  72. package/commands/restart.js +0 -29
  73. package/commands/rm.js +0 -109
  74. package/commands/run.js +0 -115
  75. package/commands/up.js +0 -133
@@ -0,0 +1,267 @@
1
+ // User store — username, name, scrypt-hashed passphrase, admin flag.
2
+ //
3
+ // Trust context: SERVICE. The service-user daemon owns /var/lib/llmux/users.json
4
+ // and is the only thing that reads/writes it. Workers receive their
5
+ // identity via env vars + token validation, not by reading the user store
6
+ // directly.
7
+ //
8
+ // References:
9
+ // V2-SYSTEM-AUTH-DESIGN.md § "User model (v2)"
10
+ // V2-SYSTEM-AUTH-DESIGN.md § "Data plane (v2)" — users.json row
11
+ // V2-SYSTEM-AUTH-DESIGN.md § "Build plan" — Phase 3
12
+
13
+ import { existsSync, readFileSync, writeFileSync, renameSync, chmodSync, mkdirSync } from 'node:fs';
14
+ import { dirname } from 'node:path';
15
+ import { randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
16
+
17
+ export interface User {
18
+ /** Stable identifier. Application-layer only — does NOT need to match any OS user on the host.
19
+ * Also the user's orch alias. Format: [a-z0-9_-]+ */
20
+ username: string;
21
+ /** Display name. */
22
+ name: string;
23
+ /** scrypt-hashed passphrase + salt + params. Plaintext never written. */
24
+ passphraseHash: string;
25
+ /** Admin can create/delete other users + mint tokens for them. */
26
+ admin: boolean;
27
+ /** ISO-8601. */
28
+ createdAt: string;
29
+ }
30
+
31
+ export interface CreateUserInput {
32
+ username: string;
33
+ name: string;
34
+ /** Plaintext — hashed by createUser before persistence. NEVER persisted. */
35
+ passphrase: string;
36
+ /** Defaults false. Only admins can set this. */
37
+ admin?: boolean;
38
+ }
39
+
40
+ export interface UserStore {
41
+ /** Create a new user. Validates username format + uniqueness + passphrase strength. */
42
+ createUser(input: CreateUserInput): Promise<User>;
43
+ /** Look up by username. Returns undefined if not present. */
44
+ getUser(username: string): Promise<User | undefined>;
45
+ /** List all users. Admin-callable; non-admin must filter to self. */
46
+ listUsers(): Promise<User[]>;
47
+ /** Update name. Username + admin status + passphrase have their own methods. */
48
+ updateName(username: string, newName: string): Promise<void>;
49
+ /** Replace the passphrase hash. Caller verifies the old one separately. */
50
+ setPassphrase(username: string, newPassphrase: string): Promise<void>;
51
+ /** Delete user record. Caller responsible for revoking tokens + reaping orch claims. */
52
+ deleteUser(username: string): Promise<void>;
53
+ /** Toggle admin flag. Caller enforces "can't demote yourself" / "last admin" rules. */
54
+ setAdmin(username: string, admin: boolean): Promise<void>;
55
+ /** Verify a candidate passphrase against the stored hash. */
56
+ verifyPassphrase(username: string, candidate: string): Promise<boolean>;
57
+ /** Whether ANY users exist. Used by the setup wizard to detect first-run. */
58
+ isEmpty(): Promise<boolean>;
59
+ }
60
+
61
+ // ── scrypt format ─────────────────────────────────────────────────────────
62
+ // Self-describing string so we can tune N over time without breaking old
63
+ // hashes. Format: `scrypt$<N>$<saltBase64>$<hashBase64>`.
64
+ //
65
+ // N=2^17 (131072) is the modern OWASP recommendation for interactive auth
66
+ // (~100ms on commodity hardware). r=8, p=1 are scrypt's standard tuning.
67
+ const SCRYPT_N = 1 << 17;
68
+ const SCRYPT_R = 8;
69
+ const SCRYPT_P = 1;
70
+ const SCRYPT_KEYLEN = 64;
71
+ const SALT_BYTES = 16;
72
+ // Memory required by scrypt is ≈ 128 * N * r bytes; for N=2^17 r=8 that's
73
+ // ~128 MB. Node's default maxmem is 32 MB, so we raise it explicitly.
74
+ const SCRYPT_MAXMEM = 256 * 1024 * 1024;
75
+
76
+ function hashPassphrase(plain: string): string {
77
+ const salt = randomBytes(SALT_BYTES);
78
+ const hash = scryptSync(plain, salt, SCRYPT_KEYLEN, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P, maxmem: SCRYPT_MAXMEM });
79
+ return `scrypt$${SCRYPT_N}$${salt.toString('base64')}$${hash.toString('base64')}`;
80
+ }
81
+
82
+ function verifyHash(plain: string, stored: string): boolean {
83
+ const parts = stored.split('$');
84
+ if (parts.length !== 4 || parts[0] !== 'scrypt') return false;
85
+ const n = Number(parts[1]);
86
+ const salt = Buffer.from(parts[2]!, 'base64');
87
+ const expected = Buffer.from(parts[3]!, 'base64');
88
+ if (!Number.isInteger(n) || n <= 0 || salt.length === 0 || expected.length === 0) return false;
89
+ let candidate: Buffer;
90
+ try {
91
+ candidate = scryptSync(plain, salt, expected.length, { N: n, r: SCRYPT_R, p: SCRYPT_P, maxmem: SCRYPT_MAXMEM });
92
+ } catch {
93
+ return false;
94
+ }
95
+ if (candidate.length !== expected.length) return false;
96
+ return timingSafeEqual(candidate, expected);
97
+ }
98
+
99
+ // ── validation ────────────────────────────────────────────────────────────
100
+ const USERNAME_RE = /^[a-z0-9_-]+$/;
101
+ const USERNAME_MAX = 32;
102
+ const PASSPHRASE_MIN = 8;
103
+
104
+ export class UserValidationError extends Error {
105
+ constructor(message: string) { super(message); this.name = 'UserValidationError'; }
106
+ }
107
+
108
+ function validateUsername(username: string): void {
109
+ if (typeof username !== 'string' || username.length === 0) {
110
+ throw new UserValidationError('username required');
111
+ }
112
+ if (username.length > USERNAME_MAX) {
113
+ throw new UserValidationError(`username too long (max ${USERNAME_MAX})`);
114
+ }
115
+ if (!USERNAME_RE.test(username)) {
116
+ throw new UserValidationError('username must match [a-z0-9_-]+ (lowercase, no spaces)');
117
+ }
118
+ }
119
+
120
+ function validatePassphrase(passphrase: string): void {
121
+ if (typeof passphrase !== 'string' || passphrase.length < PASSPHRASE_MIN) {
122
+ throw new UserValidationError(`passphrase must be at least ${PASSPHRASE_MIN} characters`);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * File-backed user store. Reads/writes /var/lib/llmux/users.json (or
128
+ * config.dataDir/users.json).
129
+ *
130
+ * Concurrency model: in-process mutex on writes (the daemon is single-process).
131
+ * Reads load + cache; writes invalidate the cache.
132
+ */
133
+ export class FileUserStore implements UserStore {
134
+ private cache: User[] | undefined;
135
+ private writeQueue: Promise<void> = Promise.resolve();
136
+
137
+ constructor(private storePath: string) {}
138
+
139
+ async createUser(input: CreateUserInput): Promise<User> {
140
+ validateUsername(input.username);
141
+ validatePassphrase(input.passphrase);
142
+ if (typeof input.name !== 'string' || input.name.trim().length === 0) {
143
+ throw new UserValidationError('name required');
144
+ }
145
+ return this.withLock(async () => {
146
+ const users = this.load();
147
+ if (users.some(u => u.username === input.username)) {
148
+ throw new UserValidationError(`username '${input.username}' already exists`);
149
+ }
150
+ const user: User = {
151
+ username: input.username,
152
+ name: input.name.trim(),
153
+ passphraseHash: hashPassphrase(input.passphrase),
154
+ admin: input.admin === true,
155
+ createdAt: new Date().toISOString(),
156
+ };
157
+ users.push(user);
158
+ this.save(users);
159
+ return user;
160
+ });
161
+ }
162
+
163
+ async getUser(username: string): Promise<User | undefined> {
164
+ return this.load().find(u => u.username === username);
165
+ }
166
+
167
+ async listUsers(): Promise<User[]> {
168
+ return [...this.load()];
169
+ }
170
+
171
+ async updateName(username: string, newName: string): Promise<void> {
172
+ if (typeof newName !== 'string' || newName.trim().length === 0) {
173
+ throw new UserValidationError('name required');
174
+ }
175
+ return this.withLock(async () => {
176
+ const users = this.load();
177
+ const u = users.find(x => x.username === username);
178
+ if (!u) throw new UserValidationError(`user '${username}' not found`);
179
+ u.name = newName.trim();
180
+ this.save(users);
181
+ });
182
+ }
183
+
184
+ async setPassphrase(username: string, newPassphrase: string): Promise<void> {
185
+ validatePassphrase(newPassphrase);
186
+ return this.withLock(async () => {
187
+ const users = this.load();
188
+ const u = users.find(x => x.username === username);
189
+ if (!u) throw new UserValidationError(`user '${username}' not found`);
190
+ u.passphraseHash = hashPassphrase(newPassphrase);
191
+ this.save(users);
192
+ });
193
+ }
194
+
195
+ async deleteUser(username: string): Promise<void> {
196
+ return this.withLock(async () => {
197
+ const users = this.load();
198
+ const idx = users.findIndex(u => u.username === username);
199
+ if (idx < 0) throw new UserValidationError(`user '${username}' not found`);
200
+ users.splice(idx, 1);
201
+ this.save(users);
202
+ });
203
+ }
204
+
205
+ async setAdmin(username: string, admin: boolean): Promise<void> {
206
+ return this.withLock(async () => {
207
+ const users = this.load();
208
+ const u = users.find(x => x.username === username);
209
+ if (!u) throw new UserValidationError(`user '${username}' not found`);
210
+ u.admin = admin === true;
211
+ this.save(users);
212
+ });
213
+ }
214
+
215
+ async verifyPassphrase(username: string, candidate: string): Promise<boolean> {
216
+ const u = this.load().find(x => x.username === username);
217
+ if (!u) return false;
218
+ return verifyHash(candidate, u.passphraseHash);
219
+ }
220
+
221
+ async isEmpty(): Promise<boolean> {
222
+ return this.load().length === 0;
223
+ }
224
+
225
+ // ── private ─────────────────────────────────────────────────────────────
226
+
227
+ private load(): User[] {
228
+ if (this.cache) return this.cache;
229
+ if (!existsSync(this.storePath)) {
230
+ this.cache = [];
231
+ return this.cache;
232
+ }
233
+ const raw = readFileSync(this.storePath, 'utf-8');
234
+ let parsed: unknown;
235
+ try {
236
+ parsed = JSON.parse(raw);
237
+ } catch (err) {
238
+ throw new Error(`users.json: invalid JSON at ${this.storePath}: ${(err as Error).message}`);
239
+ }
240
+ if (!Array.isArray(parsed)) {
241
+ throw new Error(`users.json: expected array at ${this.storePath}`);
242
+ }
243
+ this.cache = parsed as User[];
244
+ return this.cache;
245
+ }
246
+
247
+ private save(users: User[]): void {
248
+ mkdirSync(dirname(this.storePath), { recursive: true });
249
+ const tmp = this.storePath + '.tmp';
250
+ writeFileSync(tmp, JSON.stringify(users, null, 2) + '\n', { mode: 0o600 });
251
+ renameSync(tmp, this.storePath);
252
+ try { chmodSync(this.storePath, 0o600); } catch { /* best-effort */ }
253
+ this.cache = users;
254
+ }
255
+
256
+ /**
257
+ * Serialize writes through a single promise chain. The daemon is
258
+ * single-process so we don't need cross-process locking, but two
259
+ * concurrent createUser calls in the same process would otherwise
260
+ * race on load/save.
261
+ */
262
+ private withLock<T>(fn: () => Promise<T>): Promise<T> {
263
+ const run = this.writeQueue.then(fn);
264
+ this.writeQueue = run.then(() => undefined, () => undefined);
265
+ return run;
266
+ }
267
+ }
package/src/channel.ts ADDED
@@ -0,0 +1,202 @@
1
+ // crosstalkd channel — channel operations.
2
+ //
3
+ // Three operations on one subcommand (V7-SPEC §7):
4
+ // crosstalkd channel <name> — create
5
+ // crosstalkd channel <name-or-uuid> --rename <new> — rename
6
+ // crosstalkd channel <name-or-uuid> --delete — hard delete (typed-name confirmation)
7
+ //
8
+ // No archive, no restore, no purge, no --force. History stays in git log
9
+ // if recovery is ever needed.
10
+
11
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
12
+ import { resolve, join } from 'path';
13
+ import { randomUUID } from 'crypto';
14
+ import { spawnSync } from 'child_process';
15
+ import { parseFrontmatter, serializeFrontmatter } from './frontmatter.js';
16
+ import { gitCommitAndPush, discoverChannels } from './transport.js';
17
+
18
+ const transportRoot = resolve(process.cwd());
19
+ const argv = process.argv.slice(2);
20
+
21
+ function flag(name: string): string | undefined {
22
+ const i = argv.indexOf(name);
23
+ if (i === -1 || i === argv.length - 1) return undefined;
24
+ return argv[i + 1];
25
+ }
26
+
27
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
28
+
29
+ interface ChannelMeta {
30
+ uuid: string;
31
+ name: string | null;
32
+ parent: string | null;
33
+ }
34
+
35
+ function readChannelMeta(uuid: string): ChannelMeta {
36
+ const chPath = join(transportRoot, 'data', 'channels', uuid, 'CHANNEL.md');
37
+ if (!existsSync(chPath)) return { uuid, name: null, parent: null };
38
+ const raw = readFileSync(chPath, 'utf-8');
39
+ const { data } = parseFrontmatter<{ name?: unknown; parent?: unknown }>(raw);
40
+ return {
41
+ uuid,
42
+ name: typeof data.name === 'string' ? data.name : null,
43
+ parent: typeof data.parent === 'string' ? data.parent : null,
44
+ };
45
+ }
46
+
47
+ function allChannelMeta(): ChannelMeta[] {
48
+ return discoverChannels(transportRoot).map(readChannelMeta);
49
+ }
50
+
51
+ function resolveChannel(input: string): ChannelMeta | null {
52
+ if (UUID_RE.test(input)) {
53
+ const path = join(transportRoot, 'data', 'channels', input);
54
+ if (!existsSync(path)) return null;
55
+ return readChannelMeta(input);
56
+ }
57
+ for (const meta of allChannelMeta()) {
58
+ if (meta.name === input) return meta;
59
+ }
60
+ return null;
61
+ }
62
+
63
+ function nameExists(name: string): boolean {
64
+ return allChannelMeta().some((m) => m.name === name);
65
+ }
66
+
67
+ function commit(message: string): void {
68
+ const r = gitCommitAndPush(transportRoot, message);
69
+ if (!r.ok && r.error) {
70
+ console.error(`git ${r.committed ? 'push' : 'commit'} FAILED: ${r.error.slice(0, 300)}`);
71
+ process.exit(3);
72
+ }
73
+ }
74
+
75
+ function create(name: string): void {
76
+ if (UUID_RE.test(name)) {
77
+ console.error(`crosstalkd channel: '${name}' looks like a UUID — channel names cannot be UUID-shaped.`);
78
+ process.exit(1);
79
+ }
80
+ if (nameExists(name)) {
81
+ console.error(`crosstalkd channel: a channel named '${name}' already exists.`);
82
+ process.exit(1);
83
+ }
84
+ const uuid = randomUUID();
85
+ const dir = join(transportRoot, 'data', 'channels', uuid);
86
+ mkdirSync(dir, { recursive: true });
87
+ writeFileSync(join(dir, 'CHANNEL.md'), serializeFrontmatter({ name }, ''));
88
+ commit(`channel(create): ${name} (${uuid.slice(0, 8)})`);
89
+ console.log(`Created channel: ${uuid}`);
90
+ console.log(` name: ${name}`);
91
+ }
92
+
93
+ function rename(input: string, newName: string): void {
94
+ if (UUID_RE.test(newName)) {
95
+ console.error(`crosstalkd channel: '${newName}' looks like a UUID — channel names cannot be UUID-shaped.`);
96
+ process.exit(1);
97
+ }
98
+ const meta = resolveChannel(input);
99
+ if (!meta) {
100
+ console.error(`crosstalkd channel: '${input}' not found.`);
101
+ process.exit(1);
102
+ }
103
+ if (meta.name === newName) {
104
+ console.log(`crosstalkd channel: '${meta.name}' already has that name — no-op.`);
105
+ return;
106
+ }
107
+ if (nameExists(newName)) {
108
+ console.error(`crosstalkd channel: a channel named '${newName}' already exists.`);
109
+ process.exit(1);
110
+ }
111
+ const chPath = join(transportRoot, 'data', 'channels', meta.uuid, 'CHANNEL.md');
112
+ const fm: Record<string, unknown> = { name: newName };
113
+ if (meta.parent) fm['parent'] = meta.parent;
114
+ writeFileSync(chPath, serializeFrontmatter(fm, ''));
115
+ commit(`channel(rename): ${meta.name ?? '(unnamed)'} -> ${newName} (${meta.uuid.slice(0, 8)})`);
116
+ console.log(`Renamed: ${meta.name ?? '(unnamed)'} -> ${newName}`);
117
+ }
118
+
119
+ function readStdinLine(): string {
120
+ try {
121
+ const raw = readFileSync(0, 'utf-8');
122
+ return raw.split('\n')[0]!.trim();
123
+ } catch {
124
+ return '';
125
+ }
126
+ }
127
+
128
+ function del(input: string): void {
129
+ const meta = resolveChannel(input);
130
+ if (!meta) {
131
+ console.error(`crosstalkd channel: '${input}' not found.`);
132
+ process.exit(1);
133
+ }
134
+ const displayName = meta.name ?? '(unnamed)';
135
+ const confirmTarget = meta.name ?? meta.uuid;
136
+
137
+ // If stdin is a TTY, prompt; otherwise read one line of piped input.
138
+ // Either way, the user/script must type the name back to confirm.
139
+ if (process.stdin.isTTY) {
140
+ process.stderr.write(
141
+ `type "${confirmTarget}" to confirm deletion of channel ${displayName} (${meta.uuid}): `,
142
+ );
143
+ // Naive blocking read of one line via spawnSync — bun + tsx don't
144
+ // expose a clean blocking readline in pure ESM.
145
+ const r = spawnSync('sh', ['-c', 'IFS= read -r line; echo "$line"'], { stdio: ['inherit', 'pipe', 'inherit'] });
146
+ const typed = (r.stdout?.toString() ?? '').trim();
147
+ if (typed !== confirmTarget) {
148
+ console.error('crosstalkd channel: confirmation mismatch; nothing deleted.');
149
+ process.exit(1);
150
+ }
151
+ } else {
152
+ const typed = readStdinLine();
153
+ if (typed !== confirmTarget) {
154
+ console.error(`crosstalkd channel: confirmation mismatch (expected '${confirmTarget}', got '${typed}'); nothing deleted.`);
155
+ process.exit(1);
156
+ }
157
+ }
158
+
159
+ const dir = join(transportRoot, 'data', 'channels', meta.uuid);
160
+ rmSync(dir, { recursive: true, force: true });
161
+ commit(`channel(delete): ${displayName} (${meta.uuid.slice(0, 8)})`);
162
+ console.log(`Deleted channel: ${displayName} (${meta.uuid})`);
163
+ }
164
+
165
+ // arg parsing — find the positional (first arg not starting with --, not the
166
+ // value of a flag we recognise).
167
+ const flagsTakingValue = new Set(['--rename']);
168
+ let positional: string | undefined;
169
+ for (let i = 0; i < argv.length; i++) {
170
+ const a = argv[i]!;
171
+ if (a.startsWith('--')) {
172
+ if (flagsTakingValue.has(a)) i++;
173
+ continue;
174
+ }
175
+ positional = a;
176
+ break;
177
+ }
178
+
179
+ const renameTarget = flag('--rename');
180
+ const deleteMode = argv.includes('--delete');
181
+
182
+ if (!positional) {
183
+ console.error('Usage:');
184
+ console.error(' crosstalkd channel <name> # create');
185
+ console.error(' crosstalkd channel <name-or-uuid> --rename <new> # rename');
186
+ console.error(' crosstalkd channel <name-or-uuid> --delete # hard delete (typed-name confirmation)');
187
+ process.exit(1);
188
+ }
189
+
190
+ const channelsDir = join(transportRoot, 'data', 'channels');
191
+ if (!existsSync(channelsDir)) mkdirSync(channelsDir, { recursive: true });
192
+
193
+ if (renameTarget && deleteMode) {
194
+ console.error('crosstalkd channel: --rename and --delete are mutually exclusive.');
195
+ process.exit(1);
196
+ } else if (renameTarget) {
197
+ rename(positional, renameTarget);
198
+ } else if (deleteMode) {
199
+ del(positional);
200
+ } else {
201
+ create(positional);
202
+ }