pikiloop 0.4.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/LICENSE +21 -0
- package/README.md +353 -0
- package/README.v2.md +287 -0
- package/README.zh-CN.md +352 -0
- package/dashboard/dist/assets/AgentTab-UZPIhlkr.js +1 -0
- package/dashboard/dist/assets/DirBrowser-Ckcmi-Pi.js +1 -0
- package/dashboard/dist/assets/ExtensionsTab-KZhEDrdu.js +1 -0
- package/dashboard/dist/assets/IMAccessTab-Bd_IY1GQ.js +1 -0
- package/dashboard/dist/assets/Modal-CTeL0y7P.js +1 -0
- package/dashboard/dist/assets/Modals-axftHasy.js +1 -0
- package/dashboard/dist/assets/Select-C8tOdPhe.js +1 -0
- package/dashboard/dist/assets/SessionPanel-C1geSRxw.js +1 -0
- package/dashboard/dist/assets/SystemTab-DBDkaPiO.js +1 -0
- package/dashboard/dist/assets/anthropic-BAdojD7P.ico +0 -0
- package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
- package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
- package/dashboard/dist/assets/doubao-DloFDuFR.png +0 -0
- package/dashboard/dist/assets/feishu-C4OMrjCW.ico +0 -0
- package/dashboard/dist/assets/gemini-BYkEpiWr.svg +1 -0
- package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
- package/dashboard/dist/assets/index-CpM4CqZJ.js +23 -0
- package/dashboard/dist/assets/index-DXSohzrE.js +3 -0
- package/dashboard/dist/assets/index-reSbuley.css +1 -0
- package/dashboard/dist/assets/markdown-DxQYQFeH.js +29 -0
- package/dashboard/dist/assets/minimax-PuEGTfrF.ico +0 -0
- package/dashboard/dist/assets/mlx-DhWwjtMw.png +0 -0
- package/dashboard/dist/assets/ollama-Bt9O-2K_.png +0 -0
- package/dashboard/dist/assets/openrouter-CsJ_bD5Q.ico +0 -0
- package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
- package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
- package/dashboard/dist/assets/react-vendor-C7Sl8SE7.js +9 -0
- package/dashboard/dist/assets/router-DHISdpPk.js +3 -0
- package/dashboard/dist/assets/shared-BIP_4k4I.js +1 -0
- package/dashboard/dist/favicon.svg +28 -0
- package/dashboard/dist/index.html +17 -0
- package/dist/agent/acp-client.js +261 -0
- package/dist/agent/auto-update.js +432 -0
- package/dist/agent/await-resume.js +50 -0
- package/dist/agent/cli/auth.js +325 -0
- package/dist/agent/cli/catalog.js +40 -0
- package/dist/agent/cli/detector.js +136 -0
- package/dist/agent/cli/index.js +7 -0
- package/dist/agent/cli/registry.js +33 -0
- package/dist/agent/driver.js +39 -0
- package/dist/agent/drivers/claude-tui.js +2297 -0
- package/dist/agent/drivers/claude.js +2689 -0
- package/dist/agent/drivers/codex.js +2210 -0
- package/dist/agent/drivers/gemini.js +1059 -0
- package/dist/agent/drivers/hermes.js +795 -0
- package/dist/agent/goal.js +274 -0
- package/dist/agent/handover.js +130 -0
- package/dist/agent/images.js +355 -0
- package/dist/agent/index.js +50 -0
- package/dist/agent/mcp/bridge.js +791 -0
- package/dist/agent/mcp/extensions.js +637 -0
- package/dist/agent/mcp/oauth.js +353 -0
- package/dist/agent/mcp/registry.js +119 -0
- package/dist/agent/mcp/session-server.js +229 -0
- package/dist/agent/mcp/tools/ask-user.js +113 -0
- package/dist/agent/mcp/tools/await-resume.js +77 -0
- package/dist/agent/mcp/tools/goal.js +144 -0
- package/dist/agent/mcp/tools/types.js +12 -0
- package/dist/agent/mcp/tools/workspace.js +212 -0
- package/dist/agent/npm.js +31 -0
- package/dist/agent/session.js +1206 -0
- package/dist/agent/skill-installer.js +160 -0
- package/dist/agent/skills.js +257 -0
- package/dist/agent/stream.js +743 -0
- package/dist/agent/types.js +13 -0
- package/dist/agent/utils.js +687 -0
- package/dist/bot/bot.js +2499 -0
- package/dist/bot/command-ui.js +633 -0
- package/dist/bot/commands.js +513 -0
- package/dist/bot/headless-bot.js +36 -0
- package/dist/bot/host.js +192 -0
- package/dist/bot/human-loop.js +168 -0
- package/dist/bot/menu.js +48 -0
- package/dist/bot/orchestration.js +79 -0
- package/dist/bot/render-shared.js +309 -0
- package/dist/bot/session-hub.js +361 -0
- package/dist/bot/session-status.js +55 -0
- package/dist/bot/streaming.js +309 -0
- package/dist/browser-profile.js +579 -0
- package/dist/browser-supervisor.js +249 -0
- package/dist/catalog/cli-tools.js +421 -0
- package/dist/catalog/index.js +21 -0
- package/dist/catalog/local-models.js +94 -0
- package/dist/catalog/mcp-servers.js +315 -0
- package/dist/catalog/skill-repos.js +173 -0
- package/dist/channels/base.js +55 -0
- package/dist/channels/dingtalk/bot.js +549 -0
- package/dist/channels/dingtalk/channel.js +268 -0
- package/dist/channels/discord/bot.js +552 -0
- package/dist/channels/discord/channel.js +245 -0
- package/dist/channels/feishu/bot.js +1275 -0
- package/dist/channels/feishu/channel.js +911 -0
- package/dist/channels/feishu/markdown.js +91 -0
- package/dist/channels/feishu/render.js +619 -0
- package/dist/channels/health.js +109 -0
- package/dist/channels/slack/bot.js +554 -0
- package/dist/channels/slack/channel.js +283 -0
- package/dist/channels/states.js +6 -0
- package/dist/channels/telegram/bot.js +1310 -0
- package/dist/channels/telegram/channel.js +820 -0
- package/dist/channels/telegram/directory.js +111 -0
- package/dist/channels/telegram/live-preview.js +220 -0
- package/dist/channels/telegram/render.js +384 -0
- package/dist/channels/wecom/bot.js +558 -0
- package/dist/channels/wecom/channel.js +479 -0
- package/dist/channels/weixin/api.js +520 -0
- package/dist/channels/weixin/bot.js +1000 -0
- package/dist/channels/weixin/channel.js +222 -0
- package/dist/cli/autostart.js +262 -0
- package/dist/cli/channel-supervisor.js +313 -0
- package/dist/cli/channels.js +54 -0
- package/dist/cli/main.js +726 -0
- package/dist/cli/onboarding.js +227 -0
- package/dist/cli/run.js +308 -0
- package/dist/cli/setup-wizard.js +235 -0
- package/dist/core/config/runtime-config.js +201 -0
- package/dist/core/config/user-config.js +510 -0
- package/dist/core/config/validation.js +521 -0
- package/dist/core/constants.js +400 -0
- package/dist/core/git.js +145 -0
- package/dist/core/legacy-compat.js +60 -0
- package/dist/core/logging.js +101 -0
- package/dist/core/platform.js +59 -0
- package/dist/core/process-control.js +315 -0
- package/dist/core/secrets/index.js +42 -0
- package/dist/core/secrets/inline-seal.js +60 -0
- package/dist/core/secrets/ref.js +33 -0
- package/dist/core/secrets/resolver.js +65 -0
- package/dist/core/secrets/store.js +63 -0
- package/dist/core/utils.js +233 -0
- package/dist/core/version.js +15 -0
- package/dist/dashboard/platform.js +219 -0
- package/dist/dashboard/routes/agents.js +450 -0
- package/dist/dashboard/routes/cli.js +174 -0
- package/dist/dashboard/routes/config.js +523 -0
- package/dist/dashboard/routes/extensions.js +745 -0
- package/dist/dashboard/routes/local-models.js +290 -0
- package/dist/dashboard/routes/models.js +324 -0
- package/dist/dashboard/routes/sessions.js +838 -0
- package/dist/dashboard/runtime.js +410 -0
- package/dist/dashboard/server.js +237 -0
- package/dist/dashboard/session-control.js +347 -0
- package/dist/model/catalog.js +104 -0
- package/dist/model/index.js +20 -0
- package/dist/model/injector.js +272 -0
- package/dist/model/provider-models.js +112 -0
- package/dist/model/store.js +212 -0
- package/dist/model/types.js +13 -0
- package/dist/model/validation.js +203 -0
- package/package.json +82 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup/doctor state assessment and messaging.
|
|
3
|
+
*/
|
|
4
|
+
import { getAgentInstallCommand, getAgentLabel } from '../agent/npm.js';
|
|
5
|
+
function enrichAgent(agent) {
|
|
6
|
+
const label = getAgentLabel(agent.agent);
|
|
7
|
+
const installCommand = getAgentInstallCommand(agent.agent) || 'npm install -g <agent-package>';
|
|
8
|
+
return {
|
|
9
|
+
...agent,
|
|
10
|
+
label,
|
|
11
|
+
installCommand,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function defaultChannelState(channel, tokenProvided) {
|
|
15
|
+
if (channel === 'telegram') {
|
|
16
|
+
return {
|
|
17
|
+
channel: 'telegram',
|
|
18
|
+
configured: tokenProvided,
|
|
19
|
+
ready: tokenProvided,
|
|
20
|
+
validated: false,
|
|
21
|
+
status: tokenProvided ? 'ready' : 'missing',
|
|
22
|
+
detail: tokenProvided ? 'Telegram credentials are configured.' : 'Telegram is not configured.',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (channel === 'feishu') {
|
|
26
|
+
return {
|
|
27
|
+
channel: 'feishu',
|
|
28
|
+
configured: tokenProvided,
|
|
29
|
+
ready: tokenProvided,
|
|
30
|
+
validated: false,
|
|
31
|
+
status: tokenProvided ? 'ready' : 'missing',
|
|
32
|
+
detail: tokenProvided ? 'Feishu credentials are configured.' : 'Feishu is not configured.',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (channel === 'weixin') {
|
|
36
|
+
return {
|
|
37
|
+
channel: 'weixin',
|
|
38
|
+
configured: tokenProvided,
|
|
39
|
+
ready: tokenProvided,
|
|
40
|
+
validated: false,
|
|
41
|
+
status: tokenProvided ? 'ready' : 'missing',
|
|
42
|
+
detail: tokenProvided ? 'Weixin credentials are configured.' : 'Weixin is not configured.',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (channel === 'slack') {
|
|
46
|
+
return {
|
|
47
|
+
channel: 'slack',
|
|
48
|
+
configured: tokenProvided,
|
|
49
|
+
ready: tokenProvided,
|
|
50
|
+
validated: false,
|
|
51
|
+
status: tokenProvided ? 'ready' : 'missing',
|
|
52
|
+
detail: tokenProvided ? 'Slack credentials are configured.' : 'Slack is not configured.',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (channel === 'discord') {
|
|
56
|
+
return {
|
|
57
|
+
channel: 'discord',
|
|
58
|
+
configured: tokenProvided,
|
|
59
|
+
ready: tokenProvided,
|
|
60
|
+
validated: false,
|
|
61
|
+
status: tokenProvided ? 'ready' : 'missing',
|
|
62
|
+
detail: tokenProvided ? 'Discord credentials are configured.' : 'Discord is not configured.',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (channel === 'dingtalk') {
|
|
66
|
+
return {
|
|
67
|
+
channel: 'dingtalk',
|
|
68
|
+
configured: tokenProvided,
|
|
69
|
+
ready: tokenProvided,
|
|
70
|
+
validated: false,
|
|
71
|
+
status: tokenProvided ? 'ready' : 'missing',
|
|
72
|
+
detail: tokenProvided ? 'DingTalk credentials are configured.' : 'DingTalk is not configured.',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (channel === 'wecom') {
|
|
76
|
+
return {
|
|
77
|
+
channel: 'wecom',
|
|
78
|
+
configured: tokenProvided,
|
|
79
|
+
ready: tokenProvided,
|
|
80
|
+
validated: false,
|
|
81
|
+
status: tokenProvided ? 'ready' : 'missing',
|
|
82
|
+
detail: tokenProvided ? 'WeChat Work credentials are configured.' : 'WeChat Work is not configured.',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
channel: 'telegram',
|
|
87
|
+
configured: tokenProvided,
|
|
88
|
+
ready: tokenProvided,
|
|
89
|
+
validated: false,
|
|
90
|
+
status: tokenProvided ? 'ready' : 'missing',
|
|
91
|
+
detail: tokenProvided ? 'Telegram credentials are configured.' : 'Telegram is not configured.',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export function collectSetupState(args) {
|
|
95
|
+
return {
|
|
96
|
+
channel: args.channel,
|
|
97
|
+
tokenProvided: args.tokenProvided,
|
|
98
|
+
agents: args.agents.map(enrichAgent),
|
|
99
|
+
channels: args.channels?.length ? args.channels : [defaultChannelState(args.channel, args.tokenProvided)],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function agentSummary(state) {
|
|
103
|
+
if (!state.installed) {
|
|
104
|
+
return [
|
|
105
|
+
`MISSING ${state.label} is not installed.`,
|
|
106
|
+
` Install with: ${state.installCommand}`,
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
const version = state.version ? ` (${state.version})` : '';
|
|
110
|
+
return [
|
|
111
|
+
`OK ${state.label} found at ${state.path || '(unknown path)'}${version}`,
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
export function hasReadyAgent(state) {
|
|
115
|
+
return state.agents.some(agent => agent.installed);
|
|
116
|
+
}
|
|
117
|
+
export function hasInstalledAgent(state) {
|
|
118
|
+
return state.agents.some(agent => agent.installed);
|
|
119
|
+
}
|
|
120
|
+
export function isSetupReady(state) {
|
|
121
|
+
// The Web Dashboard is always an available terminal, so an installed agent is
|
|
122
|
+
// the only hard prerequisite. IM channels are optional add-ons — their
|
|
123
|
+
// readiness is surfaced separately (per-channel status), not gated here.
|
|
124
|
+
return hasReadyAgent(state);
|
|
125
|
+
}
|
|
126
|
+
export function buildSetupGuide(state, version, options) {
|
|
127
|
+
const doctor = !!options?.doctor;
|
|
128
|
+
const isTelegram = state.channel === 'telegram';
|
|
129
|
+
const channelLabel = isTelegram
|
|
130
|
+
? 'Telegram'
|
|
131
|
+
: state.channel === 'feishu'
|
|
132
|
+
? 'Feishu'
|
|
133
|
+
: state.channel === 'weixin'
|
|
134
|
+
? 'Weixin'
|
|
135
|
+
: state.channel === 'slack'
|
|
136
|
+
? 'Slack'
|
|
137
|
+
: state.channel === 'discord'
|
|
138
|
+
? 'Discord'
|
|
139
|
+
: state.channel === 'dingtalk'
|
|
140
|
+
? 'DingTalk'
|
|
141
|
+
: state.channel === 'wecom'
|
|
142
|
+
? 'WeChat Work'
|
|
143
|
+
: 'your chat app';
|
|
144
|
+
const lines = [
|
|
145
|
+
`pikiloop v${version}`,
|
|
146
|
+
'',
|
|
147
|
+
doctor ? 'Setup check' : 'First-time setup',
|
|
148
|
+
'',
|
|
149
|
+
`pikiloop connects ${channelLabel} to a local coding agent running on your machine.`,
|
|
150
|
+
'Before the bot can start, make sure these basics are ready:',
|
|
151
|
+
'1. Claude Code, Codex, or Gemini CLI installed locally',
|
|
152
|
+
isTelegram
|
|
153
|
+
? '2. A Telegram bot token from @BotFather'
|
|
154
|
+
: '2. A supported channel token',
|
|
155
|
+
'',
|
|
156
|
+
'Step 1/2 Check your local coding agent',
|
|
157
|
+
];
|
|
158
|
+
for (const agent of state.agents)
|
|
159
|
+
lines.push(...agentSummary(agent));
|
|
160
|
+
lines.push('', isTelegram ? 'Step 2/2 Get a Telegram bot token' : 'Step 2/2 Check channel access');
|
|
161
|
+
if (isTelegram && state.tokenProvided) {
|
|
162
|
+
lines.push('OK A Telegram token was provided.');
|
|
163
|
+
}
|
|
164
|
+
else if (isTelegram) {
|
|
165
|
+
lines.push('MISSING No Telegram token configured in ~/.pikiloop/setting.json', ' Run `pikiloop` to open the dashboard and configure, or:', ' 1. Open Telegram and search for @BotFather', ' 2. Send /newbot and copy the token', ' 3. Add to ~/.pikiloop/setting.json: { "telegramBotToken": "..." }');
|
|
166
|
+
}
|
|
167
|
+
else if (state.channel === 'feishu' && state.tokenProvided) {
|
|
168
|
+
lines.push('OK Feishu credentials provided (FEISHU_APP_ID + FEISHU_APP_SECRET).');
|
|
169
|
+
}
|
|
170
|
+
else if (state.channel === 'feishu') {
|
|
171
|
+
lines.push('MISSING No Feishu credentials configured in ~/.pikiloop/setting.json', ' Run `pikiloop` to open the dashboard and configure, or add feishuAppId/feishuAppSecret to setting.json.');
|
|
172
|
+
}
|
|
173
|
+
else if (state.channel === 'weixin' && state.tokenProvided) {
|
|
174
|
+
lines.push('OK Weixin credentials provided (weixinBaseUrl + weixinBotToken + weixinAccountId).');
|
|
175
|
+
}
|
|
176
|
+
else if (state.channel === 'weixin') {
|
|
177
|
+
lines.push('MISSING No Weixin credentials configured in ~/.pikiloop/setting.json', ' Run `pikiloop` to open the dashboard, scan the QR code, and validate the channel before enabling it.');
|
|
178
|
+
}
|
|
179
|
+
else if (state.channel === 'slack' && state.tokenProvided) {
|
|
180
|
+
lines.push('OK Slack credentials provided (slackBotToken + slackAppToken).');
|
|
181
|
+
}
|
|
182
|
+
else if (state.channel === 'slack') {
|
|
183
|
+
lines.push('MISSING No Slack credentials configured in ~/.pikiloop/setting.json', ' Add slackBotToken (xoxb-…) and slackAppToken (xapp-…) from your Slack App Dashboard.');
|
|
184
|
+
}
|
|
185
|
+
else if (state.channel === 'discord' && state.tokenProvided) {
|
|
186
|
+
lines.push('OK Discord bot token provided (discordBotToken).');
|
|
187
|
+
}
|
|
188
|
+
else if (state.channel === 'discord') {
|
|
189
|
+
lines.push('MISSING No Discord credentials configured in ~/.pikiloop/setting.json', ' Add discordBotToken from the Discord Developer Portal (Bot page) — and enable Message Content Intent.');
|
|
190
|
+
}
|
|
191
|
+
else if (state.channel === 'dingtalk' && state.tokenProvided) {
|
|
192
|
+
lines.push('OK DingTalk credentials provided (dingtalkClientId + dingtalkClientSecret).');
|
|
193
|
+
}
|
|
194
|
+
else if (state.channel === 'dingtalk') {
|
|
195
|
+
lines.push('MISSING No DingTalk credentials configured in ~/.pikiloop/setting.json', ' Add dingtalkClientId (AppKey) and dingtalkClientSecret (AppSecret) from the DingTalk developer console.');
|
|
196
|
+
}
|
|
197
|
+
else if (state.channel === 'wecom' && state.tokenProvided) {
|
|
198
|
+
lines.push('OK WeChat Work credentials provided (wecomBotId + wecomBotSecret).');
|
|
199
|
+
}
|
|
200
|
+
else if (state.channel === 'wecom') {
|
|
201
|
+
lines.push('MISSING No WeChat Work credentials configured in ~/.pikiloop/setting.json', ' Create a 智能机器人 in 企业微信, then add wecomBotId and wecomBotSecret to setting.json.');
|
|
202
|
+
}
|
|
203
|
+
else if (state.tokenProvided) {
|
|
204
|
+
lines.push('OK A channel token was provided.');
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
lines.push('MISSING No supported channel token was provided.');
|
|
208
|
+
}
|
|
209
|
+
lines.push('');
|
|
210
|
+
if (state.tokenProvided) {
|
|
211
|
+
lines.push('Start command:');
|
|
212
|
+
lines.push(' npx pikiloop@latest');
|
|
213
|
+
}
|
|
214
|
+
else if (!isTelegram) {
|
|
215
|
+
lines.push('Start command:');
|
|
216
|
+
lines.push(' npx pikiloop@latest --channel telegram -t <YOUR_BOT_TOKEN>');
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
lines.push('Start command after you have the token:');
|
|
220
|
+
lines.push(' npx pikiloop@latest -t <YOUR_BOT_TOKEN>');
|
|
221
|
+
}
|
|
222
|
+
lines.push('', 'Tips:', ' - Run `npx pikiloop@latest --doctor` any time to re-check your setup.', ' - Run `npx pikiloop@latest --help` for the full CLI reference.');
|
|
223
|
+
if (!doctor && !hasInstalledAgent(state)) {
|
|
224
|
+
lines.push('', 'You only need one local coding agent. Install Claude Code or Codex, then come back.');
|
|
225
|
+
}
|
|
226
|
+
return `${lines.join('\n')}\n`;
|
|
227
|
+
}
|
package/dist/cli/run.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* run.ts — Standalone CLI commands for pikiloop.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npm run command -- status
|
|
7
|
+
* npm run command -- claude-models
|
|
8
|
+
* npm run command -- codex-models
|
|
9
|
+
*/
|
|
10
|
+
import { ensureGitignore, formatThinkingForDisplay, DEFAULT_RUN_TIMEOUT_S } from '../bot/bot.js';
|
|
11
|
+
import { initializeProjectSkills, listAgents, listModels, listSkills, getUsage, doStream } from '../agent/index.js';
|
|
12
|
+
import { querySessions, querySessionTail } from '../bot/session-hub.js';
|
|
13
|
+
import { getDriver } from '../agent/driver.js';
|
|
14
|
+
import { loadUserConfig, resolveUserWorkdir } from '../core/config/user-config.js';
|
|
15
|
+
function parseArgs(argv) {
|
|
16
|
+
const args = {
|
|
17
|
+
command: null, model: null, workdir: null, prompt: null, timeout: DEFAULT_RUN_TIMEOUT_S, help: false,
|
|
18
|
+
session: null, n: 4,
|
|
19
|
+
};
|
|
20
|
+
const positional = [];
|
|
21
|
+
const it = argv[Symbol.iterator]();
|
|
22
|
+
for (const arg of it) {
|
|
23
|
+
switch (arg) {
|
|
24
|
+
case '-m':
|
|
25
|
+
case '--model':
|
|
26
|
+
args.model = it.next().value;
|
|
27
|
+
break;
|
|
28
|
+
case '-w':
|
|
29
|
+
case '--workdir':
|
|
30
|
+
args.workdir = it.next().value;
|
|
31
|
+
break;
|
|
32
|
+
case '-p':
|
|
33
|
+
case '--prompt':
|
|
34
|
+
args.prompt = it.next().value;
|
|
35
|
+
break;
|
|
36
|
+
case '-s':
|
|
37
|
+
case '--session':
|
|
38
|
+
args.session = it.next().value;
|
|
39
|
+
break;
|
|
40
|
+
case '-n':
|
|
41
|
+
args.n = parseInt(it.next().value ?? '', 10) || 4;
|
|
42
|
+
break;
|
|
43
|
+
case '--timeout':
|
|
44
|
+
args.timeout = parseInt(it.next().value ?? '', 10) || DEFAULT_RUN_TIMEOUT_S;
|
|
45
|
+
break;
|
|
46
|
+
case '-h':
|
|
47
|
+
case '--help':
|
|
48
|
+
args.help = true;
|
|
49
|
+
break;
|
|
50
|
+
default:
|
|
51
|
+
if (arg.startsWith('-')) {
|
|
52
|
+
process.stderr.write(`Unknown option: ${arg}\n`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
else
|
|
56
|
+
positional.push(arg);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
args.command = positional[0] ?? null;
|
|
60
|
+
// If no -p flag, treat remaining positional args as the prompt
|
|
61
|
+
if (!args.prompt && positional.length > 1)
|
|
62
|
+
args.prompt = positional.slice(1).join(' ');
|
|
63
|
+
return args;
|
|
64
|
+
}
|
|
65
|
+
const HELP = `pikiloop run — standalone commands
|
|
66
|
+
|
|
67
|
+
Usage:
|
|
68
|
+
npm run command -- <command> [options]
|
|
69
|
+
|
|
70
|
+
Commands:
|
|
71
|
+
skills List project-defined custom skills (.pikiloop/skills)
|
|
72
|
+
claude-run Run a single Claude prompt and print the result
|
|
73
|
+
codex-run Run a single Codex prompt and print the result
|
|
74
|
+
claude-status Show Claude agent info and API usage
|
|
75
|
+
codex-status Show Codex agent info and API usage
|
|
76
|
+
gemini-status Show Gemini agent info and API usage
|
|
77
|
+
claude-models List available Claude models
|
|
78
|
+
codex-models List available Codex models
|
|
79
|
+
claude-sessions List recent Claude sessions for the workdir
|
|
80
|
+
codex-sessions List recent Codex sessions for the workdir
|
|
81
|
+
gemini-sessions List recent Gemini sessions for the workdir
|
|
82
|
+
claude-tail Show last N messages of a Claude session
|
|
83
|
+
codex-tail Show last N messages of a Codex session
|
|
84
|
+
gemini-tail Show last N messages of a Gemini session
|
|
85
|
+
|
|
86
|
+
Options:
|
|
87
|
+
-p, --prompt <text> Prompt text (or pass after command as positional args)
|
|
88
|
+
-m, --model <model> Model to use / highlight
|
|
89
|
+
-w, --workdir <dir> Working directory [default: current process cwd]
|
|
90
|
+
-s, --session <id> Session ID (for tail; omit to use latest session)
|
|
91
|
+
-n <count> Number of messages to show [default: 4]
|
|
92
|
+
--timeout <seconds> Max seconds per request [default: ${DEFAULT_RUN_TIMEOUT_S}]
|
|
93
|
+
-h, --help Print this help
|
|
94
|
+
|
|
95
|
+
Examples:
|
|
96
|
+
npm run command -- claude-run -p "Hello world"
|
|
97
|
+
npm run command -- codex-run -m o3 "Explain this repo"
|
|
98
|
+
npm run command -- claude-run -m sonnet --timeout 60 -p "What is 1+1?"
|
|
99
|
+
npm run command -- claude-tail
|
|
100
|
+
npm run command -- claude-tail -n 10 -s <session-id>
|
|
101
|
+
`;
|
|
102
|
+
async function main() {
|
|
103
|
+
const args = parseArgs(process.argv.slice(2));
|
|
104
|
+
const userConfig = loadUserConfig();
|
|
105
|
+
const workdir = resolveUserWorkdir({ workdir: args.workdir, config: userConfig });
|
|
106
|
+
ensureGitignore(workdir);
|
|
107
|
+
initializeProjectSkills(workdir);
|
|
108
|
+
if (args.help || !args.command) {
|
|
109
|
+
process.stdout.write(HELP);
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
switch (args.command) {
|
|
113
|
+
case 'skills': {
|
|
114
|
+
const result = listSkills(workdir);
|
|
115
|
+
if (!result.skills.length) {
|
|
116
|
+
process.stdout.write(`No custom skills found in ${workdir} (.pikiloop/skills)\n`);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
process.stdout.write(`Project skills (${result.skills.length}):\n\n`);
|
|
120
|
+
for (const sk of result.skills) {
|
|
121
|
+
const desc = sk.description ? ` ${sk.description}` : '';
|
|
122
|
+
process.stdout.write(` ${sk.name}${desc}\n`);
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case 'claude-status': {
|
|
127
|
+
const info = listAgents({ includeVersion: true }).agents.find(a => a.agent === 'claude');
|
|
128
|
+
const mark = info.installed ? '\u2713' : '\u2717';
|
|
129
|
+
process.stdout.write(`${mark} claude ${info.version ?? 'not installed'} ${info.path ?? ''}\n`);
|
|
130
|
+
const usage = getUsage({ agent: 'claude' });
|
|
131
|
+
if (usage.error) {
|
|
132
|
+
process.stdout.write(` ${usage.error}\n`);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
for (const w of usage.windows) {
|
|
136
|
+
process.stdout.write(` [${w.label}] ${w.usedPercent ?? '?'}% used status=${w.status ?? 'n/a'}\n`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case 'codex-status': {
|
|
142
|
+
const info = listAgents({ includeVersion: true }).agents.find(a => a.agent === 'codex');
|
|
143
|
+
const mark = info.installed ? '\u2713' : '\u2717';
|
|
144
|
+
process.stdout.write(`${mark} codex ${info.version ?? 'not installed'} ${info.path ?? ''}\n`);
|
|
145
|
+
const usage = getUsage({ agent: 'codex' });
|
|
146
|
+
if (usage.error) {
|
|
147
|
+
process.stdout.write(` ${usage.error}\n`);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
for (const w of usage.windows) {
|
|
151
|
+
process.stdout.write(` [${w.label}] ${w.usedPercent ?? '?'}% used status=${w.status ?? 'n/a'}\n`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case 'gemini-status': {
|
|
157
|
+
const info = listAgents({ includeVersion: true }).agents.find(a => a.agent === 'gemini');
|
|
158
|
+
const mark = info.installed ? '\u2713' : '\u2717';
|
|
159
|
+
process.stdout.write(`${mark} gemini ${info.version ?? 'not installed'} ${info.path ?? ''}\n`);
|
|
160
|
+
const driver = getDriver('gemini');
|
|
161
|
+
const usage = driver.getUsageLive
|
|
162
|
+
? await driver.getUsageLive({ agent: 'gemini', model: args.model })
|
|
163
|
+
: getUsage({ agent: 'gemini', model: args.model });
|
|
164
|
+
if (usage.error) {
|
|
165
|
+
process.stdout.write(` ${usage.error}\n`);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
for (const w of usage.windows) {
|
|
169
|
+
process.stdout.write(` [${w.label}] ${w.usedPercent ?? '?'}% used status=${w.status ?? 'n/a'}\n`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case 'claude-models': {
|
|
175
|
+
const result = await listModels('claude', { workdir, currentModel: args.model });
|
|
176
|
+
process.stdout.write(`Claude models${result.note ? ` (${result.note})` : ''}:\n`);
|
|
177
|
+
for (const m of result.models) {
|
|
178
|
+
process.stdout.write(` ${m.id}${m.alias ? ` (${m.alias})` : ''}\n`);
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case 'codex-models': {
|
|
183
|
+
const result = await listModels('codex', { workdir, currentModel: args.model });
|
|
184
|
+
process.stdout.write(`Codex models${result.note ? ` (${result.note})` : ''}:\n`);
|
|
185
|
+
for (const m of result.models) {
|
|
186
|
+
process.stdout.write(` ${m.id}${m.alias ? ` (${m.alias})` : ''}\n`);
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case 'claude-sessions':
|
|
191
|
+
case 'codex-sessions':
|
|
192
|
+
case 'gemini-sessions': {
|
|
193
|
+
const agent = args.command === 'codex-sessions'
|
|
194
|
+
? 'codex'
|
|
195
|
+
: args.command === 'gemini-sessions'
|
|
196
|
+
? 'gemini'
|
|
197
|
+
: 'claude';
|
|
198
|
+
const limit = 20;
|
|
199
|
+
const result = await querySessions({ agent, workdir, limit });
|
|
200
|
+
if (!result.ok) {
|
|
201
|
+
process.stderr.write(`Error: ${result.errors.join('; ')}\n`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
if (!result.sessions.length) {
|
|
205
|
+
process.stdout.write(`No ${agent} sessions found for ${workdir}\n`);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
process.stdout.write(`${agent} sessions (${result.sessions.length}) for ${workdir}:\n\n`);
|
|
209
|
+
for (const s of result.sessions) {
|
|
210
|
+
const run = s.running ? ' [RUNNING]' : '';
|
|
211
|
+
const date = s.createdAt ? s.createdAt.replace('T', ' ').slice(0, 19) : '?';
|
|
212
|
+
const model = s.model ? ` model=${s.model}` : '';
|
|
213
|
+
const title = s.title ? ` ${s.title}` : '';
|
|
214
|
+
const displayId = s.sessionId || '(none)';
|
|
215
|
+
process.stdout.write(` ${displayId} ${date}${model}${run}\n`);
|
|
216
|
+
if (title)
|
|
217
|
+
process.stdout.write(` ${title}\n`);
|
|
218
|
+
}
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}
|
|
221
|
+
case 'claude-tail':
|
|
222
|
+
case 'codex-tail':
|
|
223
|
+
case 'gemini-tail': {
|
|
224
|
+
const agent = args.command === 'codex-tail'
|
|
225
|
+
? 'codex'
|
|
226
|
+
: args.command === 'gemini-tail'
|
|
227
|
+
? 'gemini'
|
|
228
|
+
: 'claude';
|
|
229
|
+
let sessionId = args.session;
|
|
230
|
+
// Default: find the latest session
|
|
231
|
+
if (!sessionId) {
|
|
232
|
+
const sessions = await querySessions({ agent, workdir, limit: 1 });
|
|
233
|
+
if (!sessions.ok || !sessions.sessions.length) {
|
|
234
|
+
process.stderr.write(`No ${agent} sessions found for ${workdir}\n`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
sessionId = sessions.sessions[0].sessionId;
|
|
238
|
+
if (!sessionId) {
|
|
239
|
+
process.stderr.write(`Latest ${agent} session has no usable session ID\n`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const tail = await querySessionTail({ agent, sessionId, workdir, limit: args.n });
|
|
244
|
+
if (!tail.ok) {
|
|
245
|
+
process.stderr.write(`Error: ${tail.error}\n`);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
if (!tail.messages.length) {
|
|
249
|
+
process.stdout.write(`No messages found in session ${sessionId}\n`);
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
process.stdout.write(`${agent} session ${sessionId.slice(0, 16)} (last ${tail.messages.length} messages)\n\n`);
|
|
253
|
+
for (const m of tail.messages) {
|
|
254
|
+
const icon = m.role === 'user' ? '👤 User' : '🤖 Assistant';
|
|
255
|
+
const preview = m.text.length > 300 ? m.text.slice(0, 300) + '...' : m.text;
|
|
256
|
+
process.stdout.write(`${icon}:\n${preview}\n\n`);
|
|
257
|
+
}
|
|
258
|
+
process.exit(0);
|
|
259
|
+
}
|
|
260
|
+
case 'claude-run':
|
|
261
|
+
case 'codex-run': {
|
|
262
|
+
const agent = args.command === 'codex-run' ? 'codex' : 'claude';
|
|
263
|
+
const prompt = args.prompt;
|
|
264
|
+
if (!prompt) {
|
|
265
|
+
process.stderr.write(`Missing prompt. Use -p "..." or pass text after the command.\n`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
const opts = {
|
|
269
|
+
agent, prompt, workdir, timeout: args.timeout,
|
|
270
|
+
sessionId: null, model: null, thinkingEffort: 'max',
|
|
271
|
+
onText: (text, _thinking) => {
|
|
272
|
+
process.stdout.write(`\r\x1b[K${text.slice(-120)}`);
|
|
273
|
+
},
|
|
274
|
+
claudeModel: agent === 'claude' ? (args.model || undefined) : undefined,
|
|
275
|
+
claudePermissionMode: agent === 'claude' ? 'bypassPermissions' : undefined,
|
|
276
|
+
codexModel: agent === 'codex' ? (args.model || undefined) : undefined,
|
|
277
|
+
codexFullAccess: agent === 'codex' ? true : undefined,
|
|
278
|
+
};
|
|
279
|
+
process.stdout.write(`Running ${agent}${args.model ? ` (model: ${args.model})` : ''}...\n`);
|
|
280
|
+
const result = await doStream(opts);
|
|
281
|
+
// Clear the streaming line and print final result
|
|
282
|
+
process.stdout.write('\r\x1b[K');
|
|
283
|
+
process.stdout.write(`--- ${agent} result ---\n`);
|
|
284
|
+
process.stdout.write(`ok: ${result.ok}\n`);
|
|
285
|
+
process.stdout.write(`model: ${result.model ?? '(unknown)'}\n`);
|
|
286
|
+
process.stdout.write(`session: ${result.sessionId ?? '(none)'}\n`);
|
|
287
|
+
process.stdout.write(`elapsed: ${result.elapsedS.toFixed(1)}s\n`);
|
|
288
|
+
process.stdout.write(`tokens: in=${result.inputTokens ?? '?'} out=${result.outputTokens ?? '?'} cached=${result.cachedInputTokens ?? '?'} cacheCreate=${result.cacheCreationInputTokens ?? '?'}\n`);
|
|
289
|
+
if (result.contextPercent != null) {
|
|
290
|
+
process.stdout.write(`context: ${result.contextUsedTokens}/${result.contextWindow} (${result.contextPercent}%)\n`);
|
|
291
|
+
}
|
|
292
|
+
process.stdout.write(`stop: ${result.stopReason ?? 'n/a'}\n`);
|
|
293
|
+
if (result.error)
|
|
294
|
+
process.stdout.write(`error: ${result.error}\n`);
|
|
295
|
+
process.stdout.write(`---\n`);
|
|
296
|
+
if (result.thinking) {
|
|
297
|
+
process.stdout.write(`\n<thinking>\n${formatThinkingForDisplay(result.thinking, 800)}\n</thinking>\n`);
|
|
298
|
+
}
|
|
299
|
+
process.stdout.write(`\n${result.message}\n`);
|
|
300
|
+
process.exit(result.ok ? 0 : 1);
|
|
301
|
+
}
|
|
302
|
+
default:
|
|
303
|
+
process.stderr.write(`Unknown command: ${args.command}\n`);
|
|
304
|
+
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`);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
main().catch(err => { console.error(err); process.exit(1); });
|