wispy-cli 0.6.0 → 0.7.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/bin/wispy.mjs +3 -4
- package/core/config.mjs +104 -0
- package/core/engine.mjs +532 -0
- package/core/index.mjs +12 -0
- package/core/mcp.mjs +8 -0
- package/core/providers.mjs +410 -0
- package/core/session.mjs +196 -0
- package/core/tools.mjs +526 -0
- package/lib/channels/index.mjs +45 -246
- package/lib/wispy-repl.mjs +332 -2447
- package/lib/wispy-tui.mjs +105 -588
- package/package.json +2 -1
package/lib/channels/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Channel Manager — loads config, starts/stops adapters, routes messages
|
|
3
|
+
* v0.7: uses core/engine.mjs and core/session.mjs for AI and session management
|
|
3
4
|
*
|
|
4
5
|
* Config: ~/.wispy/channels.json
|
|
5
6
|
* {
|
|
@@ -7,19 +8,22 @@
|
|
|
7
8
|
* "discord": { "enabled": true, "token": "..." },
|
|
8
9
|
* "slack": { "enabled": true, "botToken": "...", "appToken": "..." }
|
|
9
10
|
* }
|
|
10
|
-
*
|
|
11
|
-
* Per-chat conversation history is stored in ~/.wispy/channel-sessions/<channel>/<chatId>.json
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
13
|
import os from "node:os";
|
|
15
14
|
import path from "node:path";
|
|
16
15
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
16
|
+
import { createInterface } from "node:readline";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
WispyEngine,
|
|
20
|
+
sessionManager,
|
|
21
|
+
WISPY_DIR,
|
|
22
|
+
} from "../../core/index.mjs";
|
|
17
23
|
|
|
18
24
|
// ── Paths ─────────────────────────────────────────────────────────────────────
|
|
19
25
|
|
|
20
|
-
const WISPY_DIR = path.join(os.homedir(), ".wispy");
|
|
21
26
|
const CHANNELS_CONFIG = path.join(WISPY_DIR, "channels.json");
|
|
22
|
-
const SESSIONS_DIR = path.join(WISPY_DIR, "channel-sessions");
|
|
23
27
|
|
|
24
28
|
// ── Config helpers ────────────────────────────────────────────────────────────
|
|
25
29
|
|
|
@@ -37,245 +41,59 @@ export async function saveChannelsConfig(cfg) {
|
|
|
37
41
|
await writeFile(CHANNELS_CONFIG, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
// ──
|
|
44
|
+
// ── Shared AI engine ──────────────────────────────────────────────────────────
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
const dir = path.join(SESSIONS_DIR, channelName);
|
|
44
|
-
await mkdir(dir, { recursive: true });
|
|
45
|
-
// Sanitize chatId to be filename-safe
|
|
46
|
-
const safe = String(chatId).replace(/[^a-zA-Z0-9_\-]/g, "_");
|
|
47
|
-
return path.join(dir, `${safe}.json`);
|
|
48
|
-
}
|
|
46
|
+
let _sharedEngine = null;
|
|
49
47
|
|
|
50
|
-
async function
|
|
51
|
-
|
|
52
|
-
const raw = await readFile(await sessionPath(channelName, chatId), "utf8");
|
|
53
|
-
return JSON.parse(raw);
|
|
54
|
-
} catch {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
}
|
|
48
|
+
async function getEngine() {
|
|
49
|
+
if (_sharedEngine) return _sharedEngine;
|
|
58
50
|
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
await writeFile(p, JSON.stringify(trimmed, null, 2) + "\n", "utf8");
|
|
63
|
-
}
|
|
51
|
+
const engine = new WispyEngine({ workstream: "channels" });
|
|
52
|
+
const result = await engine.init();
|
|
53
|
+
if (!result) return null;
|
|
64
54
|
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
_sharedEngine = engine;
|
|
56
|
+
return engine;
|
|
67
57
|
}
|
|
68
58
|
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
import { createInterface } from "node:readline";
|
|
72
|
-
import { appendFile } from "node:fs/promises";
|
|
73
|
-
|
|
74
|
-
const PROVIDERS = {
|
|
75
|
-
google: { envKeys: ["GOOGLE_AI_KEY", "GEMINI_API_KEY"], defaultModel: "gemini-2.5-flash" },
|
|
76
|
-
anthropic: { envKeys: ["ANTHROPIC_API_KEY"], defaultModel: "claude-sonnet-4-20250514" },
|
|
77
|
-
openai: { envKeys: ["OPENAI_API_KEY"], defaultModel: "gpt-4o" },
|
|
78
|
-
openrouter: { envKeys: ["OPENROUTER_API_KEY"], defaultModel: "anthropic/claude-sonnet-4-20250514" },
|
|
79
|
-
groq: { envKeys: ["GROQ_API_KEY"], defaultModel: "llama-3.3-70b-versatile" },
|
|
80
|
-
deepseek: { envKeys: ["DEEPSEEK_API_KEY"], defaultModel: "deepseek-chat" },
|
|
81
|
-
ollama: { envKeys: ["OLLAMA_HOST"], defaultModel: "llama3.2", local: true },
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
function getEnvKey(envKeys) {
|
|
85
|
-
for (const k of envKeys) { if (process.env[k]) return process.env[k]; }
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function tryKeychainKey(service) {
|
|
90
|
-
try {
|
|
91
|
-
const { execFile } = await import("node:child_process");
|
|
92
|
-
const { promisify } = await import("node:util");
|
|
93
|
-
const exec = promisify(execFile);
|
|
94
|
-
const { stdout } = await exec("security", ["find-generic-password", "-s", service, "-a", "poropo", "-w"], { timeout: 3000 });
|
|
95
|
-
return stdout.trim() || null;
|
|
96
|
-
} catch { return null; }
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
let _detectedProvider = null;
|
|
100
|
-
|
|
101
|
-
async function getProvider() {
|
|
102
|
-
if (_detectedProvider) return _detectedProvider;
|
|
103
|
-
|
|
104
|
-
// WISPY_PROVIDER override
|
|
105
|
-
const forced = process.env.WISPY_PROVIDER;
|
|
106
|
-
if (forced && PROVIDERS[forced]) {
|
|
107
|
-
const key = getEnvKey(PROVIDERS[forced].envKeys);
|
|
108
|
-
if (key || PROVIDERS[forced].local) {
|
|
109
|
-
_detectedProvider = { provider: forced, key, model: process.env.WISPY_MODEL ?? PROVIDERS[forced].defaultModel };
|
|
110
|
-
return _detectedProvider;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Config file
|
|
115
|
-
try {
|
|
116
|
-
const cfg = JSON.parse(await readFile(path.join(WISPY_DIR, "config.json"), "utf8"));
|
|
117
|
-
if (cfg.provider && PROVIDERS[cfg.provider]) {
|
|
118
|
-
const key = getEnvKey(PROVIDERS[cfg.provider].envKeys) ?? cfg.apiKey;
|
|
119
|
-
if (key || PROVIDERS[cfg.provider].local) {
|
|
120
|
-
_detectedProvider = { provider: cfg.provider, key, model: cfg.model ?? PROVIDERS[cfg.provider].defaultModel };
|
|
121
|
-
return _detectedProvider;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
} catch {}
|
|
125
|
-
|
|
126
|
-
// Auto-detect env
|
|
127
|
-
for (const p of ["google","anthropic","openai","openrouter","groq","deepseek","ollama"]) {
|
|
128
|
-
const key = getEnvKey(PROVIDERS[p].envKeys);
|
|
129
|
-
if (key || (p === "ollama" && process.env.OLLAMA_HOST)) {
|
|
130
|
-
_detectedProvider = { provider: p, key, model: process.env.WISPY_MODEL ?? PROVIDERS[p].defaultModel };
|
|
131
|
-
return _detectedProvider;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// macOS Keychain
|
|
136
|
-
for (const [service, provider] of [["google-ai-key","google"],["anthropic-api-key","anthropic"],["openai-api-key","openai"]]) {
|
|
137
|
-
const key = await tryKeychainKey(service);
|
|
138
|
-
if (key) {
|
|
139
|
-
process.env[PROVIDERS[provider].envKeys[0]] = key;
|
|
140
|
-
_detectedProvider = { provider, key, model: process.env.WISPY_MODEL ?? PROVIDERS[provider].defaultModel };
|
|
141
|
-
return _detectedProvider;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const OPENAI_COMPAT = {
|
|
149
|
-
openai: "https://api.openai.com/v1/chat/completions",
|
|
150
|
-
openrouter: "https://openrouter.ai/api/v1/chat/completions",
|
|
151
|
-
groq: "https://api.groq.com/openai/v1/chat/completions",
|
|
152
|
-
deepseek: "https://api.deepseek.com/v1/chat/completions",
|
|
153
|
-
ollama: `${process.env.OLLAMA_HOST ?? "http://localhost:11434"}/v1/chat/completions`,
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
async function callLLM(messages, providerInfo) {
|
|
157
|
-
const { provider, key, model } = providerInfo;
|
|
158
|
-
|
|
159
|
-
if (provider === "google") {
|
|
160
|
-
return callGemini(messages, key, model);
|
|
161
|
-
}
|
|
162
|
-
if (provider === "anthropic") {
|
|
163
|
-
return callAnthropic(messages, key, model);
|
|
164
|
-
}
|
|
165
|
-
return callOpenAICompat(messages, key, model, provider);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async function callGemini(messages, key, model) {
|
|
169
|
-
const systemInstruction = messages.find(m => m.role === "system")?.content ?? "";
|
|
170
|
-
const contents = messages
|
|
171
|
-
.filter(m => m.role !== "system")
|
|
172
|
-
.map(m => ({
|
|
173
|
-
role: m.role === "assistant" ? "model" : "user",
|
|
174
|
-
parts: [{ text: m.content }],
|
|
175
|
-
}));
|
|
176
|
-
|
|
177
|
-
const resp = await fetch(
|
|
178
|
-
`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`,
|
|
179
|
-
{
|
|
180
|
-
method: "POST",
|
|
181
|
-
headers: { "Content-Type": "application/json" },
|
|
182
|
-
body: JSON.stringify({
|
|
183
|
-
system_instruction: systemInstruction ? { parts: [{ text: systemInstruction }] } : undefined,
|
|
184
|
-
contents,
|
|
185
|
-
generationConfig: { temperature: 0.7, maxOutputTokens: 4096 },
|
|
186
|
-
}),
|
|
187
|
-
}
|
|
188
|
-
);
|
|
189
|
-
if (!resp.ok) throw new Error(`Gemini API error ${resp.status}: ${await resp.text().then(t => t.slice(0,200))}`);
|
|
190
|
-
const data = await resp.json();
|
|
191
|
-
return data.candidates?.[0]?.content?.parts?.map(p => p.text ?? "").join("") ?? "";
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async function callAnthropic(messages, key, model) {
|
|
195
|
-
const systemPrompt = messages.find(m => m.role === "system")?.content ?? "";
|
|
196
|
-
const anthropicMessages = messages.filter(m => m.role !== "system").map(m => ({
|
|
197
|
-
role: m.role === "assistant" ? "assistant" : "user",
|
|
198
|
-
content: m.content,
|
|
199
|
-
}));
|
|
200
|
-
|
|
201
|
-
const resp = await fetch("https://api.anthropic.com/v1/messages", {
|
|
202
|
-
method: "POST",
|
|
203
|
-
headers: { "Content-Type": "application/json", "x-api-key": key, "anthropic-version": "2023-06-01" },
|
|
204
|
-
body: JSON.stringify({ model, max_tokens: 4096, system: systemPrompt, messages: anthropicMessages }),
|
|
205
|
-
});
|
|
206
|
-
if (!resp.ok) throw new Error(`Anthropic API error ${resp.status}: ${await resp.text().then(t => t.slice(0,200))}`);
|
|
207
|
-
const data = await resp.json();
|
|
208
|
-
return data.content?.map(c => c.text ?? "").join("") ?? "";
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async function callOpenAICompat(messages, key, model, provider) {
|
|
212
|
-
const endpoint = OPENAI_COMPAT[provider] ?? OPENAI_COMPAT.openai;
|
|
213
|
-
const headers = { "Content-Type": "application/json" };
|
|
214
|
-
if (key) headers["Authorization"] = `Bearer ${key}`;
|
|
215
|
-
if (provider === "openrouter") headers["HTTP-Referer"] = "https://wispy.dev";
|
|
216
|
-
|
|
217
|
-
const oaiMessages = messages.filter(m => m.role !== "tool_result").map(m => ({
|
|
218
|
-
role: m.role === "assistant" ? "assistant" : m.role,
|
|
219
|
-
content: m.content,
|
|
220
|
-
}));
|
|
221
|
-
|
|
222
|
-
const resp = await fetch(endpoint, {
|
|
223
|
-
method: "POST",
|
|
224
|
-
headers,
|
|
225
|
-
body: JSON.stringify({ model, messages: oaiMessages, temperature: 0.7, max_tokens: 4096 }),
|
|
226
|
-
});
|
|
227
|
-
if (!resp.ok) throw new Error(`OpenAI-compat API error ${resp.status}: ${await resp.text().then(t => t.slice(0,200))}`);
|
|
228
|
-
const data = await resp.json();
|
|
229
|
-
return data.choices?.[0]?.message?.content ?? "";
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const SYSTEM_PROMPT = `You are Wispy 🌿 — a small ghost AI assistant living in chat platforms.
|
|
59
|
+
// System prompt for channel context
|
|
60
|
+
const CHANNEL_SYSTEM_PROMPT = `You are Wispy 🌿 — a small ghost AI assistant living in chat platforms.
|
|
233
61
|
You're helpful, concise, and friendly. You can answer questions, write code, help with tasks.
|
|
234
62
|
Keep responses concise and conversational. End each message with 🌿.
|
|
235
63
|
Respond in the same language the user writes in.`;
|
|
236
64
|
|
|
237
65
|
/**
|
|
238
66
|
* Process a user message and return Wispy's response.
|
|
239
|
-
*
|
|
67
|
+
* Uses core/engine.mjs for AI and core/session.mjs for per-chat sessions.
|
|
240
68
|
*/
|
|
241
69
|
export async function processUserMessage(channelName, chatId, userText) {
|
|
242
70
|
// Handle __CLEAR__ signal
|
|
243
71
|
if (userText === "__CLEAR__") {
|
|
244
|
-
|
|
72
|
+
const key = `${channelName}:${chatId}`;
|
|
73
|
+
const session = await sessionManager.getOrCreate(key, { channel: channelName });
|
|
74
|
+
sessionManager.clear(session.id);
|
|
75
|
+
await sessionManager.save(session.id);
|
|
245
76
|
return null; // Caller should send confirmation
|
|
246
77
|
}
|
|
247
78
|
|
|
248
|
-
const
|
|
249
|
-
if (!
|
|
79
|
+
const engine = await getEngine();
|
|
80
|
+
if (!engine) {
|
|
250
81
|
return "⚠️ No AI provider configured. Run `wispy` first to set up your API key. 🌿";
|
|
251
82
|
}
|
|
252
83
|
|
|
253
|
-
//
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
// Build messages array
|
|
257
|
-
const messages = [
|
|
258
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
259
|
-
...history,
|
|
260
|
-
{ role: "user", content: userText },
|
|
261
|
-
];
|
|
84
|
+
// Get or create per-chat session
|
|
85
|
+
const key = `${channelName}:${chatId}`;
|
|
86
|
+
const session = await sessionManager.getOrCreate(key, { channel: channelName, chatId: String(chatId) });
|
|
262
87
|
|
|
263
|
-
let response;
|
|
264
88
|
try {
|
|
265
|
-
|
|
89
|
+
const result = await engine.processMessage(session.id, userText, {
|
|
90
|
+
systemPrompt: CHANNEL_SYSTEM_PROMPT,
|
|
91
|
+
noSave: false,
|
|
92
|
+
});
|
|
93
|
+
return result.content;
|
|
266
94
|
} catch (err) {
|
|
267
95
|
return `❌ Error: ${err.message.slice(0, 200)} 🌿`;
|
|
268
96
|
}
|
|
269
|
-
|
|
270
|
-
// Save history (keep last 40 turns)
|
|
271
|
-
const updatedHistory = [
|
|
272
|
-
...history,
|
|
273
|
-
{ role: "user", content: userText },
|
|
274
|
-
{ role: "assistant", content: response },
|
|
275
|
-
].slice(-40);
|
|
276
|
-
await saveSession(channelName, chatId, updatedHistory);
|
|
277
|
-
|
|
278
|
-
return response;
|
|
279
97
|
}
|
|
280
98
|
|
|
281
99
|
// ── ChannelManager ────────────────────────────────────────────────────────────
|
|
@@ -307,7 +125,6 @@ export class ChannelManager {
|
|
|
307
125
|
continue;
|
|
308
126
|
}
|
|
309
127
|
|
|
310
|
-
// Check if at least one token is present
|
|
311
128
|
const hasToken = this._hasToken(name, channelCfg);
|
|
312
129
|
if (!hasToken) {
|
|
313
130
|
console.log(`⚠ ${name}: no token configured — skipping (run \`wispy channel setup ${name}\`)`);
|
|
@@ -361,11 +178,12 @@ export class ChannelManager {
|
|
|
361
178
|
}
|
|
362
179
|
}
|
|
363
180
|
this._adapters.clear();
|
|
181
|
+
if (_sharedEngine) { _sharedEngine.destroy(); _sharedEngine = null; }
|
|
364
182
|
}
|
|
365
183
|
|
|
366
184
|
_hasToken(name, cfg) {
|
|
367
185
|
if (name === "telegram") return !!(process.env.WISPY_TELEGRAM_TOKEN ?? cfg.token);
|
|
368
|
-
if (name === "discord") return !!(process.env.WISPY_DISCORD_TOKEN
|
|
186
|
+
if (name === "discord") return !!(process.env.WISPY_DISCORD_TOKEN ?? cfg.token);
|
|
369
187
|
if (name === "slack") return !!(
|
|
370
188
|
(process.env.WISPY_SLACK_BOT_TOKEN ?? cfg.botToken) &&
|
|
371
189
|
(process.env.WISPY_SLACK_APP_TOKEN ?? cfg.appToken)
|
|
@@ -381,7 +199,6 @@ export async function channelSetup(channelName) {
|
|
|
381
199
|
const ask = (q) => new Promise(r => rl.question(q, r));
|
|
382
200
|
|
|
383
201
|
const cfg = await loadChannelsConfig();
|
|
384
|
-
|
|
385
202
|
console.log(`\n🌿 Setting up ${channelName} channel\n`);
|
|
386
203
|
|
|
387
204
|
if (channelName === "telegram") {
|
|
@@ -409,9 +226,7 @@ export async function channelSetup(channelName) {
|
|
|
409
226
|
else if (channelName === "slack") {
|
|
410
227
|
console.log("1. Go to https://api.slack.com/apps → Create New App → From scratch");
|
|
411
228
|
console.log("2. Enable Socket Mode → copy App-Level Token (starts with xapp-)");
|
|
412
|
-
console.log("3. OAuth & Permissions → copy Bot User OAuth Token (starts with xoxb-)");
|
|
413
|
-
console.log("4. Subscribe to: app_mention, message.im events");
|
|
414
|
-
console.log("5. Add /wispy slash command\n");
|
|
229
|
+
console.log("3. OAuth & Permissions → copy Bot User OAuth Token (starts with xoxb-)\n");
|
|
415
230
|
const botToken = (await ask(" Paste bot token (xoxb-...): ")).trim();
|
|
416
231
|
const appToken = (await ask(" Paste app token (xapp-...): ")).trim();
|
|
417
232
|
if (!botToken || !appToken) { rl.close(); return; }
|
|
@@ -434,14 +249,9 @@ export async function channelList() {
|
|
|
434
249
|
console.log("\n🌿 Configured channels:\n");
|
|
435
250
|
for (const name of channels) {
|
|
436
251
|
const c = cfg[name];
|
|
437
|
-
if (!c) {
|
|
438
|
-
console.log(` ○ ${name.padEnd(10)} not configured`);
|
|
439
|
-
continue;
|
|
440
|
-
}
|
|
252
|
+
if (!c) { console.log(` ○ ${name.padEnd(10)} not configured`); continue; }
|
|
441
253
|
const enabled = c.enabled !== false;
|
|
442
|
-
const hasToken = name === "slack"
|
|
443
|
-
? !!(c.botToken && c.appToken)
|
|
444
|
-
: !!c.token;
|
|
254
|
+
const hasToken = name === "slack" ? !!(c.botToken && c.appToken) : !!c.token;
|
|
445
255
|
const status = !hasToken ? "no token" : enabled ? "✅ enabled" : "disabled";
|
|
446
256
|
console.log(` ${enabled && hasToken ? "●" : "○"} ${name.padEnd(10)} ${status}`);
|
|
447
257
|
}
|
|
@@ -463,9 +273,7 @@ export async function channelTest(channelName) {
|
|
|
463
273
|
const me = await bot.api.getMe();
|
|
464
274
|
console.log(`✅ Connected as @${me.username} (${me.first_name})`);
|
|
465
275
|
await bot.stop().catch(() => {});
|
|
466
|
-
} catch (err) {
|
|
467
|
-
console.log(`❌ ${err.message}`);
|
|
468
|
-
}
|
|
276
|
+
} catch (err) { console.log(`❌ ${err.message}`); }
|
|
469
277
|
}
|
|
470
278
|
|
|
471
279
|
else if (channelName === "discord") {
|
|
@@ -477,9 +285,7 @@ export async function channelTest(channelName) {
|
|
|
477
285
|
await client.login(token);
|
|
478
286
|
console.log(`✅ Connected as ${client.user?.tag}`);
|
|
479
287
|
await client.destroy();
|
|
480
|
-
} catch (err) {
|
|
481
|
-
console.log(`❌ ${err.message}`);
|
|
482
|
-
}
|
|
288
|
+
} catch (err) { console.log(`❌ ${err.message}`); }
|
|
483
289
|
}
|
|
484
290
|
|
|
485
291
|
else if (channelName === "slack") {
|
|
@@ -490,18 +296,11 @@ export async function channelTest(channelName) {
|
|
|
490
296
|
headers: { Authorization: `Bearer ${botToken}` },
|
|
491
297
|
});
|
|
492
298
|
const data = await resp.json();
|
|
493
|
-
if (data.ok) {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
console.log(`❌ Slack error: ${data.error}`);
|
|
497
|
-
}
|
|
498
|
-
} catch (err) {
|
|
499
|
-
console.log(`❌ ${err.message}`);
|
|
500
|
-
}
|
|
299
|
+
if (data.ok) console.log(`✅ Connected as @${data.user} on team ${data.team}`);
|
|
300
|
+
else console.log(`❌ Slack error: ${data.error}`);
|
|
301
|
+
} catch (err) { console.log(`❌ ${err.message}`); }
|
|
501
302
|
}
|
|
502
303
|
|
|
503
|
-
else {
|
|
504
|
-
console.log(`Unknown channel: ${channelName}`);
|
|
505
|
-
}
|
|
304
|
+
else { console.log(`Unknown channel: ${channelName}`); }
|
|
506
305
|
console.log("");
|
|
507
306
|
}
|