kimaki 0.19.0 → 0.20.1
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/commands/agent.js +51 -4
- package/dist/commands/user-command.js +47 -5
- package/dist/discord-bot.js +36 -27
- package/dist/discord-utils.js +18 -1
- package/dist/message-formatting.js +48 -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/dist/system-message.js +16 -4
- package/dist/system-message.test.js +16 -4
- package/package.json +3 -3
- package/skills/egaki/SKILL.md +17 -0
- package/skills/goke/SKILL.md +17 -0
- package/skills/holocron/SKILL.md +66 -1
- package/skills/sigillo/SKILL.md +35 -4
- package/skills/zele/SKILL.md +13 -0
- package/src/cli-commands/project.ts +99 -87
- package/src/commands/agent.ts +64 -3
- package/src/commands/user-command.ts +65 -4
- package/src/discord-bot.ts +43 -28
- package/src/discord-utils.ts +24 -0
- package/src/message-formatting.test.ts +60 -1
- package/src/message-formatting.ts +59 -15
- package/src/session-handler/thread-session-runtime.ts +49 -18
- package/src/store.ts +8 -0
- package/src/system-message.test.ts +16 -4
- package/src/system-message.ts +16 -4
|
@@ -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/dist/system-message.js
CHANGED
|
@@ -279,10 +279,22 @@ The user is reading your messages from inside Discord, via kimaki.dev
|
|
|
279
279
|
|
|
280
280
|
## bash tool
|
|
281
281
|
|
|
282
|
-
When calling the bash tool, always include
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
282
|
+
When calling the bash tool, always include these extra fields alongside \`command\`:
|
|
283
|
+
|
|
284
|
+
\`\`\`ts
|
|
285
|
+
interface BashToolInput {
|
|
286
|
+
command: string
|
|
287
|
+
/** Short 5-10 word summary of what this command does */
|
|
288
|
+
description: string
|
|
289
|
+
/** true if the command writes files, modifies state, installs packages, or triggers external effects */
|
|
290
|
+
hasSideEffect: boolean
|
|
291
|
+
workdir?: string
|
|
292
|
+
timeout?: number
|
|
293
|
+
}
|
|
294
|
+
\`\`\`
|
|
295
|
+
|
|
296
|
+
\`description\` is shown to the user in Discord as a summary of the bash call.
|
|
297
|
+
\`hasSideEffect\` distinguishes essential bash calls from read-only ones in low-verbosity mode.
|
|
286
298
|
|
|
287
299
|
Your current OpenCode session ID is: ${sessionId}${channelId ? `\nYour current Discord channel ID is: ${channelId}` : ''}${threadId ? `\nYour current Discord thread ID is: ${threadId}` : ''}${guildId ? `\nYour current Discord guild ID is: ${guildId}` : ''}
|
|
288
300
|
|
|
@@ -34,10 +34,22 @@ describe('system-message', () => {
|
|
|
34
34
|
|
|
35
35
|
## bash tool
|
|
36
36
|
|
|
37
|
-
When calling the bash tool, always include
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
When calling the bash tool, always include these extra fields alongside \`command\`:
|
|
38
|
+
|
|
39
|
+
\`\`\`ts
|
|
40
|
+
interface BashToolInput {
|
|
41
|
+
command: string
|
|
42
|
+
/** Short 5-10 word summary of what this command does */
|
|
43
|
+
description: string
|
|
44
|
+
/** true if the command writes files, modifies state, installs packages, or triggers external effects */
|
|
45
|
+
hasSideEffect: boolean
|
|
46
|
+
workdir?: string
|
|
47
|
+
timeout?: number
|
|
48
|
+
}
|
|
49
|
+
\`\`\`
|
|
50
|
+
|
|
51
|
+
\`description\` is shown to the user in Discord as a summary of the bash call.
|
|
52
|
+
\`hasSideEffect\` distinguishes essential bash calls from read-only ones in low-verbosity mode.
|
|
41
53
|
|
|
42
54
|
Your current OpenCode session ID is: ses_123
|
|
43
55
|
Your current Discord channel ID is: chan_123
|
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.1",
|
|
6
6
|
"repository": "https://github.com/remorses/kimaki",
|
|
7
7
|
"bin": "bin.js",
|
|
8
8
|
"files": [
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"tsx": "^4.20.5",
|
|
26
26
|
"undici": "^8.0.2",
|
|
27
27
|
"discord-digital-twin": "^0.1.0",
|
|
28
|
-
"opencode-cached-provider": "^0.0.1",
|
|
29
28
|
"opencode-deterministic-provider": "^0.0.1",
|
|
29
|
+
"opencode-cached-provider": "^0.0.1",
|
|
30
30
|
"db": "^0.0.0"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"zod": "^4.3.6",
|
|
64
64
|
"zustand": "^5.0.11",
|
|
65
65
|
"errore": "^0.14.1",
|
|
66
|
-
"libsqlproxy": "^0.1.0",
|
|
67
66
|
"opencode-injection-guard": "^0.2.1",
|
|
67
|
+
"libsqlproxy": "^0.1.0",
|
|
68
68
|
"traforo": "^0.7.1"
|
|
69
69
|
},
|
|
70
70
|
"optionalDependencies": {
|
package/skills/egaki/SKILL.md
CHANGED
|
@@ -54,6 +54,23 @@ The `--model` / `-m` flag is **optional** on both `egaki image` and `egaki video
|
|
|
54
54
|
|
|
55
55
|
Agents should always pass `-m` explicitly to avoid the interactive picker.
|
|
56
56
|
|
|
57
|
+
### Preferred image models
|
|
58
|
+
|
|
59
|
+
When the user does not specify a model, prefer these in order:
|
|
60
|
+
|
|
61
|
+
1. **`gpt-image-1`** (or `gpt-image-2` / `chatgpt-image-latest` if available)
|
|
62
|
+
2. **`grok-imagine-image`** (xAI Grok)
|
|
63
|
+
3. **`nano-banana-pro-preview`**
|
|
64
|
+
|
|
65
|
+
Always try the first available model. Fall back to the next if auth is missing
|
|
66
|
+
for the preferred one.
|
|
67
|
+
|
|
68
|
+
### Preferred video models
|
|
69
|
+
|
|
70
|
+
When the user does not specify a video model, prefer:
|
|
71
|
+
|
|
72
|
+
1. **`grok-imagine-video-1.5`** (xAI Grok)
|
|
73
|
+
|
|
57
74
|
## Example commands
|
|
58
75
|
|
|
59
76
|
```bash
|
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
|
@@ -25,6 +25,19 @@ sigillo --help
|
|
|
25
25
|
|
|
26
26
|
**NEVER truncate this either.**
|
|
27
27
|
|
|
28
|
+
If a flag or command documented here is missing from `--help`, the installed binary is likely outdated. Update before proceeding:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm i -g sigillo@latest
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If sigillo is symlinked to a local dev build (check with `which sigillo`), rebuild from source instead:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cd /path/to/sigillo/cli && zig build -Dtarget=aarch64-macos
|
|
38
|
+
cp zig-out/bin/sigillo dist/darwin-arm64/sigillo
|
|
39
|
+
```
|
|
40
|
+
|
|
28
41
|
## New project setup workflow
|
|
29
42
|
|
|
30
43
|
This mirrors the Doppler workflow: check auth → link project → list secrets → run.
|
|
@@ -88,6 +101,16 @@ To read a specific value:
|
|
|
88
101
|
sigillo secrets get DATABASE_URL
|
|
89
102
|
```
|
|
90
103
|
|
|
104
|
+
`secrets get` returns YAML by default. Pass `--raw` to output only the raw value (no YAML wrapping). When stdout is piped, `--raw` is implied automatically:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# copy a secret between environments (piped stdout auto-enables raw mode)
|
|
108
|
+
sigillo secrets get DATABASE_URL -c dev | sigillo secrets set DATABASE_URL -c preview
|
|
109
|
+
|
|
110
|
+
# force raw output in a terminal (useful for scripting)
|
|
111
|
+
sigillo secrets get DATABASE_URL --raw
|
|
112
|
+
```
|
|
113
|
+
|
|
91
114
|
**5. Run your app with secrets injected:**
|
|
92
115
|
|
|
93
116
|
```bash
|
|
@@ -113,11 +136,11 @@ Never read a secret value into the agent context window or pass it as plain text
|
|
|
113
136
|
**Copying a secret from one env to another:**
|
|
114
137
|
|
|
115
138
|
```bash
|
|
116
|
-
#
|
|
139
|
+
# piped stdout auto-enables raw mode, value never seen by the agent
|
|
117
140
|
sigillo secrets get DATABASE_URL -c dev | sigillo secrets set DATABASE_URL -c preview
|
|
118
141
|
```
|
|
119
142
|
|
|
120
|
-
The same pattern works for any secret copy
|
|
143
|
+
The same pattern works for any secret copy, between environments, or when seeding a new environment from an existing one.
|
|
121
144
|
|
|
122
145
|
### Never read `.env` files or `~/.sigillo/*`
|
|
123
146
|
|
|
@@ -145,14 +168,22 @@ If a `package.json` script requires secrets, embed `sigillo run` directly in the
|
|
|
145
168
|
{
|
|
146
169
|
"scripts": {
|
|
147
170
|
"dev": "sigillo run -c dev -- vite dev",
|
|
148
|
-
"
|
|
149
|
-
"
|
|
171
|
+
"deploy": "pnpm db:migrate:preview && CLOUDFLARE_ENV=preview sigillo run -c preview --command 'tsc && vite build && wrangler deploy --env preview'",
|
|
172
|
+
"deploy:prod": "pnpm db:migrate:prod && sigillo run -c prod --command 'tsc && vite build && wrangler deploy'"
|
|
150
173
|
}
|
|
151
174
|
}
|
|
152
175
|
```
|
|
153
176
|
|
|
177
|
+
These scripts work because pnpm adds `node_modules/.bin` to PATH before executing the script string, and sigillo inherits that PATH. Commands like `tsc`, `vite`, and `wrangler` resolve correctly inside `--command` as long as the entry point is `pnpm run deploy`.
|
|
178
|
+
|
|
154
179
|
Use `-c dev` for local development, `-c preview` for staging or preview deployments, and `-c prod` or the repo's production slug for production.
|
|
155
180
|
|
|
181
|
+
### Always run `sigillo run` via pnpm scripts, not directly
|
|
182
|
+
|
|
183
|
+
`sigillo run --command` spawns `$SHELL -c '...'` and passes the inherited `PATH`. But pnpm only adds `node_modules/.bin` to `PATH` when it's running a `package.json` script. If you call `sigillo run --command 'tsc && vite build'` directly from the terminal, `tsc` and `vite` won't be found because `node_modules/.bin` is not in PATH.
|
|
184
|
+
|
|
185
|
+
Always invoke deploy/build commands through `pnpm run <script>` so pnpm augments PATH before sigillo inherits it. If you must run sigillo directly, use `pnpm exec` to resolve binaries: `sigillo run --command 'pnpm exec tsc && pnpm exec vite build'`.
|
|
186
|
+
|
|
156
187
|
### Use `--command` for shell expansion
|
|
157
188
|
|
|
158
189
|
Use direct argv mode when no shell syntax is needed:
|
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.
|
|
@@ -5,7 +5,7 @@ import { note } from '@clack/prompts'
|
|
|
5
5
|
import YAML from 'yaml'
|
|
6
6
|
import * as errore from 'errore'
|
|
7
7
|
import type { OpencodeClient, Event as OpenCodeEvent } from '@opencode-ai/sdk/v2'
|
|
8
|
-
import { Events, ActivityType, type PresenceStatusData, type Guild, Routes } from 'discord.js'
|
|
8
|
+
import { Events, ActivityType, type PresenceStatusData, type Guild, type Client, Routes } from 'discord.js'
|
|
9
9
|
import path from 'node:path'
|
|
10
10
|
import fs from 'node:fs'
|
|
11
11
|
import { fileURLToPath } from 'node:url'
|
|
@@ -98,74 +98,7 @@ cli
|
|
|
98
98
|
|
|
99
99
|
cliLogger.log('Finding guild...')
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
let guild: Guild
|
|
103
|
-
if (options.guild) {
|
|
104
|
-
const guildId = String(options.guild)
|
|
105
|
-
const foundGuild = client.guilds.cache.get(guildId)
|
|
106
|
-
if (!foundGuild) {
|
|
107
|
-
cliLogger.log('Guild not found')
|
|
108
|
-
cliLogger.error(`Guild not found: ${guildId}`)
|
|
109
|
-
void client.destroy()
|
|
110
|
-
process.exit(EXIT_NO_RESTART)
|
|
111
|
-
}
|
|
112
|
-
guild = foundGuild
|
|
113
|
-
} else {
|
|
114
|
-
const existingChannelId = await (await getDb()).query.channel_directories.findFirst({
|
|
115
|
-
where: { channel_type: 'text' },
|
|
116
|
-
orderBy: { created_at: 'desc' },
|
|
117
|
-
columns: { channel_id: true },
|
|
118
|
-
}).then((row) => row?.channel_id)
|
|
119
|
-
|
|
120
|
-
if (existingChannelId) {
|
|
121
|
-
try {
|
|
122
|
-
const ch = await client.channels.fetch(existingChannelId)
|
|
123
|
-
if (ch && !ch.isDMBased()) {
|
|
124
|
-
guild = ch.guild
|
|
125
|
-
} else {
|
|
126
|
-
throw new Error('Channel has no guild')
|
|
127
|
-
}
|
|
128
|
-
} catch (error) {
|
|
129
|
-
cliLogger.debug(
|
|
130
|
-
'Failed to fetch existing channel while selecting guild:',
|
|
131
|
-
error instanceof Error ? error.stack : String(error),
|
|
132
|
-
)
|
|
133
|
-
let firstGuild = client.guilds.cache.first()
|
|
134
|
-
if (!firstGuild) {
|
|
135
|
-
// Cache might be empty, try fetching guilds from API
|
|
136
|
-
const fetched = await client.guilds.fetch()
|
|
137
|
-
const firstOAuth2Guild = fetched.first()
|
|
138
|
-
if (firstOAuth2Guild) {
|
|
139
|
-
firstGuild = await client.guilds.fetch(firstOAuth2Guild.id)
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (!firstGuild) {
|
|
143
|
-
cliLogger.log('No guild found')
|
|
144
|
-
cliLogger.error('No guild found. Add the bot to a server first.')
|
|
145
|
-
void client.destroy()
|
|
146
|
-
process.exit(EXIT_NO_RESTART)
|
|
147
|
-
}
|
|
148
|
-
guild = firstGuild
|
|
149
|
-
}
|
|
150
|
-
} else {
|
|
151
|
-
let firstGuild = client.guilds.cache.first()
|
|
152
|
-
if (!firstGuild) {
|
|
153
|
-
// Cache might be empty, try fetching guilds from API
|
|
154
|
-
const fetched = await client.guilds.fetch()
|
|
155
|
-
const firstOAuth2Guild = fetched.first()
|
|
156
|
-
if (firstOAuth2Guild) {
|
|
157
|
-
firstGuild = await client.guilds.fetch(firstOAuth2Guild.id)
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if (!firstGuild) {
|
|
161
|
-
cliLogger.log('No guild found')
|
|
162
|
-
cliLogger.error('No guild found. Add the bot to a server first.')
|
|
163
|
-
void client.destroy()
|
|
164
|
-
process.exit(EXIT_NO_RESTART)
|
|
165
|
-
}
|
|
166
|
-
guild = firstGuild
|
|
167
|
-
}
|
|
168
|
-
}
|
|
101
|
+
const guild = await resolveGuildForProjectCommand({ client, guildIdOverride: options.guild })
|
|
169
102
|
|
|
170
103
|
// Check if channel already exists in this guild
|
|
171
104
|
cliLogger.log('Checking for existing channel...')
|
|
@@ -466,24 +399,7 @@ cli
|
|
|
466
399
|
client.login(botToken).catch(reject)
|
|
467
400
|
})
|
|
468
401
|
|
|
469
|
-
|
|
470
|
-
if (options.guild) {
|
|
471
|
-
const found = client.guilds.cache.get(options.guild)
|
|
472
|
-
if (!found) {
|
|
473
|
-
cliLogger.error(`Guild not found: ${options.guild}`)
|
|
474
|
-
void client.destroy()
|
|
475
|
-
process.exit(EXIT_NO_RESTART)
|
|
476
|
-
}
|
|
477
|
-
guild = found
|
|
478
|
-
} else {
|
|
479
|
-
const first = client.guilds.cache.first()
|
|
480
|
-
if (!first) {
|
|
481
|
-
cliLogger.error('No guild found. Add the bot to a server first.')
|
|
482
|
-
void client.destroy()
|
|
483
|
-
process.exit(EXIT_NO_RESTART)
|
|
484
|
-
}
|
|
485
|
-
guild = first
|
|
486
|
-
}
|
|
402
|
+
const guild = await resolveGuildForProjectCommand({ client, guildIdOverride: options.guild })
|
|
487
403
|
|
|
488
404
|
const { textChannelId, channelName } = await createProjectChannels({
|
|
489
405
|
guild,
|
|
@@ -505,4 +421,100 @@ cli
|
|
|
505
421
|
})
|
|
506
422
|
|
|
507
423
|
|
|
424
|
+
// Resolve the guild for project add/create commands. In gateway mode the
|
|
425
|
+
// guild cache only contains authorized guilds, so picking from cache is safe.
|
|
426
|
+
// The old approach fetched an existing channel to infer the guild, but that
|
|
427
|
+
// breaks when the channel belongs to a different guild (e.g. old self-hosted
|
|
428
|
+
// bot channels) and the gateway proxy rejects the REST call. This led to a
|
|
429
|
+
// non-deterministic fallback that picked the wrong guild.
|
|
430
|
+
async function resolveGuildForProjectCommand({ client, guildIdOverride }: { client: Client; guildIdOverride?: string }): Promise<Guild> {
|
|
431
|
+
if (guildIdOverride) {
|
|
432
|
+
const found = client.guilds.cache.get(guildIdOverride)
|
|
433
|
+
if (!found) {
|
|
434
|
+
cliLogger.error(`Guild not found: ${guildIdOverride}`)
|
|
435
|
+
void client.destroy()
|
|
436
|
+
process.exit(EXIT_NO_RESTART)
|
|
437
|
+
}
|
|
438
|
+
return found
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Try existing channel lookup to find the guild the user already has channels in.
|
|
442
|
+
// This handles multi-guild setups where we want to add to the same guild.
|
|
443
|
+
const db = await getDb()
|
|
444
|
+
const existingChannels = await db.query.channel_directories.findMany({
|
|
445
|
+
where: { channel_type: 'text' },
|
|
446
|
+
orderBy: { created_at: 'desc' },
|
|
447
|
+
columns: { channel_id: true },
|
|
448
|
+
limit: 20,
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
// Log available guilds for debugging guild selection issues
|
|
452
|
+
const cachedGuilds = Array.from(client.guilds.cache.values())
|
|
453
|
+
cliLogger.debug(`Guilds in cache (${cachedGuilds.length}): ${cachedGuilds.map((g) => `${g.name} (${g.id})`).join(', ')}`)
|
|
454
|
+
|
|
455
|
+
// When multiple guilds are available, find which guild has the most
|
|
456
|
+
// existing channels. The user's main guild will have far more channels
|
|
457
|
+
// than a test/demo guild.
|
|
458
|
+
const guildHits = new Map<string, { guild: Guild; count: number }>()
|
|
459
|
+
for (const row of existingChannels) {
|
|
460
|
+
try {
|
|
461
|
+
const ch = await client.channels.fetch(row.channel_id)
|
|
462
|
+
if (ch && !ch.isDMBased()) {
|
|
463
|
+
const entry = guildHits.get(ch.guild.id)
|
|
464
|
+
if (entry) {
|
|
465
|
+
entry.count++
|
|
466
|
+
} else {
|
|
467
|
+
guildHits.set(ch.guild.id, { guild: ch.guild, count: 1 })
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} catch {
|
|
471
|
+
// Channel might be in a different guild (gateway proxy rejects) or deleted, skip
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (guildHits.size > 0) {
|
|
476
|
+
// Pick the guild with the most channels
|
|
477
|
+
const best = Array.from(guildHits.values()).sort((a, b) => b.count - a.count)[0]!
|
|
478
|
+
cliLogger.debug(
|
|
479
|
+
`Guild channel counts: ${Array.from(guildHits.values()).map((e) => `${e.guild.name} (${e.guild.id}): ${e.count}`).join(', ')}`,
|
|
480
|
+
)
|
|
481
|
+
cliLogger.debug(`Selected guild: ${best.guild.name} (${best.guild.id}) with ${best.count} channels`)
|
|
482
|
+
return best.guild
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
cliLogger.debug('Could not resolve guild from existing channels, falling back to cache')
|
|
486
|
+
|
|
487
|
+
// If only one guild in cache, use it directly (common case).
|
|
488
|
+
// If multiple guilds, error out and ask the user to specify --guild
|
|
489
|
+
// since we can't determine which one to use.
|
|
490
|
+
if (cachedGuilds.length === 1) {
|
|
491
|
+
return cachedGuilds[0]!
|
|
492
|
+
}
|
|
493
|
+
if (cachedGuilds.length > 1) {
|
|
494
|
+
cliLogger.error(
|
|
495
|
+
`Multiple guilds found. Use --guild to specify which one:\n${cachedGuilds.map((g) => ` ${g.id} ${g.name}`).join('\n')}`,
|
|
496
|
+
)
|
|
497
|
+
void client.destroy()
|
|
498
|
+
process.exit(EXIT_NO_RESTART)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Cache empty, try fetching
|
|
502
|
+
const fetched = await client.guilds.fetch()
|
|
503
|
+
if (fetched.size === 1) {
|
|
504
|
+
const firstOAuth2Guild = fetched.first()!
|
|
505
|
+
return await client.guilds.fetch(firstOAuth2Guild.id)
|
|
506
|
+
}
|
|
507
|
+
if (fetched.size > 1) {
|
|
508
|
+
cliLogger.error(
|
|
509
|
+
`Multiple guilds found. Use --guild to specify which one:\n${Array.from(fetched.values()).map((g) => ` ${g.id} ${g.name}`).join('\n')}`,
|
|
510
|
+
)
|
|
511
|
+
void client.destroy()
|
|
512
|
+
process.exit(EXIT_NO_RESTART)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
cliLogger.error('No guild found. Add the bot to a server first.')
|
|
516
|
+
void client.destroy()
|
|
517
|
+
process.exit(EXIT_NO_RESTART)
|
|
518
|
+
}
|
|
519
|
+
|
|
508
520
|
export default cli
|