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,259 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
export const PROCESS_RESTART_EXIT_CODE = 75;
|
|
6
|
+
export const PROCESS_RESTART_STATE_FILE_ENV = 'PIKICLAW_RESTART_STATE_FILE';
|
|
7
|
+
const runtimes = new Map();
|
|
8
|
+
let nextRuntimeId = 1;
|
|
9
|
+
let restartInFlight = false;
|
|
10
|
+
export function shellSplit(str) {
|
|
11
|
+
const args = [];
|
|
12
|
+
let cur = '';
|
|
13
|
+
let inSingle = false;
|
|
14
|
+
let inDouble = false;
|
|
15
|
+
for (const ch of str) {
|
|
16
|
+
if (ch === '\'' && !inDouble) {
|
|
17
|
+
inSingle = !inSingle;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (ch === '"' && !inSingle) {
|
|
21
|
+
inDouble = !inDouble;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (ch === ' ' && !inSingle && !inDouble) {
|
|
25
|
+
if (cur)
|
|
26
|
+
args.push(cur);
|
|
27
|
+
cur = '';
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
cur += ch;
|
|
31
|
+
}
|
|
32
|
+
if (cur)
|
|
33
|
+
args.push(cur);
|
|
34
|
+
return args;
|
|
35
|
+
}
|
|
36
|
+
function isNpxBinary(bin) {
|
|
37
|
+
return path.basename(bin, path.extname(bin)).toLowerCase() === 'npx';
|
|
38
|
+
}
|
|
39
|
+
export function ensureNonInteractiveRestartArgs(bin, args) {
|
|
40
|
+
if (!isNpxBinary(bin))
|
|
41
|
+
return args;
|
|
42
|
+
if (args.includes('--yes') || args.includes('-y'))
|
|
43
|
+
return args;
|
|
44
|
+
return ['--yes', ...args];
|
|
45
|
+
}
|
|
46
|
+
export function getDefaultRestartCmd() {
|
|
47
|
+
const argv1 = process.argv[1] ?? '';
|
|
48
|
+
if (argv1.endsWith('.ts') || argv1.includes('/tsx') || argv1.includes('/ts-node')) {
|
|
49
|
+
const isTsxLoader = !process.argv[0]?.includes('/tsx')
|
|
50
|
+
&& process.execArgv?.some(arg => arg.includes('tsx'));
|
|
51
|
+
const parts = isTsxLoader ? ['tsx', argv1] : process.argv.slice(0, 2);
|
|
52
|
+
return parts.map(arg => arg.includes(' ') ? `"${arg}"` : arg).join(' ');
|
|
53
|
+
}
|
|
54
|
+
return 'npx --yes pikiclaw@latest';
|
|
55
|
+
}
|
|
56
|
+
export function buildRestartCommand(argv, restartCmd = process.env.PIKICLAW_RESTART_CMD || getDefaultRestartCmd()) {
|
|
57
|
+
const [bin, ...rawArgs] = shellSplit(restartCmd);
|
|
58
|
+
return {
|
|
59
|
+
bin,
|
|
60
|
+
args: [...ensureNonInteractiveRestartArgs(bin, rawArgs), ...argv],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function registerProcessRuntime(runtime) {
|
|
64
|
+
const id = nextRuntimeId++;
|
|
65
|
+
runtimes.set(id, runtime);
|
|
66
|
+
return () => {
|
|
67
|
+
runtimes.delete(id);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function getRegisteredRuntimeCount() {
|
|
71
|
+
return runtimes.size;
|
|
72
|
+
}
|
|
73
|
+
export function getActiveTaskCount() {
|
|
74
|
+
let total = 0;
|
|
75
|
+
for (const runtime of runtimes.values()) {
|
|
76
|
+
total += Math.max(0, runtime.getActiveTaskCount?.() || 0);
|
|
77
|
+
}
|
|
78
|
+
return total;
|
|
79
|
+
}
|
|
80
|
+
export function formatActiveTaskRestartError(activeTasks) {
|
|
81
|
+
return `${activeTasks} task(s) still running. Wait for them to finish or try again.`;
|
|
82
|
+
}
|
|
83
|
+
export function createRestartStateFilePath(ownerPid = process.pid) {
|
|
84
|
+
const dir = path.join(os.tmpdir(), 'pikiclaw');
|
|
85
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
86
|
+
return path.join(dir, `restart-${ownerPid}.json`);
|
|
87
|
+
}
|
|
88
|
+
export function clearRestartStateFile(filePath) {
|
|
89
|
+
if (!filePath)
|
|
90
|
+
return;
|
|
91
|
+
try {
|
|
92
|
+
fs.unlinkSync(filePath);
|
|
93
|
+
}
|
|
94
|
+
catch { }
|
|
95
|
+
}
|
|
96
|
+
export function writeRestartStateFile(filePath, env) {
|
|
97
|
+
const payload = { version: 1, env };
|
|
98
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
99
|
+
fs.writeFileSync(filePath, JSON.stringify(payload), 'utf8');
|
|
100
|
+
}
|
|
101
|
+
export function consumeRestartStateFile(filePath) {
|
|
102
|
+
if (!filePath)
|
|
103
|
+
return {};
|
|
104
|
+
try {
|
|
105
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
106
|
+
const parsed = JSON.parse(raw);
|
|
107
|
+
if (parsed?.version !== 1 || !parsed.env || typeof parsed.env !== 'object')
|
|
108
|
+
return {};
|
|
109
|
+
return Object.fromEntries(Object.entries(parsed.env)
|
|
110
|
+
.filter((entry) => typeof entry[0] === 'string' && typeof entry[1] === 'string')
|
|
111
|
+
.map(([key, value]) => [key, value.trim()]));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return {};
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
clearRestartStateFile(filePath);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function mergeEnvValues(target, patch) {
|
|
121
|
+
for (const [key, rawValue] of Object.entries(patch)) {
|
|
122
|
+
const value = rawValue.trim();
|
|
123
|
+
if (!value)
|
|
124
|
+
continue;
|
|
125
|
+
if (!target[key]) {
|
|
126
|
+
target[key] = value;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const merged = new Set([
|
|
130
|
+
...target[key].split(',').map(item => item.trim()).filter(Boolean),
|
|
131
|
+
...value.split(',').map(item => item.trim()).filter(Boolean),
|
|
132
|
+
]);
|
|
133
|
+
target[key] = [...merged].join(',');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function collectRestartEnv() {
|
|
137
|
+
const env = {};
|
|
138
|
+
for (const runtime of runtimes.values()) {
|
|
139
|
+
const patch = runtime.buildRestartEnv?.() || {};
|
|
140
|
+
mergeEnvValues(env, patch);
|
|
141
|
+
}
|
|
142
|
+
return env;
|
|
143
|
+
}
|
|
144
|
+
async function prepareRuntimesForRestart(log) {
|
|
145
|
+
for (const runtime of [...runtimes.values()]) {
|
|
146
|
+
const label = runtime.label ? `${runtime.label}: ` : '';
|
|
147
|
+
try {
|
|
148
|
+
await runtime.prepareForRestart?.();
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
152
|
+
log?.(`restart cleanup failed (${label}${message})`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function buildRestartEnvForSpawn(extraEnv) {
|
|
157
|
+
const env = {
|
|
158
|
+
...process.env,
|
|
159
|
+
...extraEnv,
|
|
160
|
+
npm_config_yes: process.env.npm_config_yes || 'true',
|
|
161
|
+
};
|
|
162
|
+
delete env.PIKICLAW_DAEMON_CHILD;
|
|
163
|
+
delete env[PROCESS_RESTART_STATE_FILE_ENV];
|
|
164
|
+
return env;
|
|
165
|
+
}
|
|
166
|
+
function spawnReplacementProcess(bin, args, env, log) {
|
|
167
|
+
const child = spawn(bin, args, {
|
|
168
|
+
stdio: 'inherit',
|
|
169
|
+
detached: true,
|
|
170
|
+
env,
|
|
171
|
+
});
|
|
172
|
+
child.unref();
|
|
173
|
+
log?.(`restart: new process spawned (PID ${child.pid})`);
|
|
174
|
+
return child;
|
|
175
|
+
}
|
|
176
|
+
export async function requestProcessRestart(opts = {}) {
|
|
177
|
+
const activeTasks = getActiveTaskCount();
|
|
178
|
+
if (activeTasks > 0) {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
restarting: false,
|
|
182
|
+
error: formatActiveTaskRestartError(activeTasks),
|
|
183
|
+
activeTasks,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (restartInFlight) {
|
|
187
|
+
return {
|
|
188
|
+
ok: true,
|
|
189
|
+
restarting: true,
|
|
190
|
+
error: null,
|
|
191
|
+
activeTasks: 0,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
restartInFlight = true;
|
|
195
|
+
const log = opts.log;
|
|
196
|
+
const exit = opts.exit || process.exit;
|
|
197
|
+
try {
|
|
198
|
+
const extraEnv = collectRestartEnv();
|
|
199
|
+
await prepareRuntimesForRestart(log);
|
|
200
|
+
if (process.env.PIKICLAW_DAEMON_CHILD === '1') {
|
|
201
|
+
const restartStateFile = process.env[PROCESS_RESTART_STATE_FILE_ENV];
|
|
202
|
+
if (restartStateFile) {
|
|
203
|
+
if (Object.keys(extraEnv).length)
|
|
204
|
+
writeRestartStateFile(restartStateFile, extraEnv);
|
|
205
|
+
else
|
|
206
|
+
clearRestartStateFile(restartStateFile);
|
|
207
|
+
}
|
|
208
|
+
log?.('restart: handing off to daemon supervisor');
|
|
209
|
+
exit(PROCESS_RESTART_EXIT_CODE);
|
|
210
|
+
return { ok: true, restarting: true, error: null, activeTasks: 0 };
|
|
211
|
+
}
|
|
212
|
+
const { bin, args } = buildRestartCommand(opts.argv || process.argv.slice(2), opts.restartCmd);
|
|
213
|
+
log?.(`restart: spawning \`${bin} ${args.join(' ')}\``);
|
|
214
|
+
spawnReplacementProcess(bin, args, buildRestartEnvForSpawn(extraEnv), log);
|
|
215
|
+
exit(0);
|
|
216
|
+
return { ok: true, restarting: true, error: null, activeTasks: 0 };
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
restartInFlight = false;
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
restarting: false,
|
|
223
|
+
error: err instanceof Error ? err.message : String(err),
|
|
224
|
+
activeTasks: 0,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
export function terminateProcessTree(target, opts = {}) {
|
|
229
|
+
const pid = typeof target === 'number' ? target : target?.pid;
|
|
230
|
+
if (!pid || pid <= 0)
|
|
231
|
+
return;
|
|
232
|
+
const signal = opts.signal ?? 'SIGTERM';
|
|
233
|
+
const forceSignal = opts.forceSignal ?? null;
|
|
234
|
+
const forceAfterMs = opts.forceAfterMs ?? 0;
|
|
235
|
+
const killPid = (targetPid, nextSignal) => {
|
|
236
|
+
try {
|
|
237
|
+
if (process.platform === 'win32') {
|
|
238
|
+
const args = ['/pid', String(targetPid), '/t'];
|
|
239
|
+
if (nextSignal === 'SIGKILL')
|
|
240
|
+
args.push('/f');
|
|
241
|
+
const killer = spawn('taskkill', args, { stdio: 'ignore', windowsHide: true });
|
|
242
|
+
killer.unref();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
process.kill(-targetPid, nextSignal);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
try {
|
|
249
|
+
process.kill(targetPid, nextSignal);
|
|
250
|
+
}
|
|
251
|
+
catch { }
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
killPid(pid, signal);
|
|
255
|
+
if (forceSignal == null || forceAfterMs <= 0 || forceSignal === signal)
|
|
256
|
+
return;
|
|
257
|
+
const timer = setTimeout(() => killPid(pid, forceSignal), forceAfterMs);
|
|
258
|
+
timer.unref?.();
|
|
259
|
+
}
|
package/dist/run.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* run.ts — Standalone CLI commands for pikiclaw.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npm run command -- status
|
|
7
|
+
* npm run command -- claude-models
|
|
8
|
+
* npm run command -- codex-models
|
|
9
|
+
*/
|
|
10
|
+
import { formatThinkingForDisplay } from './bot.js';
|
|
11
|
+
import { initializeProjectSkills, listAgents, listModels, listSkills, getUsage, doStream, getSessions, getSessionTail } from './code-agent.js';
|
|
12
|
+
import { loadUserConfig, resolveUserWorkdir } from './user-config.js';
|
|
13
|
+
function parseArgs(argv) {
|
|
14
|
+
const args = {
|
|
15
|
+
command: null, model: null, workdir: null, prompt: null, timeout: 1800, help: false,
|
|
16
|
+
session: null, n: 4,
|
|
17
|
+
};
|
|
18
|
+
const positional = [];
|
|
19
|
+
const it = argv[Symbol.iterator]();
|
|
20
|
+
for (const arg of it) {
|
|
21
|
+
switch (arg) {
|
|
22
|
+
case '-m':
|
|
23
|
+
case '--model':
|
|
24
|
+
args.model = it.next().value;
|
|
25
|
+
break;
|
|
26
|
+
case '-w':
|
|
27
|
+
case '--workdir':
|
|
28
|
+
args.workdir = it.next().value;
|
|
29
|
+
break;
|
|
30
|
+
case '-p':
|
|
31
|
+
case '--prompt':
|
|
32
|
+
args.prompt = it.next().value;
|
|
33
|
+
break;
|
|
34
|
+
case '-s':
|
|
35
|
+
case '--session':
|
|
36
|
+
args.session = it.next().value;
|
|
37
|
+
break;
|
|
38
|
+
case '-n':
|
|
39
|
+
args.n = parseInt(it.next().value ?? '', 10) || 4;
|
|
40
|
+
break;
|
|
41
|
+
case '--timeout':
|
|
42
|
+
args.timeout = parseInt(it.next().value ?? '', 10) || 1800;
|
|
43
|
+
break;
|
|
44
|
+
case '-h':
|
|
45
|
+
case '--help':
|
|
46
|
+
args.help = true;
|
|
47
|
+
break;
|
|
48
|
+
default:
|
|
49
|
+
if (arg.startsWith('-')) {
|
|
50
|
+
process.stderr.write(`Unknown option: ${arg}\n`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
else
|
|
54
|
+
positional.push(arg);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
args.command = positional[0] ?? null;
|
|
58
|
+
// If no -p flag, treat remaining positional args as the prompt
|
|
59
|
+
if (!args.prompt && positional.length > 1)
|
|
60
|
+
args.prompt = positional.slice(1).join(' ');
|
|
61
|
+
return args;
|
|
62
|
+
}
|
|
63
|
+
const HELP = `pikiclaw run — standalone commands
|
|
64
|
+
|
|
65
|
+
Usage:
|
|
66
|
+
npm run command -- <command> [options]
|
|
67
|
+
|
|
68
|
+
Commands:
|
|
69
|
+
skills List project-defined custom skills (.pikiclaw/skills)
|
|
70
|
+
claude-run Run a single Claude prompt and print the result
|
|
71
|
+
codex-run Run a single Codex prompt and print the result
|
|
72
|
+
claude-status Show Claude agent info and API usage
|
|
73
|
+
codex-status Show Codex agent info and API usage
|
|
74
|
+
claude-models List available Claude models
|
|
75
|
+
codex-models List available Codex models
|
|
76
|
+
claude-sessions List recent Claude sessions for the workdir
|
|
77
|
+
codex-sessions List recent Codex sessions for the workdir
|
|
78
|
+
claude-tail Show last N messages of a Claude session
|
|
79
|
+
codex-tail Show last N messages of a Codex session
|
|
80
|
+
|
|
81
|
+
Options:
|
|
82
|
+
-p, --prompt <text> Prompt text (or pass after command as positional args)
|
|
83
|
+
-m, --model <model> Model to use / highlight
|
|
84
|
+
-w, --workdir <dir> Working directory [default: current process cwd]
|
|
85
|
+
-s, --session <id> Session ID (for tail; omit to use latest session)
|
|
86
|
+
-n <count> Number of messages to show [default: 4]
|
|
87
|
+
--timeout <seconds> Max seconds per request [default: 1800]
|
|
88
|
+
-h, --help Print this help
|
|
89
|
+
|
|
90
|
+
Examples:
|
|
91
|
+
npm run command -- claude-run -p "Hello world"
|
|
92
|
+
npm run command -- codex-run -m o3 "Explain this repo"
|
|
93
|
+
npm run command -- claude-run -m sonnet --timeout 60 -p "What is 1+1?"
|
|
94
|
+
npm run command -- claude-tail
|
|
95
|
+
npm run command -- claude-tail -n 10 -s <session-id>
|
|
96
|
+
`;
|
|
97
|
+
async function main() {
|
|
98
|
+
const args = parseArgs(process.argv.slice(2));
|
|
99
|
+
const userConfig = loadUserConfig();
|
|
100
|
+
const workdir = resolveUserWorkdir({ workdir: args.workdir, config: userConfig });
|
|
101
|
+
initializeProjectSkills(workdir);
|
|
102
|
+
if (args.help || !args.command) {
|
|
103
|
+
process.stdout.write(HELP);
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
switch (args.command) {
|
|
107
|
+
case 'skills': {
|
|
108
|
+
const result = listSkills(workdir);
|
|
109
|
+
if (!result.skills.length) {
|
|
110
|
+
process.stdout.write(`No custom skills found in ${workdir} (.pikiclaw/skills, .claude/commands)\n`);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
process.stdout.write(`Project skills (${result.skills.length}):\n\n`);
|
|
114
|
+
for (const sk of result.skills) {
|
|
115
|
+
const src = sk.source === 'skills' ? 'skill' : 'command';
|
|
116
|
+
const desc = sk.description ? ` ${sk.description}` : '';
|
|
117
|
+
process.stdout.write(` ${sk.name} [${src}]${desc}\n`);
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case 'claude-status': {
|
|
122
|
+
const info = listAgents({ includeVersion: true }).agents.find(a => a.agent === 'claude');
|
|
123
|
+
const mark = info.installed ? '\u2713' : '\u2717';
|
|
124
|
+
process.stdout.write(`${mark} claude ${info.version ?? 'not installed'} ${info.path ?? ''}\n`);
|
|
125
|
+
const usage = getUsage({ agent: 'claude' });
|
|
126
|
+
if (usage.error) {
|
|
127
|
+
process.stdout.write(` ${usage.error}\n`);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
for (const w of usage.windows) {
|
|
131
|
+
process.stdout.write(` [${w.label}] ${w.usedPercent ?? '?'}% used status=${w.status ?? 'n/a'}\n`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case 'codex-status': {
|
|
137
|
+
const info = listAgents({ includeVersion: true }).agents.find(a => a.agent === 'codex');
|
|
138
|
+
const mark = info.installed ? '\u2713' : '\u2717';
|
|
139
|
+
process.stdout.write(`${mark} codex ${info.version ?? 'not installed'} ${info.path ?? ''}\n`);
|
|
140
|
+
const usage = getUsage({ agent: 'codex' });
|
|
141
|
+
if (usage.error) {
|
|
142
|
+
process.stdout.write(` ${usage.error}\n`);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
for (const w of usage.windows) {
|
|
146
|
+
process.stdout.write(` [${w.label}] ${w.usedPercent ?? '?'}% used status=${w.status ?? 'n/a'}\n`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case 'claude-models': {
|
|
152
|
+
const result = await listModels('claude', { workdir, currentModel: args.model });
|
|
153
|
+
process.stdout.write(`Claude models${result.note ? ` (${result.note})` : ''}:\n`);
|
|
154
|
+
for (const m of result.models) {
|
|
155
|
+
process.stdout.write(` ${m.id}${m.alias ? ` (${m.alias})` : ''}\n`);
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case 'codex-models': {
|
|
160
|
+
const result = await listModels('codex', { workdir, currentModel: args.model });
|
|
161
|
+
process.stdout.write(`Codex models${result.note ? ` (${result.note})` : ''}:\n`);
|
|
162
|
+
for (const m of result.models) {
|
|
163
|
+
process.stdout.write(` ${m.id}${m.alias ? ` (${m.alias})` : ''}\n`);
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case 'claude-sessions':
|
|
168
|
+
case 'codex-sessions': {
|
|
169
|
+
const agent = args.command === 'codex-sessions' ? 'codex' : 'claude';
|
|
170
|
+
const limit = 20;
|
|
171
|
+
const result = await getSessions({ agent, workdir, limit });
|
|
172
|
+
if (!result.ok) {
|
|
173
|
+
process.stderr.write(`Error: ${result.error}\n`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
if (!result.sessions.length) {
|
|
177
|
+
process.stdout.write(`No ${agent} sessions found for ${workdir}\n`);
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
process.stdout.write(`${agent} sessions (${result.sessions.length}) for ${workdir}:\n\n`);
|
|
181
|
+
for (const s of result.sessions) {
|
|
182
|
+
const run = s.running ? ' [RUNNING]' : '';
|
|
183
|
+
const date = s.createdAt ? s.createdAt.replace('T', ' ').slice(0, 19) : '?';
|
|
184
|
+
const model = s.model ? ` model=${s.model}` : '';
|
|
185
|
+
const title = s.title ? ` ${s.title}` : '';
|
|
186
|
+
const displayId = s.sessionId || '(none)';
|
|
187
|
+
process.stdout.write(` ${displayId} ${date}${model}${run}\n`);
|
|
188
|
+
if (title)
|
|
189
|
+
process.stdout.write(` ${title}\n`);
|
|
190
|
+
}
|
|
191
|
+
process.exit(0);
|
|
192
|
+
}
|
|
193
|
+
case 'claude-tail':
|
|
194
|
+
case 'codex-tail': {
|
|
195
|
+
const agent = args.command === 'codex-tail' ? 'codex' : 'claude';
|
|
196
|
+
let sessionId = args.session;
|
|
197
|
+
// Default: find the latest session
|
|
198
|
+
if (!sessionId) {
|
|
199
|
+
const sessions = await getSessions({ agent, workdir, limit: 1 });
|
|
200
|
+
if (!sessions.ok || !sessions.sessions.length) {
|
|
201
|
+
process.stderr.write(`No ${agent} sessions found for ${workdir}\n`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
sessionId = sessions.sessions[0].sessionId;
|
|
205
|
+
if (!sessionId) {
|
|
206
|
+
process.stderr.write(`Latest ${agent} session has no usable session ID\n`);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const tail = await getSessionTail({ agent, sessionId, workdir, limit: args.n });
|
|
211
|
+
if (!tail.ok) {
|
|
212
|
+
process.stderr.write(`Error: ${tail.error}\n`);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
if (!tail.messages.length) {
|
|
216
|
+
process.stdout.write(`No messages found in session ${sessionId}\n`);
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
process.stdout.write(`${agent} session ${sessionId.slice(0, 16)} (last ${tail.messages.length} messages)\n\n`);
|
|
220
|
+
for (const m of tail.messages) {
|
|
221
|
+
const icon = m.role === 'user' ? '👤 User' : '🤖 Assistant';
|
|
222
|
+
const preview = m.text.length > 300 ? m.text.slice(0, 300) + '...' : m.text;
|
|
223
|
+
process.stdout.write(`${icon}:\n${preview}\n\n`);
|
|
224
|
+
}
|
|
225
|
+
process.exit(0);
|
|
226
|
+
}
|
|
227
|
+
case 'claude-run':
|
|
228
|
+
case 'codex-run': {
|
|
229
|
+
const agent = args.command === 'codex-run' ? 'codex' : 'claude';
|
|
230
|
+
const prompt = args.prompt;
|
|
231
|
+
if (!prompt) {
|
|
232
|
+
process.stderr.write(`Missing prompt. Use -p "..." or pass text after the command.\n`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
const opts = {
|
|
236
|
+
agent, prompt, workdir, timeout: args.timeout,
|
|
237
|
+
sessionId: null, model: null, thinkingEffort: 'max',
|
|
238
|
+
onText: (text, _thinking) => {
|
|
239
|
+
process.stdout.write(`\r\x1b[K${text.slice(-120)}`);
|
|
240
|
+
},
|
|
241
|
+
claudeModel: agent === 'claude' ? (args.model || undefined) : undefined,
|
|
242
|
+
claudePermissionMode: agent === 'claude' ? 'bypassPermissions' : undefined,
|
|
243
|
+
codexModel: agent === 'codex' ? (args.model || undefined) : undefined,
|
|
244
|
+
codexFullAccess: agent === 'codex' ? true : undefined,
|
|
245
|
+
};
|
|
246
|
+
process.stdout.write(`Running ${agent}${args.model ? ` (model: ${args.model})` : ''}...\n`);
|
|
247
|
+
const result = await doStream(opts);
|
|
248
|
+
// Clear the streaming line and print final result
|
|
249
|
+
process.stdout.write('\r\x1b[K');
|
|
250
|
+
process.stdout.write(`--- ${agent} result ---\n`);
|
|
251
|
+
process.stdout.write(`ok: ${result.ok}\n`);
|
|
252
|
+
process.stdout.write(`model: ${result.model ?? '(unknown)'}\n`);
|
|
253
|
+
process.stdout.write(`session: ${result.sessionId ?? '(none)'}\n`);
|
|
254
|
+
process.stdout.write(`elapsed: ${result.elapsedS.toFixed(1)}s\n`);
|
|
255
|
+
process.stdout.write(`tokens: in=${result.inputTokens ?? '?'} out=${result.outputTokens ?? '?'} cached=${result.cachedInputTokens ?? '?'} cacheCreate=${result.cacheCreationInputTokens ?? '?'}\n`);
|
|
256
|
+
if (result.contextPercent != null) {
|
|
257
|
+
process.stdout.write(`context: ${result.contextUsedTokens}/${result.contextWindow} (${result.contextPercent}%)\n`);
|
|
258
|
+
}
|
|
259
|
+
process.stdout.write(`stop: ${result.stopReason ?? 'n/a'}\n`);
|
|
260
|
+
if (result.error)
|
|
261
|
+
process.stdout.write(`error: ${result.error}\n`);
|
|
262
|
+
process.stdout.write(`---\n`);
|
|
263
|
+
if (result.thinking) {
|
|
264
|
+
process.stdout.write(`\n<thinking>\n${formatThinkingForDisplay(result.thinking, 800)}\n</thinking>\n`);
|
|
265
|
+
}
|
|
266
|
+
process.stdout.write(`\n${result.message}\n`);
|
|
267
|
+
process.exit(result.ok ? 0 : 1);
|
|
268
|
+
}
|
|
269
|
+
default:
|
|
270
|
+
process.stderr.write(`Unknown command: ${args.command}\n`);
|
|
271
|
+
process.stderr.write(`Available commands: skills, claude-run, codex-run, claude-status, codex-status, claude-models, codex-models, claude-sessions, codex-sessions, claude-tail, codex-tail\n`);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
main().catch(err => { console.error(err); process.exit(1); });
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function getSessionRuntime(bot, session) {
|
|
2
|
+
const sessionId = session.sessionId || null;
|
|
3
|
+
if (!sessionId)
|
|
4
|
+
return null;
|
|
5
|
+
return bot.sessionStates.get(`${session.agent}:${sessionId}`) || null;
|
|
6
|
+
}
|
|
7
|
+
export function getSessionStatusForChat(bot, chat, session) {
|
|
8
|
+
const runtime = getSessionRuntime(bot, session);
|
|
9
|
+
const sessionId = session.sessionId || null;
|
|
10
|
+
const isCurrent = !!sessionId && (runtime
|
|
11
|
+
? chat.activeSessionKey === runtime.key
|
|
12
|
+
: chat.agent === session.agent && chat.sessionId === sessionId);
|
|
13
|
+
return {
|
|
14
|
+
runtime,
|
|
15
|
+
isCurrent,
|
|
16
|
+
isRunning: !!runtime?.runningTaskIds.size || !!session.running,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function getSessionStatusForBot(bot, session) {
|
|
20
|
+
const runtime = getSessionRuntime(bot, session);
|
|
21
|
+
const sessionId = session.sessionId || null;
|
|
22
|
+
let isCurrent = false;
|
|
23
|
+
if (sessionId) {
|
|
24
|
+
for (const [, chat] of bot.chats) {
|
|
25
|
+
if (runtime) {
|
|
26
|
+
if (chat.activeSessionKey === runtime.key) {
|
|
27
|
+
isCurrent = true;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (chat.agent === session.agent && chat.sessionId === sessionId) {
|
|
33
|
+
isCurrent = true;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
runtime,
|
|
40
|
+
isCurrent,
|
|
41
|
+
isRunning: !!runtime?.runningTaskIds.size || !!session.running,
|
|
42
|
+
};
|
|
43
|
+
}
|