pikiclaw 0.2.35
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/LICENSE +21 -0
- package/README.md +315 -0
- package/dist/agent-driver.js +24 -0
- package/dist/bot-command-ui.js +299 -0
- package/dist/bot-commands.js +236 -0
- package/dist/bot-feishu-render.js +527 -0
- package/dist/bot-feishu.js +752 -0
- package/dist/bot-handler.js +115 -0
- package/dist/bot-menu.js +44 -0
- package/dist/bot-streaming.js +165 -0
- package/dist/bot-telegram-directory.js +74 -0
- package/dist/bot-telegram-live-preview.js +192 -0
- package/dist/bot-telegram-render.js +369 -0
- package/dist/bot-telegram.js +789 -0
- package/dist/bot.js +897 -0
- package/dist/channel-base.js +46 -0
- package/dist/channel-feishu.js +873 -0
- package/dist/channel-states.js +3 -0
- package/dist/channel-telegram.js +773 -0
- package/dist/cli-channels.js +24 -0
- package/dist/cli.js +484 -0
- package/dist/code-agent.js +1080 -0
- package/dist/config-validation.js +244 -0
- package/dist/dashboard-ui.js +31 -0
- package/dist/dashboard.js +840 -0
- package/dist/driver-claude.js +520 -0
- package/dist/driver-codex.js +1055 -0
- package/dist/driver-gemini.js +230 -0
- package/dist/mcp-bridge.js +192 -0
- package/dist/mcp-session-server.js +321 -0
- package/dist/onboarding.js +138 -0
- package/dist/process-control.js +259 -0
- package/dist/run.js +275 -0
- package/dist/session-status.js +43 -0
- package/dist/setup-wizard.js +231 -0
- package/dist/user-config.js +195 -0
- package/package.json +60 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-session-server.ts — MCP server process for pikiclaw session bridge.
|
|
3
|
+
*
|
|
4
|
+
* Spawned by the agent CLI (claude/codex/gemini) via --mcp-config or codex mcp add.
|
|
5
|
+
* Communicates with the agent over stdio using the MCP protocol (JSON-RPC 2.0).
|
|
6
|
+
*
|
|
7
|
+
* Supports two stdio transports (auto-detected from first byte):
|
|
8
|
+
* - Content-Length framing (Claude, Gemini — standard MCP/LSP)
|
|
9
|
+
* - Newline-delimited JSON (Codex)
|
|
10
|
+
*
|
|
11
|
+
* Context is injected via environment variables:
|
|
12
|
+
* MCP_WORKSPACE_PATH — absolute path to the session workspace
|
|
13
|
+
* MCP_STAGED_FILES — JSON array of staged file relative paths
|
|
14
|
+
* MCP_CALLBACK_URL — HTTP URL for the pikiclaw callback server
|
|
15
|
+
*
|
|
16
|
+
* Tools:
|
|
17
|
+
* pikiclaw_get_session_info — returns workspace path and staged files
|
|
18
|
+
* pikiclaw_list_workspace_files — lists files in the workspace directory
|
|
19
|
+
* pikiclaw_send_file — sends a workspace file back to the IM chat
|
|
20
|
+
*/
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import http from 'node:http';
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Context from environment
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
const WORKSPACE = process.env.MCP_WORKSPACE_PATH || '';
|
|
28
|
+
const STAGED_FILES = (() => {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(process.env.MCP_STAGED_FILES || '[]');
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
const CALLBACK_URL = process.env.MCP_CALLBACK_URL || '';
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// MCP protocol — auto-detect transport format
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
/** 'framed' = Content-Length (Claude/Gemini), 'ndjson' = newline-delimited (Codex) */
|
|
41
|
+
let transport = null;
|
|
42
|
+
function send(msg) {
|
|
43
|
+
const body = JSON.stringify(msg);
|
|
44
|
+
if (transport === 'ndjson') {
|
|
45
|
+
process.stdout.write(body + '\n');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
process.stdout.write(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function respond(id, result) {
|
|
52
|
+
send({ jsonrpc: '2.0', id, result });
|
|
53
|
+
}
|
|
54
|
+
function respondError(id, code, message) {
|
|
55
|
+
send({ jsonrpc: '2.0', id, error: { code, message } });
|
|
56
|
+
}
|
|
57
|
+
function toolResult(text, isError = false) {
|
|
58
|
+
return { content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) };
|
|
59
|
+
}
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Stdio reader — auto-detecting Content-Length framed vs NDJSON
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
let buffer = '';
|
|
64
|
+
/** Process buffer in Content-Length framed mode. */
|
|
65
|
+
function processFramed() {
|
|
66
|
+
while (true) {
|
|
67
|
+
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
68
|
+
if (headerEnd < 0)
|
|
69
|
+
break;
|
|
70
|
+
const header = buffer.slice(0, headerEnd);
|
|
71
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
72
|
+
if (!match) {
|
|
73
|
+
buffer = buffer.slice(headerEnd + 4);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const len = parseInt(match[1], 10);
|
|
77
|
+
const bodyStart = headerEnd + 4;
|
|
78
|
+
if (buffer.length < bodyStart + len)
|
|
79
|
+
break;
|
|
80
|
+
const body = buffer.slice(bodyStart, bodyStart + len);
|
|
81
|
+
buffer = buffer.slice(bodyStart + len);
|
|
82
|
+
try {
|
|
83
|
+
handleMessage(JSON.parse(body));
|
|
84
|
+
}
|
|
85
|
+
catch { /* ignore parse errors */ }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Process buffer in newline-delimited JSON mode. */
|
|
89
|
+
function processNdjson() {
|
|
90
|
+
while (true) {
|
|
91
|
+
const newlineIdx = buffer.indexOf('\n');
|
|
92
|
+
if (newlineIdx < 0)
|
|
93
|
+
break;
|
|
94
|
+
const line = buffer.slice(0, newlineIdx).trim();
|
|
95
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
96
|
+
if (!line)
|
|
97
|
+
continue;
|
|
98
|
+
try {
|
|
99
|
+
handleMessage(JSON.parse(line));
|
|
100
|
+
}
|
|
101
|
+
catch { /* ignore parse errors */ }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function processBuffer() {
|
|
105
|
+
// Auto-detect transport from the first non-whitespace byte
|
|
106
|
+
if (transport === null) {
|
|
107
|
+
const trimmed = buffer.trimStart();
|
|
108
|
+
if (!trimmed)
|
|
109
|
+
return;
|
|
110
|
+
transport = trimmed[0] === '{' ? 'ndjson' : 'framed';
|
|
111
|
+
}
|
|
112
|
+
if (transport === 'ndjson')
|
|
113
|
+
processNdjson();
|
|
114
|
+
else
|
|
115
|
+
processFramed();
|
|
116
|
+
}
|
|
117
|
+
process.stdin.setEncoding('utf-8');
|
|
118
|
+
process.stdin.on('data', (chunk) => {
|
|
119
|
+
buffer += chunk;
|
|
120
|
+
processBuffer();
|
|
121
|
+
});
|
|
122
|
+
process.stdin.on('end', () => process.exit(0));
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Tool definitions
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
const TOOLS = [
|
|
127
|
+
{
|
|
128
|
+
name: 'pikiclaw_get_session_info',
|
|
129
|
+
description: 'Get the current pikiclaw session workspace path and list of user-uploaded files.',
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'pikiclaw_list_workspace_files',
|
|
137
|
+
description: 'List files and directories in the pikiclaw session workspace.',
|
|
138
|
+
inputSchema: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
subdirectory: {
|
|
142
|
+
type: 'string',
|
|
143
|
+
description: 'Subdirectory relative to workspace root. Omit to list root.',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'pikiclaw_send_file',
|
|
150
|
+
description: [
|
|
151
|
+
'Send a file back to the user via their IM chat.',
|
|
152
|
+
'IMPORTANT: You MUST call this tool to send any file (image, document, etc.) to the user. Do NOT just print the file path — the user cannot access local files.',
|
|
153
|
+
'Accepts absolute paths or paths relative to the session workspace.',
|
|
154
|
+
'Allowed locations: session workspace, agent workdir, and /tmp.',
|
|
155
|
+
'For images (png/jpg/jpeg/webp), set kind to "photo" for inline display.',
|
|
156
|
+
].join(' '),
|
|
157
|
+
inputSchema: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: {
|
|
160
|
+
path: {
|
|
161
|
+
type: 'string',
|
|
162
|
+
description: 'File path (absolute, or relative to session workspace).',
|
|
163
|
+
},
|
|
164
|
+
caption: {
|
|
165
|
+
type: 'string',
|
|
166
|
+
description: 'Optional caption or description for the file.',
|
|
167
|
+
},
|
|
168
|
+
kind: {
|
|
169
|
+
type: 'string',
|
|
170
|
+
enum: ['photo', 'document'],
|
|
171
|
+
description: 'File display type. Auto-detected from extension if omitted.',
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
required: ['path'],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Tool handlers
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
function handleGetSessionInfo(id) {
|
|
182
|
+
respond(id, toolResult(JSON.stringify({
|
|
183
|
+
workspacePath: WORKSPACE,
|
|
184
|
+
stagedFiles: STAGED_FILES,
|
|
185
|
+
}, null, 2)));
|
|
186
|
+
}
|
|
187
|
+
function handleListWorkspaceFiles(id, args) {
|
|
188
|
+
const subdir = typeof args?.subdirectory === 'string' ? args.subdirectory : '';
|
|
189
|
+
const dir = subdir ? path.resolve(WORKSPACE, subdir) : WORKSPACE;
|
|
190
|
+
// Security: ensure we stay within workspace
|
|
191
|
+
const realWorkspace = safeRealpath(WORKSPACE);
|
|
192
|
+
const realDir = safeRealpath(dir);
|
|
193
|
+
if (!realWorkspace || !realDir || !realDir.startsWith(realWorkspace)) {
|
|
194
|
+
respond(id, toolResult('Error: path is outside the workspace', true));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
199
|
+
const files = entries.map(e => {
|
|
200
|
+
const entry = { name: e.name, type: e.isDirectory() ? 'directory' : 'file' };
|
|
201
|
+
if (e.isFile()) {
|
|
202
|
+
try {
|
|
203
|
+
entry.size = fs.statSync(path.join(dir, e.name)).size;
|
|
204
|
+
}
|
|
205
|
+
catch { }
|
|
206
|
+
}
|
|
207
|
+
return entry;
|
|
208
|
+
});
|
|
209
|
+
respond(id, toolResult(JSON.stringify(files, null, 2)));
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
respond(id, toolResult(`Error listing directory: ${e.message}`, true));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function handleSendFile(id, args) {
|
|
216
|
+
const filePath = typeof args?.path === 'string' ? args.path.trim() : '';
|
|
217
|
+
if (!filePath) {
|
|
218
|
+
respond(id, toolResult('Error: "path" is required', true));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (!CALLBACK_URL) {
|
|
222
|
+
respond(id, toolResult('Error: MCP callback URL is not configured', true));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const result = await callbackSendFile(filePath, {
|
|
227
|
+
caption: typeof args?.caption === 'string' ? args.caption : undefined,
|
|
228
|
+
kind: typeof args?.kind === 'string' ? args.kind : undefined,
|
|
229
|
+
});
|
|
230
|
+
if (result.ok) {
|
|
231
|
+
respond(id, toolResult(`File sent successfully: ${filePath}`));
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
respond(id, toolResult(`Failed to send file: ${result.error || 'unknown error'}`, true));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
respond(id, toolResult(`Error sending file: ${e.message}`, true));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// HTTP callback to pikiclaw main process
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
function callbackSendFile(filePath, opts) {
|
|
245
|
+
const body = JSON.stringify({ path: filePath, ...opts });
|
|
246
|
+
const url = new URL('/send-file', CALLBACK_URL);
|
|
247
|
+
return new Promise((resolve, reject) => {
|
|
248
|
+
const req = http.request(url, {
|
|
249
|
+
method: 'POST',
|
|
250
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
251
|
+
}, res => {
|
|
252
|
+
let data = '';
|
|
253
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
254
|
+
res.on('end', () => {
|
|
255
|
+
try {
|
|
256
|
+
resolve(JSON.parse(data));
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
resolve({ ok: false, error: 'invalid callback response' });
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
req.on('error', e => reject(e));
|
|
264
|
+
req.write(body);
|
|
265
|
+
req.end();
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// Helpers
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
function safeRealpath(p) {
|
|
272
|
+
try {
|
|
273
|
+
return fs.realpathSync(p);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Message dispatcher
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
function handleMessage(msg) {
|
|
283
|
+
const { id, method, params } = msg;
|
|
284
|
+
switch (method) {
|
|
285
|
+
case 'initialize':
|
|
286
|
+
respond(id, {
|
|
287
|
+
protocolVersion: params?.protocolVersion || '2024-11-05',
|
|
288
|
+
capabilities: { tools: {} },
|
|
289
|
+
serverInfo: { name: 'pikiclaw-session', version: '1.0.0' },
|
|
290
|
+
});
|
|
291
|
+
break;
|
|
292
|
+
case 'notifications/initialized':
|
|
293
|
+
// Notification — no response needed
|
|
294
|
+
break;
|
|
295
|
+
case 'tools/list':
|
|
296
|
+
respond(id, { tools: TOOLS });
|
|
297
|
+
break;
|
|
298
|
+
case 'tools/call': {
|
|
299
|
+
const name = params?.name;
|
|
300
|
+
const args = params?.arguments || {};
|
|
301
|
+
switch (name) {
|
|
302
|
+
case 'pikiclaw_get_session_info':
|
|
303
|
+
handleGetSessionInfo(id);
|
|
304
|
+
break;
|
|
305
|
+
case 'pikiclaw_list_workspace_files':
|
|
306
|
+
handleListWorkspaceFiles(id, args);
|
|
307
|
+
break;
|
|
308
|
+
case 'pikiclaw_send_file':
|
|
309
|
+
void handleSendFile(id, args);
|
|
310
|
+
break;
|
|
311
|
+
default:
|
|
312
|
+
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
313
|
+
}
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
default:
|
|
317
|
+
if (id !== undefined) {
|
|
318
|
+
respondError(id, -32601, `Method not found: ${method}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
function enrichAgent(agent) {
|
|
2
|
+
const label = agent.agent === 'claude'
|
|
3
|
+
? 'Claude Code'
|
|
4
|
+
: agent.agent === 'gemini'
|
|
5
|
+
? 'Gemini CLI'
|
|
6
|
+
: 'Codex';
|
|
7
|
+
const installCommand = agent.agent === 'claude'
|
|
8
|
+
? 'npm install -g @anthropic-ai/claude-code'
|
|
9
|
+
: agent.agent === 'gemini'
|
|
10
|
+
? 'npm install -g @google/gemini-cli'
|
|
11
|
+
: 'npm install -g @openai/codex';
|
|
12
|
+
return {
|
|
13
|
+
...agent,
|
|
14
|
+
label,
|
|
15
|
+
installCommand,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function defaultChannelState(channel, tokenProvided) {
|
|
19
|
+
if (channel === 'telegram') {
|
|
20
|
+
return {
|
|
21
|
+
channel: 'telegram',
|
|
22
|
+
configured: tokenProvided,
|
|
23
|
+
ready: tokenProvided,
|
|
24
|
+
validated: false,
|
|
25
|
+
status: tokenProvided ? 'ready' : 'missing',
|
|
26
|
+
detail: tokenProvided ? 'Telegram credentials are configured.' : 'Telegram is not configured.',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (channel === 'feishu') {
|
|
30
|
+
return {
|
|
31
|
+
channel: 'feishu',
|
|
32
|
+
configured: tokenProvided,
|
|
33
|
+
ready: tokenProvided,
|
|
34
|
+
validated: false,
|
|
35
|
+
status: tokenProvided ? 'ready' : 'missing',
|
|
36
|
+
detail: tokenProvided ? 'Feishu credentials are configured.' : 'Feishu is not configured.',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
channel: 'whatsapp',
|
|
41
|
+
configured: tokenProvided,
|
|
42
|
+
ready: tokenProvided,
|
|
43
|
+
validated: false,
|
|
44
|
+
status: tokenProvided ? 'ready' : 'missing',
|
|
45
|
+
detail: tokenProvided ? 'WhatsApp credentials are configured.' : 'WhatsApp is not configured.',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function collectSetupState(args) {
|
|
49
|
+
return {
|
|
50
|
+
channel: args.channel,
|
|
51
|
+
tokenProvided: args.tokenProvided,
|
|
52
|
+
agents: args.agents.map(enrichAgent),
|
|
53
|
+
channels: args.channels?.length ? args.channels : [defaultChannelState(args.channel, args.tokenProvided)],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function agentSummary(state) {
|
|
57
|
+
if (!state.installed) {
|
|
58
|
+
return [
|
|
59
|
+
`MISSING ${state.label} is not installed.`,
|
|
60
|
+
` Install with: ${state.installCommand}`,
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
const version = state.version ? ` (${state.version})` : '';
|
|
64
|
+
return [
|
|
65
|
+
`OK ${state.label} found at ${state.path || '(unknown path)'}${version}`,
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
export function hasReadyAgent(state) {
|
|
69
|
+
return state.agents.some(agent => agent.installed);
|
|
70
|
+
}
|
|
71
|
+
export function hasInstalledAgent(state) {
|
|
72
|
+
return state.agents.some(agent => agent.installed);
|
|
73
|
+
}
|
|
74
|
+
export function isSetupReady(state) {
|
|
75
|
+
const readyChannel = state.channels?.some(channel => channel.ready) ?? state.tokenProvided;
|
|
76
|
+
return readyChannel && hasReadyAgent(state);
|
|
77
|
+
}
|
|
78
|
+
export function buildSetupGuide(state, version, options) {
|
|
79
|
+
const doctor = !!options?.doctor;
|
|
80
|
+
const isTelegram = state.channel === 'telegram';
|
|
81
|
+
const channelLabel = isTelegram ? 'Telegram' : state.channel === 'feishu' ? 'Feishu' : state.channel === 'whatsapp' ? 'WhatsApp' : 'your chat app';
|
|
82
|
+
const lines = [
|
|
83
|
+
`pikiclaw v${version}`,
|
|
84
|
+
'',
|
|
85
|
+
doctor ? 'Setup check' : 'First-time setup',
|
|
86
|
+
'',
|
|
87
|
+
`pikiclaw connects ${channelLabel} to a local coding agent running on your machine.`,
|
|
88
|
+
'Before the bot can start, make sure these basics are ready:',
|
|
89
|
+
'1. Claude Code, Codex, or Gemini CLI installed locally',
|
|
90
|
+
isTelegram
|
|
91
|
+
? '2. A Telegram bot token from @BotFather'
|
|
92
|
+
: '2. A supported channel token',
|
|
93
|
+
'',
|
|
94
|
+
'Step 1/2 Check your local coding agent',
|
|
95
|
+
];
|
|
96
|
+
for (const agent of state.agents)
|
|
97
|
+
lines.push(...agentSummary(agent));
|
|
98
|
+
lines.push('', isTelegram ? 'Step 2/2 Get a Telegram bot token' : 'Step 2/2 Check channel access');
|
|
99
|
+
if (isTelegram && state.tokenProvided) {
|
|
100
|
+
lines.push('OK A Telegram token was provided.');
|
|
101
|
+
}
|
|
102
|
+
else if (isTelegram) {
|
|
103
|
+
lines.push('MISSING No Telegram token configured in ~/.pikiclaw/setting.json', ' Run `pikiclaw` to open the dashboard and configure, or:', ' 1. Open Telegram and search for @BotFather', ' 2. Send /newbot and copy the token', ' 3. Add to ~/.pikiclaw/setting.json: { "telegramBotToken": "..." }');
|
|
104
|
+
}
|
|
105
|
+
else if (state.channel === 'feishu' && state.tokenProvided) {
|
|
106
|
+
lines.push('OK Feishu credentials provided (FEISHU_APP_ID + FEISHU_APP_SECRET).');
|
|
107
|
+
}
|
|
108
|
+
else if (state.channel === 'feishu') {
|
|
109
|
+
lines.push('MISSING No Feishu credentials configured in ~/.pikiclaw/setting.json', ' Run `pikiclaw` to open the dashboard and configure, or add feishuAppId/feishuAppSecret to setting.json.');
|
|
110
|
+
}
|
|
111
|
+
else if (state.channel === 'whatsapp') {
|
|
112
|
+
lines.push('MISSING WhatsApp setup is not available yet. Use `--channel telegram` for now.');
|
|
113
|
+
}
|
|
114
|
+
else if (state.tokenProvided) {
|
|
115
|
+
lines.push('OK A channel token was provided.');
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
lines.push('MISSING No supported channel token was provided.');
|
|
119
|
+
}
|
|
120
|
+
lines.push('');
|
|
121
|
+
if (state.tokenProvided) {
|
|
122
|
+
lines.push('Start command:');
|
|
123
|
+
lines.push(' npx pikiclaw@latest');
|
|
124
|
+
}
|
|
125
|
+
else if (!isTelegram) {
|
|
126
|
+
lines.push('Start command:');
|
|
127
|
+
lines.push(' npx pikiclaw@latest --channel telegram -t <YOUR_BOT_TOKEN>');
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
lines.push('Start command after you have the token:');
|
|
131
|
+
lines.push(' npx pikiclaw@latest -t <YOUR_BOT_TOKEN>');
|
|
132
|
+
}
|
|
133
|
+
lines.push('', 'Tips:', ' - Run `npx pikiclaw@latest --doctor` any time to re-check your setup.', ' - Run `npx pikiclaw@latest --help` for the full CLI reference.');
|
|
134
|
+
if (!doctor && !hasInstalledAgent(state)) {
|
|
135
|
+
lines.push('', 'You only need one local coding agent. Install Claude Code or Codex, then come back.');
|
|
136
|
+
}
|
|
137
|
+
return `${lines.join('\n')}\n`;
|
|
138
|
+
}
|