cubelife 0.2.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 +81 -0
- package/SPRITE-LICENSE +14 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +39 -0
- package/dist/commands/agents.d.ts +2 -0
- package/dist/commands/agents.js +303 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +233 -0
- package/dist/commands/billing.d.ts +2 -0
- package/dist/commands/billing.js +362 -0
- package/dist/commands/creature.d.ts +2 -0
- package/dist/commands/creature.js +166 -0
- package/dist/commands/default.d.ts +2 -0
- package/dist/commands/default.js +87 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +48 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +200 -0
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +9 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +122 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +453 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +89 -0
- package/dist/commands/tutorial.d.ts +2 -0
- package/dist/commands/tutorial.js +9 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +262 -0
- package/dist/data/sprite-data.d.ts +32 -0
- package/dist/data/sprite-data.js +865 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/lib/api.d.ts +162 -0
- package/dist/lib/api.js +160 -0
- package/dist/lib/auth.d.ts +12 -0
- package/dist/lib/auth.js +113 -0
- package/dist/lib/browser.d.ts +1 -0
- package/dist/lib/browser.js +21 -0
- package/dist/lib/command-helpers.d.ts +26 -0
- package/dist/lib/command-helpers.js +60 -0
- package/dist/lib/compositor.d.ts +34 -0
- package/dist/lib/compositor.js +232 -0
- package/dist/lib/config.d.ts +39 -0
- package/dist/lib/config.js +89 -0
- package/dist/lib/constants.d.ts +12 -0
- package/dist/lib/constants.js +39 -0
- package/dist/lib/detect.d.ts +17 -0
- package/dist/lib/detect.js +99 -0
- package/dist/lib/doctor.d.ts +18 -0
- package/dist/lib/doctor.js +321 -0
- package/dist/lib/index.d.ts +11 -0
- package/dist/lib/index.js +6 -0
- package/dist/lib/integration.d.ts +66 -0
- package/dist/lib/integration.js +337 -0
- package/dist/lib/poll.d.ts +11 -0
- package/dist/lib/poll.js +31 -0
- package/dist/lib/resolve.d.ts +1 -0
- package/dist/lib/resolve.js +10 -0
- package/dist/lib/services/account-service.d.ts +17 -0
- package/dist/lib/services/account-service.js +30 -0
- package/dist/lib/services/agent-service.d.ts +17 -0
- package/dist/lib/services/agent-service.js +62 -0
- package/dist/lib/services/creature-service.d.ts +12 -0
- package/dist/lib/services/creature-service.js +35 -0
- package/dist/lib/services/project-service.d.ts +9 -0
- package/dist/lib/services/project-service.js +22 -0
- package/dist/lib/tutorial.d.ts +12 -0
- package/dist/lib/tutorial.js +358 -0
- package/dist/mcp/server.d.ts +8 -0
- package/dist/mcp/server.js +116 -0
- package/dist/ui/banner.d.ts +3 -0
- package/dist/ui/banner.js +27 -0
- package/dist/ui/half-block.d.ts +6 -0
- package/dist/ui/half-block.js +45 -0
- package/dist/ui/helpers.d.ts +3 -0
- package/dist/ui/helpers.js +11 -0
- package/dist/ui/index.d.ts +5 -0
- package/dist/ui/index.js +5 -0
- package/dist/ui/panel.d.ts +7 -0
- package/dist/ui/panel.js +21 -0
- package/dist/ui/preview.d.ts +7 -0
- package/dist/ui/preview.js +21 -0
- package/dist/ui/table.d.ts +8 -0
- package/dist/ui/table.js +20 -0
- package/dist/ui/theme.d.ts +24 -0
- package/dist/ui/theme.js +32 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { currentUser, login, register } from './auth.js';
|
|
4
|
+
import { createAdminClient, AgentClient } from './api.js';
|
|
5
|
+
import { readProjectConfig, readAgents, resolveAgentKey } from './config.js';
|
|
6
|
+
import { CREATURE_TYPES, CREATURE_DESCRIPTIONS, CREATURE_DEFAULT_COLORS, CREATURE_NAME_MAX, ID_DISPLAY_LENGTH, isValidHexColor, } from './constants.js';
|
|
7
|
+
import { detectInstalledTools } from './detect.js';
|
|
8
|
+
import { mergeSettings, writeHookScript, readClaudeSettings, writeClaudeSettings, hasMcpServer, resolveProjectRoot, isClaudeMcpRegistered, } from './integration.js';
|
|
9
|
+
import { brand } from '../ui/theme.js';
|
|
10
|
+
import { isCancel } from '../ui/helpers.js';
|
|
11
|
+
import { getErrorMessage } from './command-helpers.js';
|
|
12
|
+
import * as agentService from './services/agent-service.js';
|
|
13
|
+
export async function detectProgress() {
|
|
14
|
+
const user = await currentUser();
|
|
15
|
+
if (!user)
|
|
16
|
+
return 0;
|
|
17
|
+
const config = await readProjectConfig();
|
|
18
|
+
if (!config?.projectId)
|
|
19
|
+
return 1;
|
|
20
|
+
if (!config.agentId)
|
|
21
|
+
return 2;
|
|
22
|
+
const store = await readAgents();
|
|
23
|
+
if (!resolveAgentKey(config.agentId, store))
|
|
24
|
+
return 2;
|
|
25
|
+
const existing = await readClaudeSettings(resolveProjectRoot());
|
|
26
|
+
if (!isClaudeMcpRegistered() && !hasMcpServer(existing))
|
|
27
|
+
return 4;
|
|
28
|
+
return 6;
|
|
29
|
+
}
|
|
30
|
+
export async function stepAccount() {
|
|
31
|
+
const user = await currentUser();
|
|
32
|
+
if (user) {
|
|
33
|
+
p.log.success(`Logged in as ${brand.primary(user.email)}`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const hasAccount = await p.confirm({
|
|
37
|
+
message: 'Do you have a CubeLife account?',
|
|
38
|
+
});
|
|
39
|
+
if (isCancel(hasAccount)) {
|
|
40
|
+
p.cancel('Tutorial cancelled.');
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
const email = await p.text({
|
|
44
|
+
message: 'Email',
|
|
45
|
+
validate: (v) => { if (!v.includes('@'))
|
|
46
|
+
return 'Please enter a valid email.'; },
|
|
47
|
+
});
|
|
48
|
+
if (isCancel(email)) {
|
|
49
|
+
p.cancel('Tutorial cancelled.');
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
const password = await p.password({ message: 'Password' });
|
|
53
|
+
if (isCancel(password)) {
|
|
54
|
+
p.cancel('Tutorial cancelled.');
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
const spin = p.spinner();
|
|
58
|
+
if (hasAccount) {
|
|
59
|
+
spin.start('Logging in');
|
|
60
|
+
try {
|
|
61
|
+
const auth = await login(email, password);
|
|
62
|
+
spin.stop(`Logged in as ${brand.primary(auth.email)}`);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
spin.stop('Login failed');
|
|
66
|
+
p.log.error(getErrorMessage(err));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const confirmPw = await p.password({ message: 'Confirm password' });
|
|
72
|
+
if (isCancel(confirmPw)) {
|
|
73
|
+
p.cancel('Tutorial cancelled.');
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
if (password !== confirmPw) {
|
|
77
|
+
p.log.error('Passwords do not match.');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
spin.start('Creating account');
|
|
81
|
+
try {
|
|
82
|
+
const auth = await register(email, password);
|
|
83
|
+
spin.stop(`Account created for ${brand.primary(auth.email)}`);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
spin.stop('Registration failed');
|
|
87
|
+
p.log.error(getErrorMessage(err));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export async function stepProject() {
|
|
93
|
+
const config = await readProjectConfig();
|
|
94
|
+
if (config?.projectId) {
|
|
95
|
+
p.log.success(`Project linked: ${brand.primary(config.projectId.slice(0, ID_DISPLAY_LENGTH))}`);
|
|
96
|
+
return config.projectId;
|
|
97
|
+
}
|
|
98
|
+
const client = await createAdminClient();
|
|
99
|
+
const spin = p.spinner();
|
|
100
|
+
spin.start('Fetching projects');
|
|
101
|
+
const { projects } = await client.listProjects();
|
|
102
|
+
spin.stop(`${projects.length} project${projects.length === 1 ? '' : 's'} found`);
|
|
103
|
+
let projectId;
|
|
104
|
+
if (projects.length > 0) {
|
|
105
|
+
const CREATE = '__create__';
|
|
106
|
+
const selected = await p.select({
|
|
107
|
+
message: 'Select a project or create a new one',
|
|
108
|
+
options: [
|
|
109
|
+
...projects.map((proj) => ({
|
|
110
|
+
value: proj.id,
|
|
111
|
+
label: proj.name,
|
|
112
|
+
hint: proj.id.slice(0, ID_DISPLAY_LENGTH),
|
|
113
|
+
})),
|
|
114
|
+
{ value: CREATE, label: '+ Create new project' },
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
if (isCancel(selected)) {
|
|
118
|
+
p.cancel('Tutorial cancelled.');
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
if (selected !== CREATE) {
|
|
122
|
+
projectId = selected;
|
|
123
|
+
await agentService.linkAgent(projectId, '');
|
|
124
|
+
return projectId;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const dirName = basename(process.cwd());
|
|
128
|
+
const name = await p.text({
|
|
129
|
+
message: 'Project name',
|
|
130
|
+
initialValue: dirName,
|
|
131
|
+
validate: (v) => { if (!v.trim())
|
|
132
|
+
return 'Name cannot be empty.'; },
|
|
133
|
+
});
|
|
134
|
+
if (isCancel(name)) {
|
|
135
|
+
p.cancel('Tutorial cancelled.');
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
const createSpin = p.spinner();
|
|
139
|
+
createSpin.start('Creating project');
|
|
140
|
+
const result = await client.createProject(name);
|
|
141
|
+
createSpin.stop(`Project created: ${brand.primary(result.name)}`);
|
|
142
|
+
await agentService.linkAgent(result.id, '');
|
|
143
|
+
return result.id;
|
|
144
|
+
}
|
|
145
|
+
export async function stepAgent(projectId) {
|
|
146
|
+
const config = await readProjectConfig();
|
|
147
|
+
if (config?.agentId) {
|
|
148
|
+
const store = await readAgents();
|
|
149
|
+
const key = resolveAgentKey(config.agentId, store);
|
|
150
|
+
if (key) {
|
|
151
|
+
const client = await createAdminClient();
|
|
152
|
+
try {
|
|
153
|
+
const agent = await client.getAgent(projectId, config.agentId);
|
|
154
|
+
p.log.success(`Agent: ${brand.primary(agent.name)} (${agent.form})`);
|
|
155
|
+
return { agentId: config.agentId, form: agent.form, apiKey: key };
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Agent deleted, fall through to creation
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const dirName = basename(process.cwd());
|
|
163
|
+
const name = await p.text({
|
|
164
|
+
message: 'What should we call your agent?',
|
|
165
|
+
initialValue: dirName,
|
|
166
|
+
validate: (v) => { if (!v.trim())
|
|
167
|
+
return 'Name cannot be empty.'; },
|
|
168
|
+
});
|
|
169
|
+
if (isCancel(name)) {
|
|
170
|
+
p.cancel('Tutorial cancelled.');
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
const form = await p.select({
|
|
174
|
+
message: 'Choose a form',
|
|
175
|
+
options: [
|
|
176
|
+
{ value: 'creature', label: 'Creature Companion', hint: '8 unique creatures with personality' },
|
|
177
|
+
{ value: 'human', label: 'Human Character', hint: 'Customisable pixel art character' },
|
|
178
|
+
],
|
|
179
|
+
});
|
|
180
|
+
if (isCancel(form)) {
|
|
181
|
+
p.cancel('Tutorial cancelled.');
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}
|
|
184
|
+
let creature;
|
|
185
|
+
if (form === 'creature') {
|
|
186
|
+
const type = await p.select({
|
|
187
|
+
message: 'Choose a creature type',
|
|
188
|
+
options: CREATURE_TYPES.map((t) => ({
|
|
189
|
+
value: t,
|
|
190
|
+
label: t.charAt(0).toUpperCase() + t.slice(1),
|
|
191
|
+
hint: CREATURE_DESCRIPTIONS[t],
|
|
192
|
+
})),
|
|
193
|
+
});
|
|
194
|
+
if (isCancel(type)) {
|
|
195
|
+
p.cancel('Tutorial cancelled.');
|
|
196
|
+
process.exit(0);
|
|
197
|
+
}
|
|
198
|
+
creature = {
|
|
199
|
+
type: type,
|
|
200
|
+
color: CREATURE_DEFAULT_COLORS[type],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const client = await createAdminClient();
|
|
204
|
+
const spin = p.spinner();
|
|
205
|
+
spin.start('Creating agent');
|
|
206
|
+
const result = await agentService.createAgent(client, projectId, {
|
|
207
|
+
name: name,
|
|
208
|
+
form: form,
|
|
209
|
+
...(creature ? { creature } : {}),
|
|
210
|
+
});
|
|
211
|
+
spin.stop('Agent created');
|
|
212
|
+
await agentService.linkAgent(projectId, result.id);
|
|
213
|
+
p.log.success(`Agent ${brand.primary(result.name)} created`);
|
|
214
|
+
return { agentId: result.id, form: form, apiKey: result.apiKey };
|
|
215
|
+
}
|
|
216
|
+
export async function stepAppearance(projectId, agentId, form) {
|
|
217
|
+
if (form === 'human') {
|
|
218
|
+
p.log.info(`Customise your character at ${brand.accent('life.cubeworld.co.za/#dashboard')}`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const wantCustomise = await p.confirm({
|
|
222
|
+
message: 'Customise creature colour and name?',
|
|
223
|
+
initialValue: false,
|
|
224
|
+
});
|
|
225
|
+
if (isCancel(wantCustomise)) {
|
|
226
|
+
p.cancel('Tutorial cancelled.');
|
|
227
|
+
process.exit(0);
|
|
228
|
+
}
|
|
229
|
+
if (!wantCustomise)
|
|
230
|
+
return;
|
|
231
|
+
const client = await createAdminClient();
|
|
232
|
+
const agent = await agentService.getAgent(client, projectId, agentId);
|
|
233
|
+
const currentType = agent.creature?.type ?? 'gremlin';
|
|
234
|
+
const defaultColour = CREATURE_DEFAULT_COLORS[currentType];
|
|
235
|
+
const colourChoice = await p.select({
|
|
236
|
+
message: 'Creature colour',
|
|
237
|
+
options: [
|
|
238
|
+
{ value: defaultColour, label: `Default (${defaultColour})`, hint: 'Recommended' },
|
|
239
|
+
{ value: '#E8A0BF', label: 'Rose' },
|
|
240
|
+
{ value: '#7EAAC1', label: 'Sky Blue' },
|
|
241
|
+
{ value: '#5B8C5A', label: 'Forest Green' },
|
|
242
|
+
{ value: '#D4874E', label: 'Amber' },
|
|
243
|
+
{ value: '#9B7ED8', label: 'Lavender' },
|
|
244
|
+
{ value: '__custom__', label: 'Custom hex colour' },
|
|
245
|
+
],
|
|
246
|
+
});
|
|
247
|
+
if (isCancel(colourChoice)) {
|
|
248
|
+
p.cancel('Tutorial cancelled.');
|
|
249
|
+
process.exit(0);
|
|
250
|
+
}
|
|
251
|
+
let colour = colourChoice;
|
|
252
|
+
if (colour === '__custom__') {
|
|
253
|
+
const hex = await p.text({
|
|
254
|
+
message: 'Hex colour (e.g. #FF5500)',
|
|
255
|
+
validate: (v) => {
|
|
256
|
+
const n = v.startsWith('#') ? v : `#${v}`;
|
|
257
|
+
if (!isValidHexColor(n))
|
|
258
|
+
return 'Expected format: #RRGGBB';
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
if (isCancel(hex)) {
|
|
262
|
+
p.cancel('Tutorial cancelled.');
|
|
263
|
+
process.exit(0);
|
|
264
|
+
}
|
|
265
|
+
colour = hex.startsWith('#') ? hex : `#${hex}`;
|
|
266
|
+
}
|
|
267
|
+
const nameInput = await p.text({
|
|
268
|
+
message: 'Name your creature (leave empty to skip)',
|
|
269
|
+
initialValue: '',
|
|
270
|
+
validate: (v) => {
|
|
271
|
+
if (v.length > CREATURE_NAME_MAX)
|
|
272
|
+
return `Must be ${CREATURE_NAME_MAX} characters or fewer.`;
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
if (isCancel(nameInput)) {
|
|
276
|
+
p.cancel('Tutorial cancelled.');
|
|
277
|
+
process.exit(0);
|
|
278
|
+
}
|
|
279
|
+
const spin = p.spinner();
|
|
280
|
+
spin.start('Updating appearance');
|
|
281
|
+
await agentService.updateAgent(client, projectId, agentId, {
|
|
282
|
+
creature: {
|
|
283
|
+
type: currentType,
|
|
284
|
+
color: colour,
|
|
285
|
+
...(nameInput ? { name: nameInput } : {}),
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
spin.stop('Appearance updated');
|
|
289
|
+
}
|
|
290
|
+
export async function stepIntegration() {
|
|
291
|
+
const tools = detectInstalledTools();
|
|
292
|
+
if (tools.length === 0) {
|
|
293
|
+
p.log.info(`No AI tools detected. Run ${brand.accent('cubelife setup')} later to configure.`);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
p.log.info(`Detected: ${tools.map((t) => brand.primary(t.name)).join(', ')}`);
|
|
297
|
+
const claudeCode = tools.find((t) => t.id === 'claude-code');
|
|
298
|
+
if (claudeCode) {
|
|
299
|
+
try {
|
|
300
|
+
const root = resolveProjectRoot();
|
|
301
|
+
await writeHookScript();
|
|
302
|
+
const existing = await readClaudeSettings(root);
|
|
303
|
+
const { settings } = mergeSettings(existing, 'both');
|
|
304
|
+
await writeClaudeSettings(root, settings);
|
|
305
|
+
p.log.success('Claude Code: MCP + Hooks configured');
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
p.log.warn(`Claude Code setup failed: ${getErrorMessage(err)}`);
|
|
309
|
+
p.log.info(`Run ${brand.accent('cubelife setup claude-code')} to retry.`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const others = tools.filter((t) => t.id !== 'claude-code');
|
|
313
|
+
if (others.length > 0) {
|
|
314
|
+
p.log.info(`Run ${brand.accent('cubelife setup <tool>')} to configure: ${others.map((t) => t.name).join(', ')}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
export async function stepVerify(apiKey) {
|
|
318
|
+
const spin = p.spinner();
|
|
319
|
+
spin.start('Sending test state report');
|
|
320
|
+
const client = new AgentClient(apiKey);
|
|
321
|
+
try {
|
|
322
|
+
await client.report('complete', { detail: 'Tutorial finished!' });
|
|
323
|
+
spin.stop('State report delivered');
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
spin.stop('Delivery failed');
|
|
327
|
+
p.log.warn(`Could not deliver state: ${getErrorMessage(err)}`);
|
|
328
|
+
p.log.info('This might be a network issue. Your agent is configured correctly.');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
export async function runTutorial() {
|
|
332
|
+
p.intro(brand.primary('Welcome to CubeLife'));
|
|
333
|
+
const progress = await detectProgress();
|
|
334
|
+
if (progress >= 6) {
|
|
335
|
+
p.log.success('Already fully configured.');
|
|
336
|
+
p.outro(`Run ${brand.accent('cubelife view')} to see your agent in the terminal.`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (progress > 0) {
|
|
340
|
+
p.log.info(`Previous progress detected (completed through step ${progress}).`);
|
|
341
|
+
}
|
|
342
|
+
p.log.step('Step 1/6: Account');
|
|
343
|
+
await stepAccount();
|
|
344
|
+
p.log.step('Step 2/6: Project');
|
|
345
|
+
const projectId = await stepProject();
|
|
346
|
+
p.log.step('Step 3/6: Agent');
|
|
347
|
+
const { agentId, form, apiKey } = await stepAgent(projectId);
|
|
348
|
+
p.log.step('Step 4/6: Appearance');
|
|
349
|
+
await stepAppearance(projectId, agentId, form);
|
|
350
|
+
p.log.step('Step 5/6: Integration');
|
|
351
|
+
await stepIntegration();
|
|
352
|
+
p.log.step('Step 6/6: Verify');
|
|
353
|
+
await stepVerify(apiKey);
|
|
354
|
+
p.outro(`You're all set!\n` +
|
|
355
|
+
` ${brand.accent('cubelife view')} See your agent in the terminal\n` +
|
|
356
|
+
` ${brand.accent('cubelife status coding "My first task"')} Report a state\n` +
|
|
357
|
+
` ${brand.accent('cubelife billing')} Check your plan and usage`);
|
|
358
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { AgentClient } from '../lib/api.js';
|
|
3
|
+
export declare function resolveApiKey(): Promise<{
|
|
4
|
+
apiKey: string;
|
|
5
|
+
agentId: string;
|
|
6
|
+
} | null>;
|
|
7
|
+
export declare function createMcpServer(client: AgentClient): McpServer;
|
|
8
|
+
export declare function startMcpServer(): Promise<void>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { AgentClient, ApiError } from '../lib/api.js';
|
|
5
|
+
import { readProjectConfig, readAgents, resolveAgentKey } from '../lib/config.js';
|
|
6
|
+
import { VALID_STATES, VALID_SENTIMENTS } from '../lib/constants.js';
|
|
7
|
+
import { CLI_VERSION } from '../version.js';
|
|
8
|
+
import { getErrorMessage } from '../lib/command-helpers.js';
|
|
9
|
+
function toolError(action, err) {
|
|
10
|
+
if (err instanceof ApiError && err.status === 429) {
|
|
11
|
+
return { content: [{ type: 'text', text: 'Rate limited. State not delivered.' }] };
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
content: [{ type: 'text', text: `Failed to ${action}: ${getErrorMessage(err)}` }],
|
|
15
|
+
isError: true,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export async function resolveApiKey() {
|
|
19
|
+
const store = await readAgents();
|
|
20
|
+
const envAgentId = process.env['CUBELIFE_AGENT_ID'];
|
|
21
|
+
if (envAgentId) {
|
|
22
|
+
const key = resolveAgentKey(envAgentId, store);
|
|
23
|
+
if (key)
|
|
24
|
+
return { apiKey: key, agentId: envAgentId };
|
|
25
|
+
}
|
|
26
|
+
const config = await readProjectConfig();
|
|
27
|
+
if (config?.agentId) {
|
|
28
|
+
const key = resolveAgentKey(config.agentId, store);
|
|
29
|
+
if (key)
|
|
30
|
+
return { apiKey: key, agentId: config.agentId };
|
|
31
|
+
}
|
|
32
|
+
const envKey = process.env['CUBELIFE_API_KEY'];
|
|
33
|
+
if (envKey)
|
|
34
|
+
return { apiKey: envKey, agentId: 'env' };
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
export function createMcpServer(client) {
|
|
38
|
+
const server = new McpServer({
|
|
39
|
+
name: 'cubelife',
|
|
40
|
+
version: CLI_VERSION,
|
|
41
|
+
});
|
|
42
|
+
server.registerTool('cubelife_report', {
|
|
43
|
+
description: 'Report the AI agent\'s current work state to CubeLife. Call this when starting a new task or changing activity.',
|
|
44
|
+
inputSchema: {
|
|
45
|
+
state: z.enum(VALID_STATES).describe('The current work state'),
|
|
46
|
+
detail: z.string().optional().describe('Brief description of what the agent is doing'),
|
|
47
|
+
progress: z.number().min(0).max(1).optional().describe('Task progress from 0 to 1'),
|
|
48
|
+
sentiment: z.enum(VALID_SENTIMENTS).optional().describe('Current sentiment'),
|
|
49
|
+
},
|
|
50
|
+
}, async ({ state, detail, progress, sentiment }) => {
|
|
51
|
+
try {
|
|
52
|
+
await client.report(state, { detail, progress, sentiment });
|
|
53
|
+
return { content: [{ type: 'text', text: `State updated: ${state}` }] };
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
return toolError('report state', err);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
server.registerTool('cubelife_complete', {
|
|
60
|
+
description: 'Mark the current task as complete. Triggers a celebration animation on the character.',
|
|
61
|
+
inputSchema: {
|
|
62
|
+
detail: z.string().optional().describe('What was completed'),
|
|
63
|
+
},
|
|
64
|
+
}, async ({ detail }) => {
|
|
65
|
+
try {
|
|
66
|
+
await client.report('complete', { detail, sentiment: 'positive' });
|
|
67
|
+
return { content: [{ type: 'text', text: 'Task marked complete.' }] };
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
return toolError('mark complete', err);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
server.registerTool('cubelife_error', {
|
|
74
|
+
description: 'Report that an error occurred. Shows frustration on the character.',
|
|
75
|
+
inputSchema: {
|
|
76
|
+
detail: z.string().optional().describe('What went wrong'),
|
|
77
|
+
},
|
|
78
|
+
}, async ({ detail }) => {
|
|
79
|
+
try {
|
|
80
|
+
await client.report('error', { detail, sentiment: 'negative' });
|
|
81
|
+
return { content: [{ type: 'text', text: 'Error state reported.' }] };
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
return toolError('report error', err);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
server.registerTool('cubelife_status', {
|
|
88
|
+
description: 'Get the agent\'s current state.',
|
|
89
|
+
inputSchema: {},
|
|
90
|
+
}, async () => {
|
|
91
|
+
try {
|
|
92
|
+
const state = await client.getState();
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: 'text', text: JSON.stringify(state) }],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: 'text', text: `Failed to get status: ${getErrorMessage(err)}` }],
|
|
100
|
+
isError: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return server;
|
|
105
|
+
}
|
|
106
|
+
export async function startMcpServer() {
|
|
107
|
+
const resolved = await resolveApiKey();
|
|
108
|
+
if (!resolved) {
|
|
109
|
+
process.stderr.write('No CubeLife API key found. Run `cubelife init` or set CUBELIFE_API_KEY.\n');
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
const client = new AgentClient(resolved.apiKey);
|
|
113
|
+
const server = createMcpServer(client);
|
|
114
|
+
const transport = new StdioServerTransport();
|
|
115
|
+
await server.connect(transport);
|
|
116
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { brand } from './theme.js';
|
|
2
|
+
const LOGO_RAW = [
|
|
3
|
+
' ▄▄▄▄▄ ▄ ▄ ▄▄▄▄ ▄▄▄▄▄',
|
|
4
|
+
' █ █ █ █ █ █ ',
|
|
5
|
+
' █ █ █ █▄▄▄▀ █▄▄▄ ',
|
|
6
|
+
' █ █ █ █ █ █ ',
|
|
7
|
+
' ▀▀▀▀▀ ▀▀▀ ▀▀▀▀ ▀▀▀▀▀',
|
|
8
|
+
];
|
|
9
|
+
const LIFE_RAW = [
|
|
10
|
+
' █ ▀ ▄▄▄▄ ▄▄▄▄▄',
|
|
11
|
+
' █ █ █ █ ',
|
|
12
|
+
' █ █ █▄▄ █▄▄▄ ',
|
|
13
|
+
' █ █ █ █ ',
|
|
14
|
+
' ▀▀▀▀▀ ▀ ▀ ▀▀▀▀▀',
|
|
15
|
+
];
|
|
16
|
+
export function logo() {
|
|
17
|
+
const cube = LOGO_RAW.map((line) => brand.primary(line));
|
|
18
|
+
const life = LIFE_RAW.map((line) => brand.accent(line));
|
|
19
|
+
return cube.map((c, i) => c + life[i]).join('\n');
|
|
20
|
+
}
|
|
21
|
+
export function sectionHeader(title) {
|
|
22
|
+
const bar = brand.muted('─'.repeat(40));
|
|
23
|
+
return `\n${bar}\n ${brand.heading(brand.primary(title))}\n${bar}`;
|
|
24
|
+
}
|
|
25
|
+
export function version(v) {
|
|
26
|
+
return brand.muted(`v${v}`);
|
|
27
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const UPPER_HALF = '▀';
|
|
2
|
+
const LOWER_HALF = '▄';
|
|
3
|
+
const RESET = '\x1b[0m';
|
|
4
|
+
function fg(r, g, b) {
|
|
5
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
6
|
+
}
|
|
7
|
+
function bg(r, g, b) {
|
|
8
|
+
return `\x1b[48;2;${r};${g};${b}m`;
|
|
9
|
+
}
|
|
10
|
+
export function renderHalfBlock(rgba, width, height, options = {}) {
|
|
11
|
+
const { bgR = 0, bgG = 0, bgB = 0 } = options;
|
|
12
|
+
const rows = [];
|
|
13
|
+
for (let y = 0; y < height; y += 2) {
|
|
14
|
+
let row = '';
|
|
15
|
+
for (let x = 0; x < width; x++) {
|
|
16
|
+
const topIdx = (y * width + x) * 4;
|
|
17
|
+
const botIdx = ((y + 1) * width + x) * 4;
|
|
18
|
+
const topR = rgba[topIdx];
|
|
19
|
+
const topG = rgba[topIdx + 1];
|
|
20
|
+
const topB = rgba[topIdx + 2];
|
|
21
|
+
const topA = rgba[topIdx + 3];
|
|
22
|
+
const hasBot = y + 1 < height;
|
|
23
|
+
const botR = hasBot ? rgba[botIdx] : 0;
|
|
24
|
+
const botG = hasBot ? rgba[botIdx + 1] : 0;
|
|
25
|
+
const botB = hasBot ? rgba[botIdx + 2] : 0;
|
|
26
|
+
const botA = hasBot ? rgba[botIdx + 3] : 0;
|
|
27
|
+
const topOpaque = topA >= 128;
|
|
28
|
+
const botOpaque = hasBot && botA >= 128;
|
|
29
|
+
if (topOpaque && botOpaque) {
|
|
30
|
+
row += fg(topR, topG, topB) + bg(botR, botG, botB) + UPPER_HALF;
|
|
31
|
+
}
|
|
32
|
+
else if (topOpaque && !botOpaque) {
|
|
33
|
+
row += fg(topR, topG, topB) + bg(bgR, bgG, bgB) + UPPER_HALF;
|
|
34
|
+
}
|
|
35
|
+
else if (!topOpaque && botOpaque) {
|
|
36
|
+
row += fg(botR, botG, botB) + bg(bgR, bgG, bgB) + LOWER_HALF;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
row += bg(bgR, bgG, bgB) + ' ';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
rows.push(row + RESET);
|
|
43
|
+
}
|
|
44
|
+
return rows.join('\n');
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
export function isCancel(value) {
|
|
3
|
+
return p.isCancel(value);
|
|
4
|
+
}
|
|
5
|
+
export function getRootOpts(cmd) {
|
|
6
|
+
let current = cmd;
|
|
7
|
+
while (current?.parent) {
|
|
8
|
+
current = current.parent;
|
|
9
|
+
}
|
|
10
|
+
return current?.opts() ?? {};
|
|
11
|
+
}
|
package/dist/ui/index.js
ADDED
package/dist/ui/panel.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DEFAULT_PANEL_WIDTH } from '../lib/constants.js';
|
|
2
|
+
import { brand } from './theme.js';
|
|
3
|
+
export function panel(lines, options = {}) {
|
|
4
|
+
const { title, width = DEFAULT_PANEL_WIDTH, padding = 1 } = options;
|
|
5
|
+
const pad = ' '.repeat(padding);
|
|
6
|
+
const innerWidth = width - 2;
|
|
7
|
+
const top = title
|
|
8
|
+
? `╭─ ${brand.heading(title)} ${'─'.repeat(Math.max(0, innerWidth - title.length - 3))}╮`
|
|
9
|
+
: `╭${'─'.repeat(innerWidth)}╮`;
|
|
10
|
+
const bottom = `╰${'─'.repeat(innerWidth)}╯`;
|
|
11
|
+
const empty = `│${' '.repeat(innerWidth)}│`;
|
|
12
|
+
const body = lines.map((line) => {
|
|
13
|
+
const visible = stripAnsi(line);
|
|
14
|
+
const fill = Math.max(0, innerWidth - visible.length - padding * 2);
|
|
15
|
+
return `│${pad}${line}${' '.repeat(fill)}${pad}│`;
|
|
16
|
+
});
|
|
17
|
+
return [top, empty, ...body, empty, bottom].join('\n');
|
|
18
|
+
}
|
|
19
|
+
function stripAnsi(str) {
|
|
20
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
21
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
interface PreviewOptions {
|
|
2
|
+
width?: number;
|
|
3
|
+
height?: number;
|
|
4
|
+
}
|
|
5
|
+
export declare function renderSprite(imagePath: string, options?: PreviewOptions): Promise<string>;
|
|
6
|
+
export declare function textFallback(form: string, colour: string, name?: string): string;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { brand } from './theme.js';
|
|
2
|
+
export async function renderSprite(imagePath, options = {}) {
|
|
3
|
+
const { width = 32, height = 16 } = options;
|
|
4
|
+
try {
|
|
5
|
+
const terminalImage = await import('terminal-image');
|
|
6
|
+
const { readFile } = await import('node:fs/promises');
|
|
7
|
+
const buffer = await readFile(imagePath);
|
|
8
|
+
return await terminalImage.default.buffer(buffer, { width, height });
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return brand.muted(`[sprite: ${imagePath}]`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function textFallback(form, colour, name) {
|
|
15
|
+
const parts = [form];
|
|
16
|
+
if (colour)
|
|
17
|
+
parts.push(colour);
|
|
18
|
+
if (name)
|
|
19
|
+
parts.push(`"${name}"`);
|
|
20
|
+
return brand.muted(`[${parts.join(', ')}]`);
|
|
21
|
+
}
|