typeclaw 0.1.5 → 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/README.md +14 -12
- package/auth.schema.json +41 -0
- package/cron.schema.json +8 -0
- package/package.json +1 -1
- package/secrets.schema.json +41 -0
- package/src/agent/auth.ts +45 -22
- package/src/agent/index.ts +189 -19
- package/src/agent/multimodal/index.ts +12 -0
- package/src/agent/multimodal/look-at.ts +185 -0
- package/src/agent/multimodal/looker.ts +145 -0
- package/src/agent/plugin-tools.ts +30 -1
- package/src/agent/session-origin.ts +194 -46
- package/src/agent/subagents.ts +57 -1
- package/src/agent/system-prompt.ts +1 -1
- package/src/agent/tool-result-budget.ts +121 -0
- package/src/bundled-plugins/backup/index.ts +23 -8
- package/src/bundled-plugins/backup/runner.ts +22 -0
- package/src/bundled-plugins/memory/README.md +7 -4
- package/src/bundled-plugins/memory/append-tool.ts +87 -61
- package/src/bundled-plugins/memory/dreaming.ts +23 -9
- package/src/bundled-plugins/memory/find-entry-tool.ts +62 -0
- package/src/bundled-plugins/memory/fragment-parser.ts +19 -44
- package/src/bundled-plugins/memory/index.ts +91 -8
- package/src/bundled-plugins/memory/load-memory.ts +74 -34
- package/src/bundled-plugins/memory/memory-logger.ts +72 -29
- package/src/bundled-plugins/memory/migration.ts +276 -0
- package/src/bundled-plugins/memory/stream-events.ts +55 -0
- package/src/bundled-plugins/memory/stream-io.ts +63 -0
- package/src/bundled-plugins/memory/watermark.ts +48 -8
- package/src/bundled-plugins/security/index.ts +103 -10
- package/src/bundled-plugins/security/permissions.ts +12 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +51 -18
- package/src/bundled-plugins/tool-result-cap/README.md +9 -4
- package/src/bundled-plugins/tool-result-cap/cap-jsonl.ts +115 -0
- package/src/bundled-plugins/tool-result-cap/cap-result.ts +25 -13
- package/src/bundled-plugins/tool-result-cap/index.ts +16 -2
- package/src/channels/adapters/discord-bot-classify.ts +2 -6
- package/src/channels/adapters/discord-bot.ts +4 -45
- package/src/channels/adapters/kakaotalk-classify.ts +3 -7
- package/src/channels/adapters/kakaotalk.ts +28 -47
- package/src/channels/adapters/slack-bot-classify.ts +2 -6
- package/src/channels/adapters/slack-bot.ts +4 -50
- package/src/channels/adapters/telegram-bot-classify.ts +8 -10
- package/src/channels/adapters/telegram-bot.ts +3 -16
- package/src/channels/index.ts +3 -2
- package/src/channels/manager.ts +15 -1
- package/src/channels/persistence.ts +44 -10
- package/src/channels/router.ts +228 -19
- package/src/channels/schema.ts +6 -156
- package/src/cli/channel.ts +200 -4
- package/src/cli/compose-usage.ts +182 -0
- package/src/cli/compose.ts +33 -0
- package/src/cli/hostd.ts +49 -1
- package/src/cli/index.ts +4 -0
- package/src/cli/init.ts +799 -319
- package/src/cli/model.ts +244 -0
- package/src/cli/provider.ts +404 -0
- package/src/cli/reload.ts +6 -1
- package/src/cli/role.ts +156 -0
- package/src/cli/run.ts +3 -1
- package/src/cli/tui.ts +8 -1
- package/src/cli/usage-args.ts +47 -0
- package/src/cli/usage.ts +97 -0
- package/src/compose/index.ts +1 -0
- package/src/compose/usage.ts +65 -0
- package/src/config/config.ts +385 -12
- package/src/config/index.ts +7 -0
- package/src/config/models-mutation.ts +209 -0
- package/src/config/providers-mutation.ts +250 -0
- package/src/config/providers.ts +141 -2
- package/src/config/reloadable.ts +15 -4
- package/src/container/index.ts +5 -0
- package/src/container/require-running.ts +33 -0
- package/src/container/start.ts +39 -58
- package/src/cron/consumer.ts +22 -2
- package/src/cron/index.ts +45 -4
- package/src/cron/schema.ts +104 -0
- package/src/doctor/checks.ts +50 -33
- package/src/git/system-commit.ts +103 -0
- package/src/hostd/daemon.ts +16 -0
- package/src/hostd/kakao-renewal-manager.ts +223 -0
- package/src/hostd/paths.ts +7 -0
- package/src/init/dockerfile.ts +32 -6
- package/src/init/index.ts +190 -61
- package/src/init/kakaotalk-auth.ts +18 -1
- package/src/init/models-dev.ts +26 -1
- package/src/init/run-owner-claim.ts +77 -0
- package/src/permissions/builtins.ts +70 -0
- package/src/permissions/grant.ts +99 -0
- package/src/permissions/index.ts +29 -0
- package/src/permissions/match-rule.ts +305 -0
- package/src/permissions/permissions.ts +196 -0
- package/src/permissions/resolve.ts +80 -0
- package/src/permissions/schema.ts +79 -0
- package/src/plugin/context.ts +8 -4
- package/src/plugin/define.ts +2 -0
- package/src/plugin/index.ts +2 -0
- package/src/plugin/manager.ts +41 -0
- package/src/plugin/registry.ts +9 -0
- package/src/plugin/types.ts +35 -1
- package/src/role-claim/client.ts +182 -0
- package/src/role-claim/code.ts +53 -0
- package/src/role-claim/controller.ts +194 -0
- package/src/role-claim/index.ts +19 -0
- package/src/role-claim/match-rule.ts +43 -0
- package/src/role-claim/pending.ts +100 -0
- package/src/run/channel-session-factory.ts +76 -5
- package/src/run/index.ts +55 -6
- package/src/secrets/encryption.ts +116 -0
- package/src/secrets/kakao-renewal.ts +248 -0
- package/src/secrets/kakao-store.ts +66 -7
- package/src/secrets/keys.ts +173 -0
- package/src/secrets/schema.ts +23 -0
- package/src/secrets/storage.ts +68 -0
- package/src/server/index.ts +122 -11
- package/src/shared/index.ts +4 -0
- package/src/shared/protocol.ts +27 -0
- package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +3 -3
- package/src/skills/typeclaw-config/SKILL.md +38 -64
- package/src/skills/typeclaw-memory/SKILL.md +1 -1
- package/src/skills/typeclaw-permissions/SKILL.md +166 -0
- package/src/stream/types.ts +7 -1
- package/src/usage/aggregate.ts +117 -0
- package/src/usage/format.ts +30 -0
- package/src/usage/index.ts +68 -0
- package/src/usage/report.ts +354 -0
- package/src/usage/scan.ts +186 -0
- package/typeclaw.schema.json +57 -45
package/src/cli/role.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { intro, note, outro, spinner } from '@clack/prompts'
|
|
2
|
+
import { defineCommand } from 'citty'
|
|
3
|
+
|
|
4
|
+
import { requireContainerRunning, resolveHostPort, resolveTuiToken } from '@/container'
|
|
5
|
+
import { findAgentDir } from '@/init'
|
|
6
|
+
import { runClaimSession } from '@/role-claim'
|
|
7
|
+
|
|
8
|
+
import { c, errorLine } from './ui'
|
|
9
|
+
|
|
10
|
+
const claimSub = defineCommand({
|
|
11
|
+
meta: {
|
|
12
|
+
name: 'claim',
|
|
13
|
+
description:
|
|
14
|
+
'claim a channel identity (Slack/Discord/etc.) for a role on this agent. Sends a code via the host CLI; you DM that code back to the bot to prove control of the channel account.',
|
|
15
|
+
},
|
|
16
|
+
args: {
|
|
17
|
+
as: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'which role to claim (owner | member | trusted | <custom>)',
|
|
20
|
+
default: 'owner',
|
|
21
|
+
},
|
|
22
|
+
channel: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
description: 'restrict the claim to one channel adapter (slack-bot | discord-bot | telegram-bot | kakaotalk)',
|
|
25
|
+
},
|
|
26
|
+
ttl: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'how long the code stays valid, in milliseconds',
|
|
29
|
+
default: '600000',
|
|
30
|
+
},
|
|
31
|
+
url: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'agent websocket url (defaults to ws://127.0.0.1:<host port> discovered from the running container)',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
async run({ args }) {
|
|
37
|
+
const url = args.url ?? (await defaultUrl())
|
|
38
|
+
const ttlMs = Number(args.ttl)
|
|
39
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
40
|
+
console.error(errorLine(`--ttl must be a positive integer (milliseconds); got "${args.ttl}"`))
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
intro(`Claiming "${args.as}" role`)
|
|
45
|
+
|
|
46
|
+
const s = spinner()
|
|
47
|
+
s.start('Waiting for code...')
|
|
48
|
+
|
|
49
|
+
let started = false
|
|
50
|
+
const result = await runClaimSession({
|
|
51
|
+
url,
|
|
52
|
+
role: args.as,
|
|
53
|
+
ttlMs,
|
|
54
|
+
...(args.channel !== undefined ? { channel: args.channel } : {}),
|
|
55
|
+
onStarted: (payload) => {
|
|
56
|
+
started = true
|
|
57
|
+
s.stop('Ready.')
|
|
58
|
+
const expiresInSec = Math.max(0, Math.round((payload.expiresAt - Date.now()) / 1000))
|
|
59
|
+
const lines = [
|
|
60
|
+
`Send this message to your bot as a DM:`,
|
|
61
|
+
'',
|
|
62
|
+
` ${c.bold(payload.code)}`,
|
|
63
|
+
'',
|
|
64
|
+
`(expires in ${formatDuration(expiresInSec)})`,
|
|
65
|
+
]
|
|
66
|
+
note(lines.join('\n'), 'Claim code')
|
|
67
|
+
const waitMsg =
|
|
68
|
+
payload.channel !== undefined ? `Listening on ${payload.channel}...` : 'Listening on all wired channels...'
|
|
69
|
+
s.start(waitMsg)
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
if (!started) {
|
|
74
|
+
s.stop('Failed to start claim session.')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (result.kind === 'completed') {
|
|
78
|
+
s.stop(c.green(`Paired as ${result.payload.role}.`))
|
|
79
|
+
outro(`Match rule added: ${c.bold(result.payload.matchRule)}`)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
if (result.kind === 'error') {
|
|
83
|
+
s.stop(c.red(`Claim failed: ${result.payload.reason}`))
|
|
84
|
+
process.exit(1)
|
|
85
|
+
}
|
|
86
|
+
s.stop(c.red(`Claim timed out. Run "typeclaw role claim" again to retry.`))
|
|
87
|
+
process.exit(1)
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const listSub = defineCommand({
|
|
92
|
+
meta: {
|
|
93
|
+
name: 'list',
|
|
94
|
+
description: 'show the roles declared on this agent (typeclaw.json#roles)',
|
|
95
|
+
},
|
|
96
|
+
async run() {
|
|
97
|
+
const cwd = findAgentDir(process.cwd()) ?? process.cwd()
|
|
98
|
+
const { loadConfigSync } = await import('@/config')
|
|
99
|
+
const config = loadConfigSync(cwd)
|
|
100
|
+
if (!config.roles || Object.keys(config.roles).length === 0) {
|
|
101
|
+
console.log(c.dim('No roles declared. Run `typeclaw role claim` to add one, or edit typeclaw.json by hand.'))
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
for (const [name, role] of Object.entries(config.roles)) {
|
|
105
|
+
console.log(c.bold(name))
|
|
106
|
+
if (role.match.length === 0) {
|
|
107
|
+
console.log(` ${c.dim('(no match rules)')}`)
|
|
108
|
+
}
|
|
109
|
+
for (const rule of role.match) {
|
|
110
|
+
console.log(` match: ${describeRule(rule)}`)
|
|
111
|
+
}
|
|
112
|
+
if (role.permissions !== undefined) {
|
|
113
|
+
for (const perm of role.permissions) {
|
|
114
|
+
console.log(` permission: ${perm}`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
export const roleCommand = defineCommand({
|
|
122
|
+
meta: {
|
|
123
|
+
name: 'role',
|
|
124
|
+
description: 'manage role memberships on this agent',
|
|
125
|
+
},
|
|
126
|
+
subCommands: {
|
|
127
|
+
claim: () => Promise.resolve(claimSub),
|
|
128
|
+
list: () => Promise.resolve(listSub),
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
async function defaultUrl(): Promise<string> {
|
|
133
|
+
const cwd = findAgentDir(process.cwd()) ?? process.cwd()
|
|
134
|
+
const precheck = await requireContainerRunning({ cwd })
|
|
135
|
+
if (!precheck.ok) {
|
|
136
|
+
console.error(errorLine(precheck.reason))
|
|
137
|
+
process.exit(1)
|
|
138
|
+
}
|
|
139
|
+
const port = await resolveHostPort({ cwd })
|
|
140
|
+
const token = await resolveTuiToken({ cwd })
|
|
141
|
+
const url = new URL(`ws://127.0.0.1:${port}`)
|
|
142
|
+
if (token !== null) url.searchParams.set('token', token)
|
|
143
|
+
return url.toString()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function formatDuration(sec: number): string {
|
|
147
|
+
if (sec < 60) return `${sec}s`
|
|
148
|
+
const m = Math.floor(sec / 60)
|
|
149
|
+
const s = sec % 60
|
|
150
|
+
if (s === 0) return `${m}m`
|
|
151
|
+
return `${m}m ${s}s`
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function describeRule(rule: unknown): string {
|
|
155
|
+
return JSON.stringify(rule)
|
|
156
|
+
}
|
package/src/cli/run.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { CONTAINER_PORT } from '@/container'
|
|
|
4
4
|
import { isInitialized } from '@/init'
|
|
5
5
|
import { startAgent } from '@/run'
|
|
6
6
|
|
|
7
|
+
import { errorLine } from './ui'
|
|
8
|
+
|
|
7
9
|
export const run = defineCommand({
|
|
8
10
|
meta: {
|
|
9
11
|
name: 'run',
|
|
@@ -31,7 +33,7 @@ export const run = defineCommand({
|
|
|
31
33
|
},
|
|
32
34
|
async run({ args }) {
|
|
33
35
|
if (!isInitialized(process.cwd())) {
|
|
34
|
-
console.error('TypeClaw config file not found. Run `typeclaw init` first.')
|
|
36
|
+
console.error(errorLine('TypeClaw config file not found. Run `typeclaw init` first.'))
|
|
35
37
|
process.exit(1)
|
|
36
38
|
}
|
|
37
39
|
|
package/src/cli/tui.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { defineCommand } from 'citty'
|
|
2
2
|
|
|
3
|
-
import { resolveHostPort, resolveTuiToken } from '@/container'
|
|
3
|
+
import { requireContainerRunning, resolveHostPort, resolveTuiToken } from '@/container'
|
|
4
4
|
import { findAgentDir } from '@/init'
|
|
5
5
|
import { createTui } from '@/tui'
|
|
6
6
|
|
|
7
|
+
import { errorLine } from './ui'
|
|
8
|
+
|
|
7
9
|
export const tui = defineCommand({
|
|
8
10
|
meta: {
|
|
9
11
|
name: 'tui',
|
|
@@ -30,6 +32,11 @@ export const tui = defineCommand({
|
|
|
30
32
|
|
|
31
33
|
async function defaultUrl(): Promise<string> {
|
|
32
34
|
const cwd = findAgentDir(process.cwd()) ?? process.cwd()
|
|
35
|
+
const precheck = await requireContainerRunning({ cwd })
|
|
36
|
+
if (!precheck.ok) {
|
|
37
|
+
console.error(errorLine(precheck.reason))
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
33
40
|
const port = await resolveHostPort({ cwd })
|
|
34
41
|
const token = await resolveTuiToken({ cwd })
|
|
35
42
|
const url = new URL(`ws://127.0.0.1:${port}`)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { startOfDaysAgo, startOfToday } from '@/usage'
|
|
2
|
+
|
|
3
|
+
export const USAGE_COMMON_ARGS = {
|
|
4
|
+
json: {
|
|
5
|
+
type: 'boolean' as const,
|
|
6
|
+
description: 'emit the usage report as JSON',
|
|
7
|
+
default: false,
|
|
8
|
+
},
|
|
9
|
+
since: {
|
|
10
|
+
type: 'string' as const,
|
|
11
|
+
description: "ISO date or relative duration ('today', '7d', '30d')",
|
|
12
|
+
},
|
|
13
|
+
until: {
|
|
14
|
+
type: 'string' as const,
|
|
15
|
+
description: 'ISO date upper bound (exclusive)',
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function parseSince(value: unknown, command: string): number | undefined {
|
|
20
|
+
if (value === undefined || value === null) return undefined
|
|
21
|
+
if (typeof value !== 'string' || value.length === 0) return undefined
|
|
22
|
+
if (value === 'today') return startOfToday()
|
|
23
|
+
const days = /^(\d+)d$/.exec(value)
|
|
24
|
+
if (days) {
|
|
25
|
+
const n = Number(days[1])
|
|
26
|
+
if (n <= 0) exitInvalid(command, 'since', value, "duration must be at least 1 day (e.g. '1d', '7d')")
|
|
27
|
+
// `Nd` means "last N calendar days INCLUDING today" → window starts
|
|
28
|
+
// midnight (N-1) days before today.
|
|
29
|
+
return startOfDaysAgo(n - 1)
|
|
30
|
+
}
|
|
31
|
+
const ms = Date.parse(value)
|
|
32
|
+
if (Number.isFinite(ms)) return ms
|
|
33
|
+
exitInvalid(command, 'since', value, "expected ISO date, 'today', or '<n>d' (e.g. 7d)")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseUntil(value: unknown, command: string): number | undefined {
|
|
37
|
+
if (value === undefined || value === null) return undefined
|
|
38
|
+
if (typeof value !== 'string' || value.length === 0) return undefined
|
|
39
|
+
const ms = Date.parse(value)
|
|
40
|
+
if (Number.isFinite(ms)) return ms
|
|
41
|
+
exitInvalid(command, 'until', value, 'expected ISO date (e.g. 2026-05-01)')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function exitInvalid(command: string, flag: string, value: string, hint: string): never {
|
|
45
|
+
process.stderr.write(`${command}: invalid --${flag} value "${value}"; ${hint}\n`)
|
|
46
|
+
process.exit(2)
|
|
47
|
+
}
|
package/src/cli/usage.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { defineCommand } from 'citty'
|
|
2
|
+
|
|
3
|
+
import { findAgentDir } from '@/init'
|
|
4
|
+
import { runUsage } from '@/usage'
|
|
5
|
+
import { formatJson, formatReport } from '@/usage/report'
|
|
6
|
+
|
|
7
|
+
import { parseSince, parseUntil, USAGE_COMMON_ARGS } from './usage-args'
|
|
8
|
+
|
|
9
|
+
const SUBCOMMANDS = ['daily', 'session', 'models'] as const
|
|
10
|
+
type Subcommand = (typeof SUBCOMMANDS)[number]
|
|
11
|
+
type View = 'summary' | Subcommand
|
|
12
|
+
|
|
13
|
+
const COMMON_ARGS = {
|
|
14
|
+
...USAGE_COMMON_ARGS,
|
|
15
|
+
cwd: {
|
|
16
|
+
type: 'string' as const,
|
|
17
|
+
description: 'override the agent folder',
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const subcommand = (view: View, description: string) =>
|
|
22
|
+
defineCommand({
|
|
23
|
+
meta: { name: view, description },
|
|
24
|
+
args: {
|
|
25
|
+
...COMMON_ARGS,
|
|
26
|
+
...(view === 'session' ? { limit: { type: 'string' as const, description: 'max sessions (default 20)' } } : {}),
|
|
27
|
+
},
|
|
28
|
+
async run({ args }) {
|
|
29
|
+
await emit(view, args)
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export const usageCommand = defineCommand({
|
|
34
|
+
meta: {
|
|
35
|
+
name: 'usage',
|
|
36
|
+
description: 'report LLM token usage and cost for this agent folder',
|
|
37
|
+
},
|
|
38
|
+
args: COMMON_ARGS,
|
|
39
|
+
subCommands: {
|
|
40
|
+
daily: subcommand('daily', 'one row per calendar day'),
|
|
41
|
+
session: subcommand('session', 'top sessions by cost'),
|
|
42
|
+
models: subcommand('models', 'one row per provider/model'),
|
|
43
|
+
},
|
|
44
|
+
async run({ args }) {
|
|
45
|
+
// citty invokes both the matched subcommand's `run` and the parent's
|
|
46
|
+
// `run`. Suppress the summary when a subcommand was dispatched.
|
|
47
|
+
const first = args._?.[0]
|
|
48
|
+
if (typeof first === 'string' && (SUBCOMMANDS as readonly string[]).includes(first)) return
|
|
49
|
+
await emit('summary', args)
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
async function emit(view: View, args: Record<string, unknown>): Promise<void> {
|
|
54
|
+
const cwdArg = typeof args.cwd === 'string' && args.cwd.length > 0 ? args.cwd : process.cwd()
|
|
55
|
+
const agentDir = findAgentDir(cwdArg) ?? cwdArg
|
|
56
|
+
const since = parseSince(args.since, 'typeclaw usage')
|
|
57
|
+
const until = parseUntil(args.until, 'typeclaw usage')
|
|
58
|
+
const limit = parseLimit(args.limit)
|
|
59
|
+
|
|
60
|
+
const report = await runUsage({
|
|
61
|
+
agentDir,
|
|
62
|
+
...(since !== undefined ? { since } : {}),
|
|
63
|
+
...(until !== undefined ? { until } : {}),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
if (args.json === true) {
|
|
67
|
+
process.stdout.write(`${formatJson(report)}\n`)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const useColor = Boolean(process.stdout.isTTY) && process.env.NO_COLOR === undefined
|
|
72
|
+
const terminalWidth = resolveTerminalWidth()
|
|
73
|
+
const text = formatReport(report, {
|
|
74
|
+
useColor,
|
|
75
|
+
view,
|
|
76
|
+
...(terminalWidth !== undefined ? { terminalWidth } : {}),
|
|
77
|
+
...(limit !== undefined ? { limit } : {}),
|
|
78
|
+
})
|
|
79
|
+
process.stdout.write(`${text}\n`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveTerminalWidth(): number | undefined {
|
|
83
|
+
// process.stdout.columns is undefined when stdout is not a TTY (piped to
|
|
84
|
+
// less/grep/file). Fall back to $COLUMNS so users can force a width when
|
|
85
|
+
// piping, and so tests with an inherited COLUMNS env var see the override.
|
|
86
|
+
if (process.stdout.columns !== undefined) return process.stdout.columns
|
|
87
|
+
const env = process.env.COLUMNS
|
|
88
|
+
if (env === undefined || env === '') return undefined
|
|
89
|
+
const n = Number(env)
|
|
90
|
+
return Number.isFinite(n) && n > 0 ? n : undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseLimit(value: unknown): number | undefined {
|
|
94
|
+
if (typeof value !== 'string') return undefined
|
|
95
|
+
const n = Number(value)
|
|
96
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined
|
|
97
|
+
}
|
package/src/compose/index.ts
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { runUsage, type UsageReport } from '@/usage'
|
|
2
|
+
|
|
3
|
+
import { discoverAgents, type AgentEntry } from './discover'
|
|
4
|
+
import type { AgentResult } from './start'
|
|
5
|
+
|
|
6
|
+
export type ComposeUsageEvent =
|
|
7
|
+
| { kind: 'agent-start'; name: string }
|
|
8
|
+
| { kind: 'agent-done'; name: string; result: AgentResult<UsageReport> }
|
|
9
|
+
|
|
10
|
+
export type ComposeUsageOptions = {
|
|
11
|
+
rootCwd: string
|
|
12
|
+
since?: number
|
|
13
|
+
until?: number
|
|
14
|
+
onProgress?: (event: ComposeUsageEvent) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ComposeUsageResult = {
|
|
18
|
+
rootCwd: string
|
|
19
|
+
range: { since: number | null; until: number | null }
|
|
20
|
+
agents: AgentEntry[]
|
|
21
|
+
results: AgentResult<UsageReport>[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Fans out `runUsage` across every typeclaw agent in `rootCwd`'s immediate
|
|
25
|
+
// subdirectories. Per-agent failures are captured as `AgentResult` rather than
|
|
26
|
+
// thrown — one corrupt sessions/ dir shouldn't blank out the whole report.
|
|
27
|
+
export async function composeUsage({
|
|
28
|
+
rootCwd,
|
|
29
|
+
since,
|
|
30
|
+
until,
|
|
31
|
+
onProgress,
|
|
32
|
+
}: ComposeUsageOptions): Promise<ComposeUsageResult> {
|
|
33
|
+
const agents = discoverAgents(rootCwd)
|
|
34
|
+
const results = await Promise.all(
|
|
35
|
+
agents.map(async (agent): Promise<AgentResult<UsageReport>> => {
|
|
36
|
+
onProgress?.({ kind: 'agent-start', name: agent.name })
|
|
37
|
+
const result = await runOne(agent, since, until)
|
|
38
|
+
onProgress?.({ kind: 'agent-done', name: agent.name, result })
|
|
39
|
+
return result
|
|
40
|
+
}),
|
|
41
|
+
)
|
|
42
|
+
return {
|
|
43
|
+
rootCwd,
|
|
44
|
+
range: { since: since ?? null, until: until ?? null },
|
|
45
|
+
agents,
|
|
46
|
+
results,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function runOne(
|
|
51
|
+
agent: AgentEntry,
|
|
52
|
+
since: number | undefined,
|
|
53
|
+
until: number | undefined,
|
|
54
|
+
): Promise<AgentResult<UsageReport>> {
|
|
55
|
+
try {
|
|
56
|
+
const report = await runUsage({
|
|
57
|
+
agentDir: agent.cwd,
|
|
58
|
+
...(since !== undefined ? { since } : {}),
|
|
59
|
+
...(until !== undefined ? { until } : {}),
|
|
60
|
+
})
|
|
61
|
+
return { name: agent.name, ok: true, data: report }
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return { name: agent.name, ok: false, reason: error instanceof Error ? error.message : String(error) }
|
|
64
|
+
}
|
|
65
|
+
}
|