@onmars/lunar-core 0.1.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 +13 -0
- package/package.json +32 -0
- package/src/__tests__/clear-command.test.ts +214 -0
- package/src/__tests__/command-handler.test.ts +169 -0
- package/src/__tests__/compact-command.test.ts +80 -0
- package/src/__tests__/config-command.test.ts +240 -0
- package/src/__tests__/config-loader.test.ts +1512 -0
- package/src/__tests__/config.test.ts +429 -0
- package/src/__tests__/cron-command.test.ts +418 -0
- package/src/__tests__/cron-parser.test.ts +259 -0
- package/src/__tests__/daemon.test.ts +346 -0
- package/src/__tests__/dedup.test.ts +404 -0
- package/src/__tests__/e2e-sanitization.ts +168 -0
- package/src/__tests__/e2e-skill-loader.test.ts +176 -0
- package/src/__tests__/fixtures/AGENTS.md +4 -0
- package/src/__tests__/fixtures/IDENTITY.md +2 -0
- package/src/__tests__/fixtures/SOUL.md +3 -0
- package/src/__tests__/fixtures/moons/athena/IDENTITY.md +2 -0
- package/src/__tests__/fixtures/moons/athena/SOUL.md +3 -0
- package/src/__tests__/fixtures/moons/hermes/SOUL.md +3 -0
- package/src/__tests__/fixtures/skills/brain/SKILL.md +6 -0
- package/src/__tests__/fixtures/skills/empty/SKILL.md +3 -0
- package/src/__tests__/fixtures/skills/multiline/SKILL.md +7 -0
- package/src/__tests__/fixtures/skills/no-desc/SKILL.md +5 -0
- package/src/__tests__/fixtures/skills/notion/SKILL.md +6 -0
- package/src/__tests__/fixtures/skills/quoted/SKILL.md +6 -0
- package/src/__tests__/hook-runner.test.ts +1689 -0
- package/src/__tests__/input-sanitization.test.ts +367 -0
- package/src/__tests__/logger.test.ts +163 -0
- package/src/__tests__/memory-orchestrator.test.ts +552 -0
- package/src/__tests__/model-catalog.test.ts +215 -0
- package/src/__tests__/model-command.test.ts +185 -0
- package/src/__tests__/moon-loader.test.ts +398 -0
- package/src/__tests__/ping-command.test.ts +85 -0
- package/src/__tests__/plugin.test.ts +258 -0
- package/src/__tests__/remind-command.test.ts +368 -0
- package/src/__tests__/reset-command.test.ts +92 -0
- package/src/__tests__/router.test.ts +1246 -0
- package/src/__tests__/scheduler.test.ts +469 -0
- package/src/__tests__/security.test.ts +214 -0
- package/src/__tests__/session-meta.test.ts +101 -0
- package/src/__tests__/session-tracker.test.ts +389 -0
- package/src/__tests__/session.test.ts +241 -0
- package/src/__tests__/skill-loader.test.ts +153 -0
- package/src/__tests__/status-command.test.ts +153 -0
- package/src/__tests__/stop-command.test.ts +60 -0
- package/src/__tests__/think-command.test.ts +146 -0
- package/src/__tests__/usage-api.test.ts +222 -0
- package/src/__tests__/usage-command-api-fail.test.ts +48 -0
- package/src/__tests__/usage-command-no-oauth.test.ts +48 -0
- package/src/__tests__/usage-command.test.ts +173 -0
- package/src/__tests__/whoami-command.test.ts +124 -0
- package/src/index.ts +122 -0
- package/src/lib/command-handler.ts +135 -0
- package/src/lib/commands/clear.ts +69 -0
- package/src/lib/commands/compact.ts +14 -0
- package/src/lib/commands/config-show.ts +49 -0
- package/src/lib/commands/cron.ts +118 -0
- package/src/lib/commands/help.ts +26 -0
- package/src/lib/commands/model.ts +71 -0
- package/src/lib/commands/ping.ts +24 -0
- package/src/lib/commands/remind.ts +75 -0
- package/src/lib/commands/status.ts +118 -0
- package/src/lib/commands/stop.ts +18 -0
- package/src/lib/commands/think.ts +42 -0
- package/src/lib/commands/usage.ts +56 -0
- package/src/lib/commands/whoami.ts +23 -0
- package/src/lib/config-loader.ts +1449 -0
- package/src/lib/config.ts +202 -0
- package/src/lib/cron-parser.ts +388 -0
- package/src/lib/daemon.ts +216 -0
- package/src/lib/dedup.ts +414 -0
- package/src/lib/hook-runner.ts +1270 -0
- package/src/lib/logger.ts +55 -0
- package/src/lib/memory-orchestrator.ts +415 -0
- package/src/lib/model-catalog.ts +240 -0
- package/src/lib/moon-loader.ts +291 -0
- package/src/lib/plugin.ts +148 -0
- package/src/lib/router.ts +1135 -0
- package/src/lib/scheduler.ts +422 -0
- package/src/lib/security.ts +259 -0
- package/src/lib/session-tracker.ts +222 -0
- package/src/lib/session.ts +158 -0
- package/src/lib/skill-loader.ts +166 -0
- package/src/lib/usage-api.ts +145 -0
- package/src/types/agent.ts +86 -0
- package/src/types/channel.ts +93 -0
- package/src/types/index.ts +32 -0
- package/src/types/memory.ts +92 -0
- package/src/types/moon.ts +56 -0
- package/src/types/voice.ts +74 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /model — View or change the active model at runtime.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* /model — Show current model and available aliases
|
|
6
|
+
* /model <alias> — Set model override for this session
|
|
7
|
+
* /model clear — Remove override, revert to configured model
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CommandContext } from '../command-handler'
|
|
11
|
+
import { getModelInfo } from '../model-catalog'
|
|
12
|
+
|
|
13
|
+
/** Well-known aliases for user convenience */
|
|
14
|
+
const ALIASES: Record<string, string> = {
|
|
15
|
+
opus: 'claude-opus-4-6',
|
|
16
|
+
sonnet: 'claude-sonnet-4-6',
|
|
17
|
+
'sonnet-4.5': 'claude-sonnet-4-5',
|
|
18
|
+
haiku: 'claude-haiku-4-5',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function modelCommand(args: string, ctx: CommandContext): string {
|
|
22
|
+
const requestedModel = args.trim()
|
|
23
|
+
|
|
24
|
+
// No arg: show current model + available aliases
|
|
25
|
+
if (!requestedModel) {
|
|
26
|
+
const lines: string[] = []
|
|
27
|
+
lines.push('**Current Model**')
|
|
28
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
29
|
+
|
|
30
|
+
const meta = ctx.getSessionMeta?.()
|
|
31
|
+
const sessionOverride = meta?.modelOverride as string | undefined
|
|
32
|
+
const effective = sessionOverride ?? ctx.channelPersona?.model ?? ctx.moon?.model ?? 'default'
|
|
33
|
+
const info = getModelInfo(effective)
|
|
34
|
+
|
|
35
|
+
lines.push(`**Active:** ${info.displayName} (\`${effective}\`)`)
|
|
36
|
+
if (sessionOverride) {
|
|
37
|
+
lines.push('_Override active for this session. Use `/model clear` to revert._')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
lines.push('')
|
|
41
|
+
lines.push('**Available aliases:**')
|
|
42
|
+
for (const [alias, full] of Object.entries(ALIASES)) {
|
|
43
|
+
const aInfo = getModelInfo(full)
|
|
44
|
+
lines.push(` \`${alias}\` → ${aInfo.displayName}`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return lines.join('\n')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// "clear" / "reset" subcommand: remove override
|
|
51
|
+
if (requestedModel === 'clear' || requestedModel === 'reset') {
|
|
52
|
+
if (!ctx.updateSessionMeta) {
|
|
53
|
+
return 'Session metadata not available.'
|
|
54
|
+
}
|
|
55
|
+
ctx.updateSessionMeta({ modelOverride: null })
|
|
56
|
+
const effective = ctx.channelPersona?.model ?? ctx.moon?.model ?? 'default'
|
|
57
|
+
return `Model override cleared. Reverted to: \`${effective}\``
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Set model override
|
|
61
|
+
if (!ctx.updateSessionMeta) {
|
|
62
|
+
return 'Session metadata not available.'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const resolved = ALIASES[requestedModel.toLowerCase()] ?? requestedModel
|
|
66
|
+
const info = getModelInfo(resolved)
|
|
67
|
+
|
|
68
|
+
ctx.updateSessionMeta({ modelOverride: resolved })
|
|
69
|
+
|
|
70
|
+
return `Model set to **${info.displayName}** (\`${resolved}\`) for this session.\nUse \`/model clear\` to revert.`
|
|
71
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /ping — Quick latency check and daemon uptime.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CommandContext } from '../command-handler'
|
|
6
|
+
import { formatDuration } from '../session-tracker'
|
|
7
|
+
|
|
8
|
+
export function pingCommand(_args: string, ctx: CommandContext): string {
|
|
9
|
+
const now = Date.now()
|
|
10
|
+
const latencyMs = now - ctx.message.timestamp.getTime()
|
|
11
|
+
|
|
12
|
+
const lines: string[] = []
|
|
13
|
+
lines.push('**Pong!**')
|
|
14
|
+
|
|
15
|
+
if (latencyMs >= 0 && latencyMs < 60_000) {
|
|
16
|
+
lines.push(`Latency: ${latencyMs}ms`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (ctx.daemonStartedAt > 0) {
|
|
20
|
+
lines.push(`Uptime: ${formatDuration(now - ctx.daemonStartedAt)}`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return lines.join('\n')
|
|
24
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { CommandContext, CommandFn } from '../command-handler'
|
|
2
|
+
import type { Job } from '../scheduler'
|
|
3
|
+
|
|
4
|
+
export const remindCommand: CommandFn = async (args: string, ctx: CommandContext) => {
|
|
5
|
+
if (!ctx.scheduler) {
|
|
6
|
+
return '❌ Scheduler is not enabled in config.yaml'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (!args.trim()) {
|
|
10
|
+
return 'Usage: `/remind <time> <message>`\nExamples:\n- `/remind in 15m Turn off oven`\n- `/remind in 2h Check email`\n- `/remind 2026-03-01T15:30 Meeting time`\n- `/remind 14:30 Daily summary`'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const match = args.match(
|
|
14
|
+
/^(in\s+\d+[mh]|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(?:Z|[+-]\d{2}:\d{2})?|\d{2}:\d{2})\s+(.+)$/i,
|
|
15
|
+
)
|
|
16
|
+
if (!match) {
|
|
17
|
+
return '❌ Invalid format. Use `in 15m <msg>`, `in 2h <msg>`, `HH:MM <msg>` or `YYYY-MM-DDTHH:MMZ <msg>`.'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let atTime: Date
|
|
21
|
+
const timeStr = match[1]
|
|
22
|
+
const message = match[3] // match[3] since the optional (:ss)? is 2, wait, let's just split by the first space.
|
|
23
|
+
|
|
24
|
+
// Re-parse to avoid capturing group index issues
|
|
25
|
+
const firstSpace = args.search(/\s+(.+)/)
|
|
26
|
+
let rawTime = args.trim()
|
|
27
|
+
let rawMsg = ''
|
|
28
|
+
|
|
29
|
+
if (args.toLowerCase().startsWith('in ')) {
|
|
30
|
+
const nextSpace = args.indexOf(' ', 3)
|
|
31
|
+
rawTime = args.substring(0, nextSpace)
|
|
32
|
+
rawMsg = args.substring(nextSpace + 1)
|
|
33
|
+
} else {
|
|
34
|
+
const nextSpace = args.indexOf(' ')
|
|
35
|
+
rawTime = args.substring(0, nextSpace)
|
|
36
|
+
rawMsg = args.substring(nextSpace + 1)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (rawTime.toLowerCase().startsWith('in ')) {
|
|
40
|
+
const amountStr = rawTime.substring(3).trim()
|
|
41
|
+
const amount = Number.parseInt(amountStr.slice(0, -1), 10)
|
|
42
|
+
const unit = amountStr.slice(-1).toLowerCase()
|
|
43
|
+
const ms = unit === 'h' ? amount * 3600000 : amount * 60000
|
|
44
|
+
atTime = new Date(Date.now() + ms)
|
|
45
|
+
} else if (rawTime.includes('T')) {
|
|
46
|
+
atTime = new Date(rawTime)
|
|
47
|
+
} else {
|
|
48
|
+
// HH:MM today
|
|
49
|
+
const [h, m] = rawTime.split(':').map(Number)
|
|
50
|
+
atTime = new Date()
|
|
51
|
+
atTime.setHours(h, m, 0, 0)
|
|
52
|
+
if (atTime.getTime() < Date.now()) {
|
|
53
|
+
// If time already passed today, assume tomorrow
|
|
54
|
+
atTime.setDate(atTime.getDate() + 1)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (Number.isNaN(atTime.getTime())) {
|
|
59
|
+
return '❌ Invalid time format.'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const job: Job = {
|
|
63
|
+
id: crypto.randomUUID(),
|
|
64
|
+
schedule: { kind: 'at', at: atTime.toISOString() },
|
|
65
|
+
payload: { kind: 'message', text: rawMsg.trim() },
|
|
66
|
+
channel: ctx.channelPersona?.name || 'orbit',
|
|
67
|
+
enabled: true,
|
|
68
|
+
oneShot: true,
|
|
69
|
+
createdAt: Math.floor(Date.now() / 1000),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
ctx.scheduler.getStore().add(job)
|
|
73
|
+
|
|
74
|
+
return `✅ Reminder set for **${atTime.toLocaleString()}**`
|
|
75
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /status — Session status command.
|
|
3
|
+
*
|
|
4
|
+
* Displays real-time session metrics:
|
|
5
|
+
* - Model, channel info
|
|
6
|
+
* - Tokens: actual new tokens (non-cached input + output)
|
|
7
|
+
* - Cache: hit rate, cached reads, new cache writes
|
|
8
|
+
* - Context: window usage from last API call
|
|
9
|
+
* - Account quota: 5h/7d usage windows (from Anthropic API)
|
|
10
|
+
* - Cost, duration, memory, uptime
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { CommandContext } from '../command-handler'
|
|
14
|
+
import { getModelInfo } from '../model-catalog'
|
|
15
|
+
import { buildProgressBar, formatDuration, formatTokens } from '../session-tracker'
|
|
16
|
+
|
|
17
|
+
export function statusCommand(_args: string, ctx: CommandContext): string {
|
|
18
|
+
const status = ctx.tracker.getStatus(ctx.sessionKey)
|
|
19
|
+
const moonName = ctx.moon?.name ?? 'none'
|
|
20
|
+
const channelName = ctx.channelPersona?.name ?? `channel-${ctx.message.channelId.slice(-6)}`
|
|
21
|
+
const platform = ctx.channelPersona?.platform ?? 'unknown'
|
|
22
|
+
|
|
23
|
+
const lines: string[] = []
|
|
24
|
+
|
|
25
|
+
// ═══════════════════════════════════════════
|
|
26
|
+
// Header
|
|
27
|
+
// ═══════════════════════════════════════════
|
|
28
|
+
|
|
29
|
+
lines.push(`🌑 **${moonName}** — Status`)
|
|
30
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
31
|
+
|
|
32
|
+
// ═══════════════════════════════════════════
|
|
33
|
+
// System info
|
|
34
|
+
// ═══════════════════════════════════════════
|
|
35
|
+
|
|
36
|
+
// Model: show actual (from API) or configured (from moon), with context1m badge
|
|
37
|
+
const moonModel = ctx.moon?.model
|
|
38
|
+
const isContext1m = ctx.moon?.context1m === true
|
|
39
|
+
|
|
40
|
+
if (status?.usage.model) {
|
|
41
|
+
const modelInfo = getModelInfo(status.usage.model)
|
|
42
|
+
const context1mBadge = isContext1m ? ' · **1M context**' : ''
|
|
43
|
+
lines.push(`🤖 **Model:** ${modelInfo.displayName}${context1mBadge}`)
|
|
44
|
+
} else if (moonModel) {
|
|
45
|
+
const context1mBadge = isContext1m ? ' · **1M context**' : ''
|
|
46
|
+
lines.push(`🤖 **Model:** ${moonModel} (configured)${context1mBadge}`)
|
|
47
|
+
} else {
|
|
48
|
+
lines.push('🤖 **Model:** not yet determined')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
lines.push(`📡 **Channel:** #${channelName} (${platform})`)
|
|
52
|
+
|
|
53
|
+
// ═══════════════════════════════════════════
|
|
54
|
+
// Session usage
|
|
55
|
+
// ═══════════════════════════════════════════
|
|
56
|
+
|
|
57
|
+
if (status) {
|
|
58
|
+
const { usage } = status
|
|
59
|
+
const turns = usage.queryCount
|
|
60
|
+
|
|
61
|
+
// Tokens: actual new tokens (non-cached)
|
|
62
|
+
lines.push(
|
|
63
|
+
`🔢 **Tokens:** ${formatTokens(usage.inputTokens)} in / ${formatTokens(usage.outputTokens)} out`,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
// Cache: hit rate + breakdown
|
|
67
|
+
const totalCache = usage.cacheReadTokens + usage.cacheWriteTokens
|
|
68
|
+
if (totalCache > 0) {
|
|
69
|
+
const totalInput = usage.inputTokens + totalCache
|
|
70
|
+
const hitRate = totalInput > 0 ? Math.round((usage.cacheReadTokens / totalInput) * 100) : 0
|
|
71
|
+
lines.push(
|
|
72
|
+
`💾 **Cache:** ${hitRate}% hit · ${formatTokens(usage.cacheReadTokens)} cached, ${formatTokens(usage.cacheWriteTokens)} new`,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Context: actual window usage from last API call
|
|
77
|
+
const pct = Math.round(status.contextUsedPercent)
|
|
78
|
+
const bar = buildProgressBar(pct, 10)
|
|
79
|
+
lines.push(
|
|
80
|
+
`📊 **Context:** ${bar} ${formatTokens(usage.lastContextTokens)}/${formatTokens(status.contextWindowSize)} (${pct}%)`,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
// Cost estimate
|
|
84
|
+
const costStr =
|
|
85
|
+
status.estimatedCostUsd < 0.01 ? '<$0.01' : `~$${status.estimatedCostUsd.toFixed(2)}`
|
|
86
|
+
lines.push(`💰 **Cost:** ${costStr}`)
|
|
87
|
+
|
|
88
|
+
// Session info
|
|
89
|
+
lines.push(
|
|
90
|
+
`💬 **Session:** ${usage.messageCount} messages · ${turns} turn${turns !== 1 ? 's' : ''} · ${status.sessionAge}`,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
// API time
|
|
94
|
+
if (usage.totalApiDurationMs > 0) {
|
|
95
|
+
lines.push(`⏱️ **API time:** ${formatDuration(usage.totalApiDurationMs)}`)
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
lines.push('')
|
|
99
|
+
lines.push('_No queries yet in this session._')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ═══════════════════════════════════════════
|
|
103
|
+
// System footer
|
|
104
|
+
// ═══════════════════════════════════════════
|
|
105
|
+
|
|
106
|
+
const systemParts: string[] = []
|
|
107
|
+
if (ctx.memory) systemParts.push(`Memory: ${ctx.memory.id} ✅`)
|
|
108
|
+
if (ctx.daemonStartedAt > 0) {
|
|
109
|
+
systemParts.push(`Uptime: ${formatDuration(Date.now() - ctx.daemonStartedAt)}`)
|
|
110
|
+
}
|
|
111
|
+
systemParts.push(`Agent: ${ctx.agent.name}`)
|
|
112
|
+
|
|
113
|
+
if (systemParts.length > 0) {
|
|
114
|
+
lines.push(`⚙️ ${systemParts.join(' · ')}`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return lines.join('\n')
|
|
118
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /stop — Cancel the currently running query.
|
|
3
|
+
*
|
|
4
|
+
* Kills the active CLI subprocess for the agent handling this channel.
|
|
5
|
+
* The in-flight query will terminate and return an error/empty response.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CommandContext } from '../command-handler'
|
|
9
|
+
|
|
10
|
+
export function stopCommand(_args: string, ctx: CommandContext): string {
|
|
11
|
+
if (!ctx.abortAgent) {
|
|
12
|
+
return 'Stop not available for this agent.'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
ctx.abortAgent()
|
|
16
|
+
|
|
17
|
+
return 'Stopping current query...'
|
|
18
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /think — Set the thinking level for the current session.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* /think — Show current level
|
|
6
|
+
* /think <level> — Set level (off, low, medium, high)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { CommandContext } from '../command-handler'
|
|
10
|
+
|
|
11
|
+
const VALID_LEVELS = ['off', 'low', 'medium', 'high'] as const
|
|
12
|
+
|
|
13
|
+
const DESCRIPTIONS: Record<string, string> = {
|
|
14
|
+
off: 'Thinking disabled — fastest responses',
|
|
15
|
+
low: 'Brief internal reasoning',
|
|
16
|
+
medium: 'Moderate step-by-step reasoning',
|
|
17
|
+
high: 'Deep extended thinking — most thorough',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function thinkCommand(args: string, ctx: CommandContext): string {
|
|
21
|
+
const level = args.trim().toLowerCase()
|
|
22
|
+
|
|
23
|
+
// No arg: show current level
|
|
24
|
+
if (!level) {
|
|
25
|
+
const meta = ctx.getSessionMeta?.()
|
|
26
|
+
const current = (meta?.thinkingLevel as string) ?? 'default'
|
|
27
|
+
|
|
28
|
+
return `**Thinking level:** ${current}\nAvailable: ${VALID_LEVELS.join(', ')}\nUsage: \`/think <level>\``
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!(VALID_LEVELS as readonly string[]).includes(level)) {
|
|
32
|
+
return `Invalid level: \`${level}\`. Available: ${VALID_LEVELS.join(', ')}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!ctx.updateSessionMeta) {
|
|
36
|
+
return 'Session metadata not available.'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
ctx.updateSessionMeta({ thinkingLevel: level })
|
|
40
|
+
|
|
41
|
+
return `Thinking set to **${level}** for this session.\n_${DESCRIPTIONS[level]}_`
|
|
42
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /usage — Show session usage and account quota.
|
|
3
|
+
*
|
|
4
|
+
* Session: tokens, turns, cost (from SessionTracker).
|
|
5
|
+
* Account: 5h/7d rolling windows (from Anthropic OAuth API).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CommandContext } from '../command-handler'
|
|
9
|
+
import { buildProgressBar, formatDuration, formatTokens } from '../session-tracker'
|
|
10
|
+
import { fetchAccountUsage, hasOAuthCredentials } from '../usage-api'
|
|
11
|
+
|
|
12
|
+
export async function usageCommand(_args: string, ctx: CommandContext): Promise<string> {
|
|
13
|
+
const lines: string[] = []
|
|
14
|
+
const moonName = ctx.moon?.name ?? 'Lunar'
|
|
15
|
+
|
|
16
|
+
lines.push(`**${moonName}** — Usage`)
|
|
17
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
18
|
+
|
|
19
|
+
const status = ctx.tracker.getStatus(ctx.sessionKey)
|
|
20
|
+
if (status) {
|
|
21
|
+
const { usage } = status
|
|
22
|
+
lines.push('**Session:**')
|
|
23
|
+
lines.push(` Model: ${status.modelDisplayName}`)
|
|
24
|
+
lines.push(` Tokens: ${formatTokens(usage.inputTokens)} in / ${formatTokens(usage.outputTokens)} out`)
|
|
25
|
+
lines.push(` Turns: ${usage.queryCount} · Messages: ${usage.messageCount}`)
|
|
26
|
+
const costStr = status.estimatedCostUsd < 0.01 ? '<$0.01' : `~$${status.estimatedCostUsd.toFixed(2)}`
|
|
27
|
+
lines.push(` Cost: ${costStr} · Duration: ${status.sessionAge}`)
|
|
28
|
+
} else {
|
|
29
|
+
lines.push('**Session:** no queries yet')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (hasOAuthCredentials()) {
|
|
33
|
+
const account = await fetchAccountUsage()
|
|
34
|
+
if (account) {
|
|
35
|
+
lines.push('')
|
|
36
|
+
lines.push('**Account Quota:**')
|
|
37
|
+
|
|
38
|
+
if (account.fiveHour) {
|
|
39
|
+
const bar = buildProgressBar(account.fiveHour.percent, 10)
|
|
40
|
+
lines.push(` 5h: ${bar} ${account.fiveHour.percent}% (${formatTokens(account.fiveHour.used)}/${formatTokens(account.fiveHour.limit)})`)
|
|
41
|
+
}
|
|
42
|
+
if (account.sevenDay) {
|
|
43
|
+
const bar = buildProgressBar(account.sevenDay.percent, 10)
|
|
44
|
+
lines.push(` 7d: ${bar} ${account.sevenDay.percent}% (${formatTokens(account.sevenDay.used)}/${formatTokens(account.sevenDay.limit)})`)
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
lines.push('')
|
|
48
|
+
lines.push('**Account:** could not fetch quota')
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
lines.push('')
|
|
52
|
+
lines.push('**Account:** no OAuth credentials (quota unavailable)')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return lines.join('\n')
|
|
56
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /whoami — Show sender identity and current channel info.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CommandContext } from '../command-handler'
|
|
6
|
+
|
|
7
|
+
export function whoamiCommand(_args: string, ctx: CommandContext): string {
|
|
8
|
+
const { sender } = ctx.message
|
|
9
|
+
const platform = ctx.channelPersona?.platform ?? 'unknown'
|
|
10
|
+
const channel = ctx.channelPersona?.name ?? ctx.message.channelId
|
|
11
|
+
|
|
12
|
+
const lines: string[] = []
|
|
13
|
+
lines.push('**Who Am I**')
|
|
14
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
15
|
+
lines.push(`**Name:** ${sender.name}`)
|
|
16
|
+
if (sender.username) lines.push(`**Username:** ${sender.username}`)
|
|
17
|
+
lines.push(`**ID:** \`${sender.id}\``)
|
|
18
|
+
lines.push(`**Platform:** ${platform}`)
|
|
19
|
+
lines.push(`**Channel:** #${channel}`)
|
|
20
|
+
lines.push(`**Agent:** ${ctx.agent.name} (\`${ctx.agent.id}\`)`)
|
|
21
|
+
|
|
22
|
+
return lines.join('\n')
|
|
23
|
+
}
|