kimaki 0.18.0 → 0.20.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/dist/cli-commands/project.js +85 -87
- package/dist/cli-commands/send.js +34 -10
- package/dist/cli-commands/session.js +4 -1
- package/dist/commands/agent.js +51 -4
- package/dist/discord-bot.js +24 -10
- package/dist/markdown.js +59 -4
- package/dist/markdown.test.js +73 -2
- package/dist/message-formatting.js +46 -16
- package/dist/message-formatting.test.js +39 -1
- package/dist/session-handler/thread-session-runtime.js +40 -19
- package/dist/store.js +1 -0
- package/package.json +4 -4
- package/skills/goke/SKILL.md +17 -0
- package/skills/holocron/SKILL.md +66 -1
- package/skills/sigillo/SKILL.md +34 -0
- package/skills/zele/SKILL.md +13 -0
- package/src/cli-commands/project.ts +99 -87
- package/src/cli-commands/send.ts +37 -13
- package/src/cli-commands/session.ts +5 -2
- package/src/commands/agent.ts +64 -3
- package/src/discord-bot.ts +29 -10
- package/src/markdown.test.ts +89 -2
- package/src/markdown.ts +63 -4
- package/src/message-formatting.test.ts +60 -1
- package/src/message-formatting.ts +58 -15
- package/src/session-handler/thread-session-runtime.ts +49 -18
- package/src/store.ts +8 -0
package/dist/markdown.test.js
CHANGED
|
@@ -36,6 +36,18 @@ function createMatchers() {
|
|
|
36
36
|
],
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
|
+
const toolCallMatcher = {
|
|
40
|
+
id: 'tool-call-reply',
|
|
41
|
+
priority: 90,
|
|
42
|
+
when: { latestUserTextIncludes: 'use a tool please' },
|
|
43
|
+
then: {
|
|
44
|
+
parts: [
|
|
45
|
+
{ type: 'stream-start', warnings: [] },
|
|
46
|
+
{ type: 'tool-call', toolCallId: 'tc1', toolName: 'bash', input: JSON.stringify({ command: 'echo hello world', description: 'Print greeting' }) },
|
|
47
|
+
{ type: 'finish', finishReason: 'tool-calls', usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 } },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
};
|
|
39
51
|
const defaultMatcher = {
|
|
40
52
|
id: 'default-reply',
|
|
41
53
|
priority: 1,
|
|
@@ -49,12 +61,13 @@ function createMatchers() {
|
|
|
49
61
|
],
|
|
50
62
|
},
|
|
51
63
|
};
|
|
52
|
-
return [helloMatcher, defaultMatcher];
|
|
64
|
+
return [helloMatcher, toolCallMatcher, defaultMatcher];
|
|
53
65
|
}
|
|
54
66
|
let client;
|
|
55
67
|
let directories;
|
|
56
68
|
let testStartTime;
|
|
57
69
|
let sessionID;
|
|
70
|
+
let toolSessionID;
|
|
58
71
|
beforeAll(async () => {
|
|
59
72
|
testStartTime = Date.now();
|
|
60
73
|
directories = createRunDirectories();
|
|
@@ -117,7 +130,34 @@ beforeAll(async () => {
|
|
|
117
130
|
setTimeout(resolve, 200);
|
|
118
131
|
});
|
|
119
132
|
}
|
|
120
|
-
|
|
133
|
+
// Create a second session that triggers a tool call (bash echo)
|
|
134
|
+
const toolCreateResult = await client.session.create({
|
|
135
|
+
directory: directories.projectDirectory,
|
|
136
|
+
title: 'Tool Call Session',
|
|
137
|
+
});
|
|
138
|
+
toolSessionID = toolCreateResult.data.id;
|
|
139
|
+
await client.session.promptAsync({
|
|
140
|
+
sessionID: toolSessionID,
|
|
141
|
+
directory: directories.projectDirectory,
|
|
142
|
+
parts: [{ type: 'text', text: 'use a tool please' }],
|
|
143
|
+
});
|
|
144
|
+
// Wait for tool execution to complete
|
|
145
|
+
const toolMaxWait = 15_000;
|
|
146
|
+
const toolPollStart = Date.now();
|
|
147
|
+
while (Date.now() - toolPollStart < toolMaxWait) {
|
|
148
|
+
const msgs = await client.session.messages({
|
|
149
|
+
sessionID: toolSessionID,
|
|
150
|
+
directory: directories.projectDirectory,
|
|
151
|
+
});
|
|
152
|
+
const messages = msgs.data || [];
|
|
153
|
+
const hasToolPart = messages.some((m) => m.parts.some((p) => p.type === 'tool' && p.state?.status === 'completed'));
|
|
154
|
+
if (hasToolPart) {
|
|
155
|
+
await new Promise((resolve) => { setTimeout(resolve, 500); });
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
await new Promise((resolve) => { setTimeout(resolve, 200); });
|
|
159
|
+
}
|
|
160
|
+
}, 30_000);
|
|
121
161
|
afterAll(async () => {
|
|
122
162
|
if (directories) {
|
|
123
163
|
await cleanupTestSessions({
|
|
@@ -262,3 +302,34 @@ test('generate markdown with lastAssistantOnly', async () => {
|
|
|
262
302
|
// Should contain the assistant response
|
|
263
303
|
expect(markdown).toContain('Hello! This is a deterministic markdown test response.');
|
|
264
304
|
});
|
|
305
|
+
test('compact tools: tool calls show one-liner with line count', async () => {
|
|
306
|
+
const exporter = new ShareMarkdown(client);
|
|
307
|
+
const result = await exporter.generate({
|
|
308
|
+
sessionID: toolSessionID,
|
|
309
|
+
compactTools: true,
|
|
310
|
+
});
|
|
311
|
+
expect(errore.isOk(result)).toBe(true);
|
|
312
|
+
const md = errore.unwrap(result);
|
|
313
|
+
// Compact mode: exact one-liner format with params and line count
|
|
314
|
+
expect(md).toContain('> 🛠️ **bash** command=echo hello world, description=Print greeting');
|
|
315
|
+
expect(md).toMatch(/\(\d+ lines?\)/);
|
|
316
|
+
// Should NOT contain full output code blocks or input YAML
|
|
317
|
+
expect(md).not.toContain('**Output:**');
|
|
318
|
+
expect(md).not.toContain('**Input:**');
|
|
319
|
+
expect(md).not.toContain('```yaml');
|
|
320
|
+
});
|
|
321
|
+
test('verbose tools: tool calls show full input and output', async () => {
|
|
322
|
+
const exporter = new ShareMarkdown(client);
|
|
323
|
+
const result = await exporter.generate({
|
|
324
|
+
sessionID: toolSessionID,
|
|
325
|
+
compactTools: false,
|
|
326
|
+
});
|
|
327
|
+
expect(errore.isOk(result)).toBe(true);
|
|
328
|
+
const md = errore.unwrap(result);
|
|
329
|
+
// Verbose mode: full tool rendering with input YAML and output code block
|
|
330
|
+
expect(md).toContain('#### 🛠️ Tool: bash');
|
|
331
|
+
expect(md).toContain('**Input:**');
|
|
332
|
+
expect(md).toContain('```yaml');
|
|
333
|
+
expect(md).toContain('**Output:**');
|
|
334
|
+
expect(md).toContain('hello world');
|
|
335
|
+
});
|
|
@@ -266,6 +266,42 @@ export async function getFileAttachments(message) {
|
|
|
266
266
|
return results.filter((r) => r !== null);
|
|
267
267
|
}
|
|
268
268
|
const MAX_BASH_COMMAND_INLINE_LENGTH = 100;
|
|
269
|
+
/**
|
|
270
|
+
* Format the inline title for a bash tool part. Handles three cases:
|
|
271
|
+
* 1. Short single-line command → show full command
|
|
272
|
+
* 2. Long/multiline command with description → show description
|
|
273
|
+
* 3. Long/multiline command without description → truncate first line of command
|
|
274
|
+
*
|
|
275
|
+
* The description field was removed from the bash tool schema in newer opencode
|
|
276
|
+
* versions, so case 3 is the common path now. Without this fallback, long commands
|
|
277
|
+
* would render as just "┣ bash" with no context.
|
|
278
|
+
*/
|
|
279
|
+
export function formatBashToolTitle({ command, description, stateTitle, }) {
|
|
280
|
+
if (!command && !description && !stateTitle)
|
|
281
|
+
return '';
|
|
282
|
+
const isSingleLine = !command.includes('\n');
|
|
283
|
+
// Find first non-empty line to handle commands with leading blank lines
|
|
284
|
+
const firstMeaningfulLine = command
|
|
285
|
+
.split('\n')
|
|
286
|
+
.find((line) => line.trim().length > 0)
|
|
287
|
+
?.trimStart() ?? '';
|
|
288
|
+
if (command && isSingleLine && command.length <= MAX_BASH_COMMAND_INLINE_LENGTH) {
|
|
289
|
+
return ` _${escapeInlineMarkdown(command)}_`;
|
|
290
|
+
}
|
|
291
|
+
if (description) {
|
|
292
|
+
return ` _${escapeInlineMarkdown(description)}_`;
|
|
293
|
+
}
|
|
294
|
+
if (firstMeaningfulLine.length > 0) {
|
|
295
|
+
const truncated = firstMeaningfulLine.length > MAX_BASH_COMMAND_INLINE_LENGTH
|
|
296
|
+
? firstMeaningfulLine.slice(0, MAX_BASH_COMMAND_INLINE_LENGTH) + '…'
|
|
297
|
+
: firstMeaningfulLine;
|
|
298
|
+
return ` _${escapeInlineMarkdown(truncated)}_`;
|
|
299
|
+
}
|
|
300
|
+
if (stateTitle) {
|
|
301
|
+
return ` _${escapeInlineMarkdown(stateTitle)}_`;
|
|
302
|
+
}
|
|
303
|
+
return '';
|
|
304
|
+
}
|
|
269
305
|
export function getToolSummaryText(part) {
|
|
270
306
|
if (part.type !== 'tool')
|
|
271
307
|
return '';
|
|
@@ -449,12 +485,10 @@ export function formatPart(part, prefix) {
|
|
|
449
485
|
}
|
|
450
486
|
const command = part.state.input?.command || '';
|
|
451
487
|
const description = part.state.input?.description || '';
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
? ` _${escapeInlineMarkdown(description)}_`
|
|
457
|
-
: '';
|
|
488
|
+
const toolTitle = formatBashToolTitle({
|
|
489
|
+
command,
|
|
490
|
+
description,
|
|
491
|
+
});
|
|
458
492
|
return `┣ ${pfx}bash${toolTitle}`;
|
|
459
493
|
}
|
|
460
494
|
const summaryText = getToolSummaryText(part);
|
|
@@ -466,16 +500,12 @@ export function formatPart(part, prefix) {
|
|
|
466
500
|
else if (part.tool === 'bash') {
|
|
467
501
|
const command = part.state.input?.command || '';
|
|
468
502
|
const description = part.state.input?.description || '';
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
else if (stateTitle) {
|
|
477
|
-
toolTitle = `_${escapeInlineMarkdown(stateTitle)}_`;
|
|
478
|
-
}
|
|
503
|
+
const formatted = formatBashToolTitle({
|
|
504
|
+
command,
|
|
505
|
+
description,
|
|
506
|
+
stateTitle,
|
|
507
|
+
});
|
|
508
|
+
toolTitle = formatted.startsWith(' ') ? formatted.slice(1) : formatted;
|
|
479
509
|
}
|
|
480
510
|
else if (stateTitle) {
|
|
481
511
|
toolTitle = `_${escapeInlineMarkdown(stateTitle)}_`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from 'vitest';
|
|
2
|
-
import { formatPart, formatTodoList, serializeEmbeds, serializePoll, serializeMessageSnapshots } from './message-formatting.js';
|
|
2
|
+
import { formatBashToolTitle, formatPart, formatTodoList, serializeEmbeds, serializePoll, serializeMessageSnapshots } from './message-formatting.js';
|
|
3
3
|
describe('formatPart', () => {
|
|
4
4
|
test('callout text does not get ⬥ prefix', () => {
|
|
5
5
|
const part = {
|
|
@@ -42,6 +42,44 @@ describe('formatPart', () => {
|
|
|
42
42
|
`);
|
|
43
43
|
});
|
|
44
44
|
});
|
|
45
|
+
describe('formatBashToolTitle', () => {
|
|
46
|
+
test('short single-line command shown in full', () => {
|
|
47
|
+
expect(formatBashToolTitle({ command: 'echo hello' })).toMatchInlineSnapshot(`" _echo hello_"`);
|
|
48
|
+
});
|
|
49
|
+
test('multiline command without description truncates to first line', () => {
|
|
50
|
+
expect(formatBashToolTitle({ command: 'echo hello\necho world\necho done' })).toMatchInlineSnapshot(`" _echo hello_"`);
|
|
51
|
+
});
|
|
52
|
+
test('long single-line command is truncated with ellipsis', () => {
|
|
53
|
+
const longCommand = 'a'.repeat(150);
|
|
54
|
+
const result = formatBashToolTitle({ command: longCommand });
|
|
55
|
+
expect(result).toContain('…');
|
|
56
|
+
expect(result.length).toBeLessThan(150);
|
|
57
|
+
});
|
|
58
|
+
test('description is preferred over truncated command when present', () => {
|
|
59
|
+
expect(formatBashToolTitle({
|
|
60
|
+
command: 'echo hello\necho world',
|
|
61
|
+
description: 'Print greeting',
|
|
62
|
+
})).toMatchInlineSnapshot(`" _Print greeting_"`);
|
|
63
|
+
});
|
|
64
|
+
test('stateTitle used as last resort', () => {
|
|
65
|
+
expect(formatBashToolTitle({ command: '', stateTitle: 'Running tests' })).toMatchInlineSnapshot(`" _Running tests_"`);
|
|
66
|
+
});
|
|
67
|
+
test('empty inputs return empty string', () => {
|
|
68
|
+
expect(formatBashToolTitle({ command: '' })).toBe('');
|
|
69
|
+
});
|
|
70
|
+
test('leading blank line skipped, uses first meaningful line', () => {
|
|
71
|
+
expect(formatBashToolTitle({ command: '\npnpm test\npnpm build' })).toMatchInlineSnapshot(`" _pnpm test_"`);
|
|
72
|
+
});
|
|
73
|
+
test('whitespace-only first line skipped', () => {
|
|
74
|
+
expect(formatBashToolTitle({ command: ' \npnpm test' })).toMatchInlineSnapshot(`" _pnpm test_"`);
|
|
75
|
+
});
|
|
76
|
+
test('no description field (new opencode) with multiline command', () => {
|
|
77
|
+
// This is the exact scenario that was broken: opencode removed `description`
|
|
78
|
+
// from the bash tool schema, so multiline commands rendered as just "┣ bash"
|
|
79
|
+
const command = 'git diff HEAD~1 --stat && git log --oneline -5';
|
|
80
|
+
expect(formatBashToolTitle({ command: command + '\n' + 'echo done' })).toMatchInlineSnapshot(`" _git diff HEAD\\~1 --stat && git log --oneline -5_"`);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
45
83
|
describe('formatTodoList', () => {
|
|
46
84
|
test('formats active todo with monospace numbers', () => {
|
|
47
85
|
const part = {
|
|
@@ -190,23 +190,23 @@ function getTokenTotal(tokens) {
|
|
|
190
190
|
tokens.cache.read +
|
|
191
191
|
tokens.cache.write);
|
|
192
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Built-in read-only tools that are hidden in default verbosity mode.
|
|
195
|
+
* Any tool NOT in this list is considered "essential" and shown,
|
|
196
|
+
* which means custom tools, MCP tools, and plugin tools are visible by default.
|
|
197
|
+
*/
|
|
198
|
+
const HIDDEN_READONLY_TOOLS = [
|
|
199
|
+
'read',
|
|
200
|
+
'glob',
|
|
201
|
+
'grep',
|
|
202
|
+
'describe-media',
|
|
203
|
+
'todoread',
|
|
204
|
+
];
|
|
193
205
|
/** Check if a tool part is "essential" (shown in text-and-essential-tools mode). */
|
|
194
206
|
export function isEssentialToolName(toolName) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
'apply_patch',
|
|
199
|
-
'bash',
|
|
200
|
-
'webfetch',
|
|
201
|
-
'websearch',
|
|
202
|
-
'googlesearch',
|
|
203
|
-
'codesearch',
|
|
204
|
-
'task',
|
|
205
|
-
'todowrite',
|
|
206
|
-
'skill',
|
|
207
|
-
];
|
|
208
|
-
// Also match any MCP tool that contains these names
|
|
209
|
-
return essentialTools.some((name) => {
|
|
207
|
+
// Hide known read-only built-in tools; show everything else
|
|
208
|
+
// (custom tools, MCP tools, plugin tools are visible by default)
|
|
209
|
+
return !HIDDEN_READONLY_TOOLS.some((name) => {
|
|
210
210
|
return toolName === name || toolName.endsWith(`_${name}`);
|
|
211
211
|
});
|
|
212
212
|
}
|
|
@@ -2062,6 +2062,15 @@ export class ThreadSessionRuntime {
|
|
|
2062
2062
|
skippedBySessionGuard = true;
|
|
2063
2063
|
return;
|
|
2064
2064
|
}
|
|
2065
|
+
// Context-only messages (noReply) should not create a new session.
|
|
2066
|
+
// If there is no existing session, silently skip.
|
|
2067
|
+
if (input.noReply) {
|
|
2068
|
+
const existingSessionId = this.state?.sessionId || await getThreadSession(this.thread.id) || undefined;
|
|
2069
|
+
if (!existingSessionId) {
|
|
2070
|
+
logger.log(`[INGRESS] Skipping noReply message for thread ${this.threadId}: no existing session`);
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2065
2074
|
// Helper: stop typing and drain queued local messages on error.
|
|
2066
2075
|
const cleanupOnError = async (errorMessage) => {
|
|
2067
2076
|
this.stopTyping();
|
|
@@ -2269,6 +2278,7 @@ export class ThreadSessionRuntime {
|
|
|
2269
2278
|
...(resolvedAgent ? { agent: resolvedAgent } : {}),
|
|
2270
2279
|
...(modelField ? { model: modelField } : {}),
|
|
2271
2280
|
...variantField,
|
|
2281
|
+
...(input.noReply ? { noReply: true } : {}),
|
|
2272
2282
|
};
|
|
2273
2283
|
const promptResult = await getClient().session.promptAsync(request)
|
|
2274
2284
|
.catch((e) => new OpenCodeSdkError({ operation: 'session.promptAsync', cause: e }));
|
|
@@ -2284,7 +2294,10 @@ export class ThreadSessionRuntime {
|
|
|
2284
2294
|
return;
|
|
2285
2295
|
}
|
|
2286
2296
|
logger.log(`[INGRESS] promptAsync accepted by opencode queue sessionId=${session.id} threadId=${this.threadId}`);
|
|
2287
|
-
|
|
2297
|
+
// noReply messages don't trigger the agent loop, so don't mark as busy
|
|
2298
|
+
if (!input.noReply) {
|
|
2299
|
+
this.markQueueDispatchBusy(session.id);
|
|
2300
|
+
}
|
|
2288
2301
|
});
|
|
2289
2302
|
if (skippedBySessionGuard) {
|
|
2290
2303
|
return { queued: false };
|
|
@@ -2426,9 +2439,17 @@ export class ThreadSessionRuntime {
|
|
|
2426
2439
|
// Route with the resolved mode through normal paths.
|
|
2427
2440
|
// Await the enqueue so session state (ensureSession, setThreadSession)
|
|
2428
2441
|
// is persisted before the next message's preprocessing reads it.
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2442
|
+
// noReply messages always go through the opencode path so the flag
|
|
2443
|
+
// reaches promptAsync; local queue doesn't support noReply.
|
|
2444
|
+
const enqueueResult = resolvedInput.noReply
|
|
2445
|
+
? await this.submitViaOpencodeQueue({
|
|
2446
|
+
...resolvedInput,
|
|
2447
|
+
mode: 'opencode',
|
|
2448
|
+
command: undefined,
|
|
2449
|
+
})
|
|
2450
|
+
: (resolvedInput.mode === 'local-queue' || resolvedInput.command)
|
|
2451
|
+
? await this.enqueueViaLocalQueue(resolvedInput)
|
|
2452
|
+
: await this.submitViaOpencodeQueue(resolvedInput);
|
|
2432
2453
|
resolveOuter(enqueueResult);
|
|
2433
2454
|
}
|
|
2434
2455
|
catch (err) {
|
package/dist/store.js
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "kimaki",
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.20.0",
|
|
6
6
|
"repository": "https://github.com/remorses/kimaki",
|
|
7
7
|
"bin": "bin.js",
|
|
8
8
|
"files": [
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"tsx": "^4.20.5",
|
|
26
26
|
"undici": "^8.0.2",
|
|
27
27
|
"discord-digital-twin": "^0.1.0",
|
|
28
|
-
"
|
|
28
|
+
"opencode-deterministic-provider": "^0.0.1",
|
|
29
29
|
"opencode-cached-provider": "^0.0.1",
|
|
30
|
-
"
|
|
30
|
+
"db": "^0.0.0"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@ai-sdk/google": "^3.0.53",
|
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
"yaml": "^2.8.3",
|
|
63
63
|
"zod": "^4.3.6",
|
|
64
64
|
"zustand": "^5.0.11",
|
|
65
|
-
"errore": "^0.14.1",
|
|
66
65
|
"libsqlproxy": "^0.1.0",
|
|
67
66
|
"opencode-injection-guard": "^0.2.1",
|
|
67
|
+
"errore": "^0.14.1",
|
|
68
68
|
"traforo": "^0.7.1"
|
|
69
69
|
},
|
|
70
70
|
"optionalDependencies": {
|
package/skills/goke/SKILL.md
CHANGED
|
@@ -178,6 +178,23 @@ import { openInBrowser } from 'goke'
|
|
|
178
178
|
await openInBrowser('https://example.com/dashboard')
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
+
## Command Descriptions and Examples
|
|
182
|
+
|
|
183
|
+
**Use backtick formatting in descriptions** for flags and command references (e.g. `` `--status` ``, `` `mycli deploy` ``). Plain text flag names won't render as code in generated docs.
|
|
184
|
+
|
|
185
|
+
**Use `.example()` for usage examples, not the description string.** `generateDocs()` auto-wraps `.example()` strings in fenced `` ```sh `` code blocks. Examples in the description render as plain text without syntax highlighting.
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
cli.command(
|
|
189
|
+
'query <sql>',
|
|
190
|
+
dedent`
|
|
191
|
+
Run a SQL query. Add \`--json\` for the raw JSON envelope.
|
|
192
|
+
`,
|
|
193
|
+
)
|
|
194
|
+
.example('mycli query "SELECT * FROM users" -p my-app')
|
|
195
|
+
.example('mycli query "SELECT * FROM users FORMAT CSV" > out.csv')
|
|
196
|
+
```
|
|
197
|
+
|
|
181
198
|
## Command Naming Conventions
|
|
182
199
|
|
|
183
200
|
**ALWAYS read existing commands before adding a new one.** Scan the CLI for option names, verbs, and noun patterns already in use. New commands must stay consistent with what exists.
|
package/skills/holocron/SKILL.md
CHANGED
|
@@ -43,6 +43,7 @@ When the user asks about a specific workflow, fetch the matching page directly:
|
|
|
43
43
|
- **Node deploy**: https://holocron.so/docs/deploy/node.md
|
|
44
44
|
- **Holocron deploy**: https://holocron.so/docs/deploy/holocron.md
|
|
45
45
|
- **AI assistant docs**: https://holocron.so/docs/ai/assistant.md
|
|
46
|
+
- **Bleed**: https://holocron.so/docs/customize/bleed.md
|
|
46
47
|
|
|
47
48
|
```bash
|
|
48
49
|
curl -fsSL https://holocron.so/docs/quickstart.md
|
|
@@ -326,6 +327,36 @@ sidebarTitle: Auth Providers
|
|
|
326
327
|
below the title and in search engine snippets. Write a single sentence that
|
|
327
328
|
summarizes the page content. Longer descriptions get truncated with an ellipsis.
|
|
328
329
|
|
|
330
|
+
## Page titles (MUST follow)
|
|
331
|
+
|
|
332
|
+
Holocron renders the browser `<title>` as `{page title} — {site name}`. Two
|
|
333
|
+
frontmatter fields control what appears where:
|
|
334
|
+
|
|
335
|
+
- **`title`** — browser tab, Google results, OG image, page heading. Write for SEO.
|
|
336
|
+
- **`sidebarTitle`** — sidebar navigation only. Write for scannability (short label).
|
|
337
|
+
|
|
338
|
+
**Always set both.** `title` is the full descriptive phrase; `sidebarTitle` is
|
|
339
|
+
the short sidebar label. Without `sidebarTitle`, the full title shows in the
|
|
340
|
+
sidebar and often wraps or looks verbose.
|
|
341
|
+
|
|
342
|
+
```mdx
|
|
343
|
+
---
|
|
344
|
+
title: Open-source browser automation for AI agents
|
|
345
|
+
sidebarTitle: Home
|
|
346
|
+
description: Automate any browser with a simple TypeScript API.
|
|
347
|
+
---
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Browser tab: `Open-source browser automation for AI agents — Playwriter`
|
|
351
|
+
Sidebar: `Home`
|
|
352
|
+
|
|
353
|
+
**Rules:**
|
|
354
|
+
|
|
355
|
+
- **Never set `title` identical to the site name.** `Playwriter — Playwriter` is redundant.
|
|
356
|
+
- **Every `title` MUST be descriptive, not a generic label.** `Introduction`, `Getting Started`, `Overview` say nothing in a browser tab. Write what the page covers.
|
|
357
|
+
- **Every `title` MUST be unique across the site.**
|
|
358
|
+
- **`index.mdx` MUST use `sidebarTitle: Home`** (or `Overview`) and a descriptive `title`.
|
|
359
|
+
|
|
329
360
|
## Custom entry (mounting docs inside a Spiceflow app)
|
|
330
361
|
|
|
331
362
|
When the site mounts Holocron inside an existing **Spiceflow** app (the `entry`
|
|
@@ -416,6 +447,12 @@ below the main content to accommodate the aside height. Keep aside callouts
|
|
|
416
447
|
short, and only place them in sections that have at least a few paragraphs of
|
|
417
448
|
body text.
|
|
418
449
|
|
|
450
|
+
## Moving or renaming a page
|
|
451
|
+
|
|
452
|
+
When moving or renaming a page, **always add a redirect** from the old slug to
|
|
453
|
+
the new one so existing links and bookmarks don't break. Update internal links
|
|
454
|
+
in other pages and the slug in `docs.json` navigation to match.
|
|
455
|
+
|
|
419
456
|
## New pages and navigation
|
|
420
457
|
|
|
421
458
|
After creating a new `.mdx` or `.md` page, add its slug to `docs.jsonc`
|
|
@@ -569,7 +606,7 @@ Use ASCII diagrams frequently in MDX pages. Always use the **`diagram`** languag
|
|
|
569
606
|
- Never exceed 94 characters per line.
|
|
570
607
|
- Prefer a **varied, organic layout**. Mix plain text labels, boxes for major components, and directional arrows.
|
|
571
608
|
- All connections must use **directional arrows**. Never use plain lines without an arrowhead.
|
|
572
|
-
- Verify alignment by
|
|
609
|
+
- Verify alignment by running `npx -y @holocron.so/cli diagrams fix <file>` (see dedicated section below).
|
|
573
610
|
|
|
574
611
|
````mdx
|
|
575
612
|
```diagram
|
|
@@ -590,6 +627,23 @@ docs.jsonc ───►│ Vite Plugin │──────► Build Output
|
|
|
590
627
|
```
|
|
591
628
|
````
|
|
592
629
|
|
|
630
|
+
## Always run `diagrams fix` after editing diagrams
|
|
631
|
+
|
|
632
|
+
LLMs cannot count characters reliably. Every time you create or edit a diagram in an MDX page, you **must** run the alignment fixer before committing. This is not optional.
|
|
633
|
+
|
|
634
|
+
```bash
|
|
635
|
+
npx -y @holocron.so/cli diagrams fix path/to/file.mdx
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
The command:
|
|
639
|
+
- Fixes box alignment (padding, border widths, junctions) in-place
|
|
640
|
+
- Reports how many lines were changed
|
|
641
|
+
- Exits with code 1 if any lines exceed max width (94 cols by default)
|
|
642
|
+
|
|
643
|
+
If the output says "No changes", the diagram was already correct. If it reports width violations, shorten the offending lines manually and re-run.
|
|
644
|
+
|
|
645
|
+
**Do not skip this step.** Even if the diagram looks correct in your editor, run the command. Off-by-one padding errors are invisible to LLMs but obvious to humans in monospace fonts.
|
|
646
|
+
|
|
593
647
|
## OpenAPI summaries
|
|
594
648
|
|
|
595
649
|
OpenAPI operation `summary` fields become sidebar titles. The sidebar is only
|
|
@@ -601,6 +655,15 @@ characters. Use the same concise style as page `sidebarTitle` fields.
|
|
|
601
655
|
- Never repeat the tag/group name in the summary. If the tag is `Deploy`, the summary should not start with "Deploy".
|
|
602
656
|
- If the summary reads well as a sidebar label at 230px, it is short enough.
|
|
603
657
|
|
|
658
|
+
## Bleed — extending content past the prose column
|
|
659
|
+
|
|
660
|
+
Always wrap **YouTube embeds, videos, and large images** in `<div className='bleed'>`
|
|
661
|
+
so they extend into both page margins instead of sitting at narrow prose width.
|
|
662
|
+
`<Frame>` also accepts `className='bleed'`. Containers like Callout, Card, and
|
|
663
|
+
Accordion apply `no-bleed` automatically to keep descendants inside their frame.
|
|
664
|
+
|
|
665
|
+
See https://holocron.so/docs/customize/bleed.md for the full reference.
|
|
666
|
+
|
|
604
667
|
## Agent rules
|
|
605
668
|
|
|
606
669
|
- **Always place MDX pages inside the Holocron `pagesDir`.** Before creating or
|
|
@@ -614,5 +677,7 @@ characters. Use the same concise style as page `sidebarTitle` fields.
|
|
|
614
677
|
- All ASCII diagrams in MDX pages must use the `diagram` language hint on the
|
|
615
678
|
code fence (` ```diagram `). This renders them with proper styling on the
|
|
616
679
|
website instead of plain monospace.
|
|
680
|
+
- After creating or editing any diagram, run `npx -y @holocron.so/cli diagrams fix <file>`
|
|
681
|
+
to auto-fix alignment. LLMs cannot count characters reliably.
|
|
617
682
|
- Never pipe curl or `--help` output through `head`, `tail`, or any truncation
|
|
618
683
|
command. Always read the full output.
|
package/skills/sigillo/SKILL.md
CHANGED
|
@@ -199,6 +199,40 @@ After setup, `sigillo run` in any subdirectory uses that project + environment a
|
|
|
199
199
|
|
|
200
200
|
Avoid `sigillo secrets download` unless a specific tool requires a file. Prefer injecting directly via `sigillo run --` so values never touch the filesystem.
|
|
201
201
|
|
|
202
|
+
## Placeholder secrets (user fills in later)
|
|
203
|
+
|
|
204
|
+
When the user asks to add a secret but will provide the actual value later via the dashboard, set it with an empty value:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
sigillo secrets set DATABASE_URL ""
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
This creates the secret as a placeholder. The CLI shows `empty: true` when listing secrets so empty placeholders are visible. After setting the placeholder, always print the dashboard URL so the user can fill it in:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
https://sigillo.dev/dash/projects/<PROJECT_ID>/envs/<ENV_SLUG>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Replace `<PROJECT_ID>` and `<ENV_SLUG>` with the actual values from the current setup. To find them:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
sigillo secrets
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The `environment_id` in the output is the env ID. The project ID is from `sigillo setup` or `sigillo projects`.
|
|
223
|
+
|
|
224
|
+
To set placeholders across all environments at once:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
sigillo secrets set DATABASE_URL "" -c dev -c preview -c prod
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
For secrets that need real random values immediately (auth secrets, encryption keys), generate them instead of leaving empty:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
sigillo secrets set AUTH_SECRET "$(openssl rand -base64 32)" -c dev -c preview -c prod
|
|
234
|
+
```
|
|
235
|
+
|
|
202
236
|
## Bootstrapping a project for a new codebase
|
|
203
237
|
|
|
204
238
|
When a codebase needs Sigillo for the first time, the agent creates the org, project, and placeholder secrets. The user fills in real values later via the web UI.
|
package/skills/zele/SKILL.md
CHANGED
|
@@ -47,3 +47,16 @@ The README and `zele --help` output are the source of truth for commands, option
|
|
|
47
47
|
```
|
|
48
48
|
5. **Google-only features** (labels, Gmail filters, `zele cal *`, full profile) fail on IMAP accounts with a clear error. Check `zele whoami` output for account type before using them.
|
|
49
49
|
6. **Headless Google login** requires a tmux wrapper because `zele login` is interactive. See the README "Remote / headless login" section for the exact pattern.
|
|
50
|
+
7. **Waiting for emails** with `zele mail watch`. It polls for new emails matching a filter and exits as soon as one arrives. Use this to wait for replies, verification codes, or any expected email:
|
|
51
|
+
```bash
|
|
52
|
+
# wait for a reply from alice (no timeout, blocks until match)
|
|
53
|
+
zele mail watch --filter "is:unread from:alice@example.com"
|
|
54
|
+
|
|
55
|
+
# wait for a verification code with a 5-minute timeout
|
|
56
|
+
zele mail watch --filter "is:unread subject:verification" --timeout 300
|
|
57
|
+
|
|
58
|
+
# send an email then wait for the reply
|
|
59
|
+
zele mail send --to bob@example.com --subject "Question" --body "Hey, can you check this?"
|
|
60
|
+
zele mail watch --filter "is:unread from:bob@example.com subject:Re:Question" --timeout 600
|
|
61
|
+
```
|
|
62
|
+
If the matched email wasn't the expected one, call `zele mail watch` again with a more specific filter. Exit code 0 means a match was found, exit code 1 means timeout.
|