typeclaw 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 +134 -0
- package/auth.schema.json +63 -0
- package/cron.schema.json +96 -0
- package/package.json +72 -0
- package/scripts/emit-base-dockerfile.ts +5 -0
- package/scripts/generate-schema.ts +34 -0
- package/secrets.schema.json +63 -0
- package/src/agent/auth.ts +119 -0
- package/src/agent/compaction.ts +35 -0
- package/src/agent/git-nudge.ts +95 -0
- package/src/agent/index.ts +451 -0
- package/src/agent/plugin-tools.ts +269 -0
- package/src/agent/reload-tool.ts +71 -0
- package/src/agent/self.ts +45 -0
- package/src/agent/session-origin.ts +288 -0
- package/src/agent/subagents.ts +253 -0
- package/src/agent/system-prompt.ts +68 -0
- package/src/agent/tools/channel-fetch-attachment.ts +118 -0
- package/src/agent/tools/channel-history.ts +119 -0
- package/src/agent/tools/channel-reply.ts +182 -0
- package/src/agent/tools/channel-send.ts +212 -0
- package/src/agent/tools/ddg.ts +218 -0
- package/src/agent/tools/restart.ts +122 -0
- package/src/agent/tools/stream-snapshot.ts +181 -0
- package/src/agent/tools/webfetch/fetch.ts +102 -0
- package/src/agent/tools/webfetch/index.ts +1 -0
- package/src/agent/tools/webfetch/strategies/grep.ts +70 -0
- package/src/agent/tools/webfetch/strategies/jq.ts +31 -0
- package/src/agent/tools/webfetch/strategies/raw.ts +3 -0
- package/src/agent/tools/webfetch/strategies/readability.ts +30 -0
- package/src/agent/tools/webfetch/strategies/selector.ts +41 -0
- package/src/agent/tools/webfetch/strategies/snapshot.ts +135 -0
- package/src/agent/tools/webfetch/tool.ts +281 -0
- package/src/agent/tools/webfetch/types.ts +33 -0
- package/src/agent/tools/websearch.ts +96 -0
- package/src/agent/tools/wikipedia.ts +52 -0
- package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +170 -0
- package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +421 -0
- package/src/bundled-plugins/agent-browser/index.ts +179 -0
- package/src/bundled-plugins/agent-browser/shim-install.ts +158 -0
- package/src/bundled-plugins/agent-browser/shim.ts +152 -0
- package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +113 -0
- package/src/bundled-plugins/guard/index.ts +26 -0
- package/src/bundled-plugins/guard/policies/non-workspace-write.ts +98 -0
- package/src/bundled-plugins/guard/policies/skill-authoring.ts +185 -0
- package/src/bundled-plugins/guard/policies/uncommitted-changes.ts +85 -0
- package/src/bundled-plugins/guard/policy.ts +18 -0
- package/src/bundled-plugins/memory/README.md +71 -0
- package/src/bundled-plugins/memory/append-tool.ts +84 -0
- package/src/bundled-plugins/memory/dreaming-state.ts +86 -0
- package/src/bundled-plugins/memory/dreaming.ts +470 -0
- package/src/bundled-plugins/memory/fragment-parser.ts +67 -0
- package/src/bundled-plugins/memory/index.ts +238 -0
- package/src/bundled-plugins/memory/load-memory.ts +122 -0
- package/src/bundled-plugins/memory/memory-logger.ts +257 -0
- package/src/bundled-plugins/memory/secret-detector.ts +49 -0
- package/src/bundled-plugins/memory/watermark.ts +15 -0
- package/src/bundled-plugins/security/index.ts +35 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +120 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +167 -0
- package/src/bundled-plugins/security/policies/prompt-injection.ts +488 -0
- package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +99 -0
- package/src/bundled-plugins/security/policies/secret-exfil-read.ts +127 -0
- package/src/bundled-plugins/security/policies/session-search-secrets.ts +86 -0
- package/src/bundled-plugins/security/policies/ssrf.ts +196 -0
- package/src/bundled-plugins/security/policies/system-prompt-leak.ts +81 -0
- package/src/bundled-plugins/security/policy.ts +9 -0
- package/src/channels/adapters/discord-bot-channel-resolver.ts +77 -0
- package/src/channels/adapters/discord-bot-classify.ts +148 -0
- package/src/channels/adapters/discord-bot.ts +640 -0
- package/src/channels/adapters/kakaotalk-author-resolver.ts +78 -0
- package/src/channels/adapters/kakaotalk-channel-resolver.ts +105 -0
- package/src/channels/adapters/kakaotalk-classify.ts +77 -0
- package/src/channels/adapters/kakaotalk.ts +622 -0
- package/src/channels/adapters/slack-bot-author-resolver.ts +80 -0
- package/src/channels/adapters/slack-bot-channel-resolver.ts +84 -0
- package/src/channels/adapters/slack-bot-classify.ts +213 -0
- package/src/channels/adapters/slack-bot-dedupe.ts +51 -0
- package/src/channels/adapters/slack-bot-time.ts +10 -0
- package/src/channels/adapters/slack-bot.ts +881 -0
- package/src/channels/adapters/telegram-bot-classify.ts +155 -0
- package/src/channels/adapters/telegram-bot-format.ts +309 -0
- package/src/channels/adapters/telegram-bot.ts +604 -0
- package/src/channels/engagement.ts +227 -0
- package/src/channels/index.ts +21 -0
- package/src/channels/manager.ts +292 -0
- package/src/channels/membership-cache.ts +116 -0
- package/src/channels/membership-from-history.ts +53 -0
- package/src/channels/membership.ts +30 -0
- package/src/channels/participants.ts +47 -0
- package/src/channels/persistence.ts +209 -0
- package/src/channels/reloadable.ts +28 -0
- package/src/channels/router.ts +1570 -0
- package/src/channels/schema.ts +273 -0
- package/src/channels/types.ts +160 -0
- package/src/cli/channel.ts +403 -0
- package/src/cli/compose-status.ts +95 -0
- package/src/cli/compose.ts +240 -0
- package/src/cli/hostd.ts +163 -0
- package/src/cli/index.ts +27 -0
- package/src/cli/init.ts +592 -0
- package/src/cli/logs.ts +38 -0
- package/src/cli/reload.ts +68 -0
- package/src/cli/restart.ts +66 -0
- package/src/cli/run.ts +77 -0
- package/src/cli/shell.ts +33 -0
- package/src/cli/start.ts +57 -0
- package/src/cli/status.ts +178 -0
- package/src/cli/stop.ts +31 -0
- package/src/cli/tui.ts +35 -0
- package/src/cli/ui.ts +110 -0
- package/src/commands/index.ts +74 -0
- package/src/compose/discover.ts +43 -0
- package/src/compose/index.ts +25 -0
- package/src/compose/logs.ts +162 -0
- package/src/compose/restart.ts +69 -0
- package/src/compose/start.ts +62 -0
- package/src/compose/status.ts +28 -0
- package/src/compose/stop.ts +43 -0
- package/src/config/config.ts +424 -0
- package/src/config/index.ts +25 -0
- package/src/config/providers.ts +234 -0
- package/src/config/reloadable.ts +47 -0
- package/src/container/index.ts +27 -0
- package/src/container/logs.ts +37 -0
- package/src/container/port.ts +137 -0
- package/src/container/shared.ts +290 -0
- package/src/container/shell.ts +58 -0
- package/src/container/start.ts +670 -0
- package/src/container/status.ts +76 -0
- package/src/container/stop.ts +120 -0
- package/src/container/verify-running.ts +149 -0
- package/src/cron/consumer.ts +138 -0
- package/src/cron/index.ts +54 -0
- package/src/cron/reloadable.ts +64 -0
- package/src/cron/scheduler.ts +200 -0
- package/src/cron/schema.ts +96 -0
- package/src/hostd/client.ts +113 -0
- package/src/hostd/daemon.ts +587 -0
- package/src/hostd/index.ts +25 -0
- package/src/hostd/paths.ts +82 -0
- package/src/hostd/portbroker-manager.ts +101 -0
- package/src/hostd/protocol.ts +48 -0
- package/src/hostd/spawn.ts +224 -0
- package/src/hostd/supervisor.ts +60 -0
- package/src/hostd/tailscale.ts +172 -0
- package/src/hostd/version.ts +115 -0
- package/src/init/dockerfile.ts +327 -0
- package/src/init/ensure-deps.ts +152 -0
- package/src/init/gitignore.ts +46 -0
- package/src/init/hatching.ts +60 -0
- package/src/init/index.ts +786 -0
- package/src/init/kakaotalk-auth.ts +114 -0
- package/src/init/models-dev.ts +130 -0
- package/src/init/oauth-login.ts +74 -0
- package/src/init/packagejson.ts +94 -0
- package/src/init/paths.ts +2 -0
- package/src/init/run-bun-install.ts +20 -0
- package/src/markdown/chunk.ts +299 -0
- package/src/markdown/index.ts +1 -0
- package/src/plugin/context.ts +40 -0
- package/src/plugin/define.ts +35 -0
- package/src/plugin/hooks.ts +204 -0
- package/src/plugin/index.ts +63 -0
- package/src/plugin/loader.ts +111 -0
- package/src/plugin/manager.ts +136 -0
- package/src/plugin/registry.ts +145 -0
- package/src/plugin/skills.ts +62 -0
- package/src/plugin/types.ts +172 -0
- package/src/portbroker/bind-with-forward.ts +102 -0
- package/src/portbroker/container-server.ts +305 -0
- package/src/portbroker/forward-result-bus.ts +36 -0
- package/src/portbroker/hostd-client.ts +443 -0
- package/src/portbroker/index.ts +33 -0
- package/src/portbroker/policy.ts +24 -0
- package/src/portbroker/proc-net-tcp.ts +72 -0
- package/src/portbroker/protocol.ts +39 -0
- package/src/reload/client.ts +59 -0
- package/src/reload/index.ts +3 -0
- package/src/reload/registry.ts +60 -0
- package/src/reload/types.ts +13 -0
- package/src/run/bundled-plugins.ts +24 -0
- package/src/run/channel-session-factory.ts +105 -0
- package/src/run/index.ts +432 -0
- package/src/run/plugin-runtime.ts +43 -0
- package/src/run/schema-with-plugins.ts +14 -0
- package/src/secrets/index.ts +13 -0
- package/src/secrets/migrate.ts +95 -0
- package/src/secrets/schema.ts +75 -0
- package/src/secrets/storage.ts +231 -0
- package/src/server/index.ts +436 -0
- package/src/sessions/index.ts +23 -0
- package/src/shared/index.ts +9 -0
- package/src/shared/local-time.ts +21 -0
- package/src/shared/protocol.ts +25 -0
- package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +87 -0
- package/src/skills/typeclaw-channel-telegram-bot/SKILL.md +64 -0
- package/src/skills/typeclaw-config/SKILL.md +643 -0
- package/src/skills/typeclaw-cron/SKILL.md +159 -0
- package/src/skills/typeclaw-git/SKILL.md +89 -0
- package/src/skills/typeclaw-memory/SKILL.md +174 -0
- package/src/skills/typeclaw-monorepo/SKILL.md +175 -0
- package/src/skills/typeclaw-plugins/SKILL.md +594 -0
- package/src/skills/typeclaw-skills/SKILL.md +246 -0
- package/src/stream/broker.ts +161 -0
- package/src/stream/index.ts +16 -0
- package/src/stream/types.ts +69 -0
- package/src/tui/client.ts +45 -0
- package/src/tui/format.ts +317 -0
- package/src/tui/index.ts +225 -0
- package/src/tui/theme.ts +41 -0
- package/typeclaw.schema.json +826 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import type { DefinedPlugin } from './types'
|
|
6
|
+
|
|
7
|
+
export type ResolvedPlugin = {
|
|
8
|
+
name: string
|
|
9
|
+
version: string | undefined
|
|
10
|
+
source: string
|
|
11
|
+
defined: DefinedPlugin<any>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type LoadPluginEntryFn = (entry: string, agentDir: string) => Promise<ResolvedPlugin>
|
|
15
|
+
|
|
16
|
+
export async function loadPluginEntry(entry: string, agentDir: string): Promise<ResolvedPlugin> {
|
|
17
|
+
if (isLocalPath(entry)) {
|
|
18
|
+
return loadLocal(entry, agentDir)
|
|
19
|
+
}
|
|
20
|
+
return loadNpm(entry, agentDir)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isLocalPath(entry: string): boolean {
|
|
24
|
+
return entry.startsWith('./') || entry.startsWith('../') || isAbsolute(entry)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function loadLocal(entry: string, agentDir: string): Promise<ResolvedPlugin> {
|
|
28
|
+
const resolved = resolve(agentDir, entry)
|
|
29
|
+
// Confine local plugin paths to within agentDir so a malicious typeclaw.json
|
|
30
|
+
// cannot point at arbitrary files on the host.
|
|
31
|
+
const rel = relative(agentDir, resolved)
|
|
32
|
+
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
33
|
+
throw new Error(`plugin path escapes agent directory: ${entry} (resolved to ${resolved})`)
|
|
34
|
+
}
|
|
35
|
+
if (!existsSync(resolved)) {
|
|
36
|
+
throw new Error(`plugin path does not exist: ${entry} (resolved to ${resolved})`)
|
|
37
|
+
}
|
|
38
|
+
const url = pathToFileURL(resolved).href
|
|
39
|
+
const mod = (await import(url)) as { default?: unknown }
|
|
40
|
+
const defined = expectDefined(mod, entry)
|
|
41
|
+
const name = basename(resolved).replace(/\.(ts|tsx|js|mjs|cjs)$/i, '')
|
|
42
|
+
return { name, version: undefined, source: entry, defined }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function loadNpm(entry: string, agentDir: string): Promise<ResolvedPlugin> {
|
|
46
|
+
const pkgJsonPath = findPackageJson(entry, agentDir)
|
|
47
|
+
let pkgName = entry
|
|
48
|
+
let version: string | undefined
|
|
49
|
+
let entryPath: string | null = null
|
|
50
|
+
if (pkgJsonPath !== null) {
|
|
51
|
+
try {
|
|
52
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8')) as {
|
|
53
|
+
name?: unknown
|
|
54
|
+
version?: unknown
|
|
55
|
+
main?: unknown
|
|
56
|
+
module?: unknown
|
|
57
|
+
}
|
|
58
|
+
if (typeof pkg.name === 'string' && pkg.name.length > 0) pkgName = pkg.name
|
|
59
|
+
if (typeof pkg.version === 'string' && pkg.version.length > 0) version = pkg.version
|
|
60
|
+
const main = typeof pkg.module === 'string' ? pkg.module : typeof pkg.main === 'string' ? pkg.main : null
|
|
61
|
+
if (main !== null) {
|
|
62
|
+
const candidate = join(dirname(pkgJsonPath), main)
|
|
63
|
+
if (existsSync(candidate)) {
|
|
64
|
+
entryPath = candidate
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Fall through to bare-import resolution.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Falls back to bare-import resolution when entryPath cannot be located on
|
|
72
|
+
// disk. Modern packages with `exports` map (and no `main`/`module`) take
|
|
73
|
+
// this path so Bun's resolver can read `exports`.
|
|
74
|
+
const importTarget = entryPath !== null ? pathToFileURL(entryPath).href : entry
|
|
75
|
+
const mod = (await import(importTarget)) as { default?: unknown }
|
|
76
|
+
const defined = expectDefined(mod, entry)
|
|
77
|
+
const name = derivePluginNameFromPackage(pkgName)
|
|
78
|
+
return { name, version, source: entry, defined }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function derivePluginNameFromPackage(packageName: string): string {
|
|
82
|
+
const PREFIX = 'typeclaw-plugin-'
|
|
83
|
+
const SCOPED_PREFIX_RE = /^@[^/]+\//
|
|
84
|
+
const stripped = packageName.replace(SCOPED_PREFIX_RE, '')
|
|
85
|
+
return stripped.startsWith(PREFIX) ? stripped.slice(PREFIX.length) : stripped
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function findPackageJson(entry: string, agentDir: string): string | null {
|
|
89
|
+
const PACKAGE_JSON = 'package.json'
|
|
90
|
+
let cur = agentDir
|
|
91
|
+
while (true) {
|
|
92
|
+
const p = join(cur, 'node_modules', entry, PACKAGE_JSON)
|
|
93
|
+
if (existsSync(p)) return p
|
|
94
|
+
const parent = dirname(cur)
|
|
95
|
+
if (parent === cur) return null
|
|
96
|
+
cur = parent
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function expectDefined(mod: { default?: unknown }, entry: string): DefinedPlugin<any> {
|
|
101
|
+
const def = mod.default
|
|
102
|
+
if (
|
|
103
|
+
def !== null &&
|
|
104
|
+
typeof def === 'object' &&
|
|
105
|
+
'plugin' in (def as Record<string, unknown>) &&
|
|
106
|
+
typeof (def as { plugin: unknown }).plugin === 'function'
|
|
107
|
+
) {
|
|
108
|
+
return def as DefinedPlugin<any>
|
|
109
|
+
}
|
|
110
|
+
throw new Error(`plugin ${entry}: default export is not a definePlugin(...) result`)
|
|
111
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import type { CronJob } from '@/cron'
|
|
4
|
+
|
|
5
|
+
import { createPluginContext, createPluginLogger, type SpawnSubagentFn } from './context'
|
|
6
|
+
import { createHookBus, type HookBus } from './hooks'
|
|
7
|
+
import { loadPluginEntry, type LoadPluginEntryFn, type ResolvedPlugin } from './loader'
|
|
8
|
+
import { discardRegistrationsBy, emptyRegistry, type PluginRegistry, registerContributions } from './registry'
|
|
9
|
+
import type { PluginExports } from './types'
|
|
10
|
+
|
|
11
|
+
export type LoadPluginsOptions = {
|
|
12
|
+
entries: string[]
|
|
13
|
+
agentDir: string
|
|
14
|
+
configsByName: Record<string, unknown>
|
|
15
|
+
loadEntry?: LoadPluginEntryFn
|
|
16
|
+
// Bundled plugins resolved by the runtime (not from typeclaw.json). Loaded
|
|
17
|
+
// before user-declared `entries` so a config block named after a bundled
|
|
18
|
+
// plugin (e.g. "memory") is consumed by the bundled plugin, and so plugin-
|
|
19
|
+
// name conflicts with a user-declared entry surface as a clear error.
|
|
20
|
+
bundled?: ResolvedPlugin[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type LoadPluginsResult = {
|
|
24
|
+
registry: PluginRegistry
|
|
25
|
+
hooks: HookBus
|
|
26
|
+
loadedPlugins: { name: string; version: string | undefined; source: string }[]
|
|
27
|
+
markBooted: () => void
|
|
28
|
+
setSpawnSubagent: (fn: SpawnSubagentFn) => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function loadPlugins(opts: LoadPluginsOptions): Promise<LoadPluginsResult> {
|
|
32
|
+
const registry = emptyRegistry()
|
|
33
|
+
const hooks = createHookBus()
|
|
34
|
+
const loaded: { name: string; version: string | undefined; source: string }[] = []
|
|
35
|
+
const loadEntry = opts.loadEntry ?? loadPluginEntry
|
|
36
|
+
|
|
37
|
+
let booted = false
|
|
38
|
+
let spawnSubagentImpl: SpawnSubagentFn = async () => {
|
|
39
|
+
throw new Error('plugin: spawnSubagent is not yet wired')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const allPlugins: { entry: string; resolved: ResolvedPlugin }[] = [
|
|
43
|
+
...(opts.bundled?.map((resolved) => ({ entry: `<bundled:${resolved.name}>`, resolved })) ?? []),
|
|
44
|
+
...(await Promise.all(
|
|
45
|
+
opts.entries.map(async (entry) => ({ entry, resolved: await loadEntry(entry, opts.agentDir) })),
|
|
46
|
+
)),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
for (const { entry, resolved } of allPlugins) {
|
|
50
|
+
if (loaded.find((l) => l.name === resolved.name)) {
|
|
51
|
+
throw new Error(`plugin name conflict: ${resolved.name} (entry ${entry}) already loaded`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let validatedConfig: unknown = undefined
|
|
55
|
+
if (resolved.defined.configSchema) {
|
|
56
|
+
const raw = opts.configsByName[resolved.name]
|
|
57
|
+
const parsed = (resolved.defined.configSchema as z.ZodType<unknown>).safeParse(raw ?? {})
|
|
58
|
+
if (!parsed.success) {
|
|
59
|
+
throw new Error(`plugin ${resolved.name}: config invalid: ${formatZodIssues(parsed.error)}`)
|
|
60
|
+
}
|
|
61
|
+
validatedConfig = parsed.data
|
|
62
|
+
} else if (opts.configsByName[resolved.name] !== undefined) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`plugin ${resolved.name}: config block "${resolved.name}" present in typeclaw.json but plugin declares no configSchema`,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const logger = createPluginLogger(resolved.name)
|
|
69
|
+
const ctx = createPluginContext({
|
|
70
|
+
name: resolved.name,
|
|
71
|
+
version: resolved.version,
|
|
72
|
+
agentDir: opts.agentDir,
|
|
73
|
+
config: validatedConfig as never,
|
|
74
|
+
logger,
|
|
75
|
+
spawnSubagent: (name, payload) => spawnSubagentImpl(name, payload),
|
|
76
|
+
isBooted: () => booted,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
let exports: PluginExports
|
|
80
|
+
try {
|
|
81
|
+
exports = await resolved.defined.plugin(ctx)
|
|
82
|
+
} catch (err) {
|
|
83
|
+
discardRegistrationsBy(resolved.name, registry, hooks)
|
|
84
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
85
|
+
throw new Error(`plugin ${resolved.name}: factory threw: ${message}`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
registerContributions({
|
|
90
|
+
pluginName: resolved.name,
|
|
91
|
+
logger,
|
|
92
|
+
exports,
|
|
93
|
+
registry,
|
|
94
|
+
hooks,
|
|
95
|
+
agentDir: opts.agentDir,
|
|
96
|
+
})
|
|
97
|
+
} catch (err) {
|
|
98
|
+
discardRegistrationsBy(resolved.name, registry, hooks)
|
|
99
|
+
throw err
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
loaded.push({ name: resolved.name, version: resolved.version, source: resolved.source })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
registry,
|
|
107
|
+
hooks,
|
|
108
|
+
loadedPlugins: loaded,
|
|
109
|
+
markBooted: () => {
|
|
110
|
+
booted = true
|
|
111
|
+
},
|
|
112
|
+
setSpawnSubagent: (fn) => {
|
|
113
|
+
spawnSubagentImpl = fn
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function summarizeLoaded(loaded: LoadPluginsResult['loadedPlugins'], registry: PluginRegistry): string {
|
|
119
|
+
const head = loaded.map((p) => (p.version !== undefined ? `${p.name} v${p.version}` : p.name)).join(', ')
|
|
120
|
+
const counts = [
|
|
121
|
+
`${registry.tools.length} tool(s)`,
|
|
122
|
+
`${registry.subagents.length} subagent(s)`,
|
|
123
|
+
`${registry.cronJobs.length} cron job(s)`,
|
|
124
|
+
`${registry.skills.length} skill(s)`,
|
|
125
|
+
`${registry.skillsDirs.length} skills dir(s)`,
|
|
126
|
+
].join(', ')
|
|
127
|
+
return `${loaded.length} plugin(s): ${head} [${counts}]`
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function pluginCronJobs(registry: PluginRegistry): CronJob[] {
|
|
131
|
+
return registry.cronJobs.map((j) => j.job)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function formatZodIssues(error: z.ZodError): string {
|
|
135
|
+
return error.issues.map((i) => `${i.path.length > 0 ? i.path.join('.') : '<root>'}: ${i.message}`).join('; ')
|
|
136
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
import type { CronJob, PromptJob } from '@/cron'
|
|
4
|
+
|
|
5
|
+
import type { HookBus } from './hooks'
|
|
6
|
+
import type { PluginCronJob, PluginExports, PluginLogger, PluginSkill, Subagent, Tool } from './types'
|
|
7
|
+
|
|
8
|
+
export type RegisteredTool = { pluginName: string; toolName: string; tool: Tool<any>; logger: PluginLogger }
|
|
9
|
+
export type RegisteredSubagent = { pluginName: string; subagentName: string; subagent: Subagent<any> }
|
|
10
|
+
export type RegisteredCronJob = { pluginName: string; localId: string; globalId: string; job: CronJob }
|
|
11
|
+
export type RegisteredSkillEntry = { pluginName: string; localName: string; skill: PluginSkill }
|
|
12
|
+
export type RegisteredSkillDir = { pluginName: string; path: string }
|
|
13
|
+
|
|
14
|
+
export type PluginRegistry = {
|
|
15
|
+
tools: RegisteredTool[]
|
|
16
|
+
subagents: RegisteredSubagent[]
|
|
17
|
+
cronJobs: RegisteredCronJob[]
|
|
18
|
+
skills: RegisteredSkillEntry[]
|
|
19
|
+
skillsDirs: RegisteredSkillDir[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type RegisterContributionsOptions = {
|
|
23
|
+
pluginName: string
|
|
24
|
+
logger: PluginLogger
|
|
25
|
+
exports: PluginExports
|
|
26
|
+
registry: PluginRegistry
|
|
27
|
+
hooks: HookBus
|
|
28
|
+
agentDir: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildPluginCronGlobalId(pluginName: string, localId: string): string {
|
|
32
|
+
return `__plugin_${pluginName}_${localId}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function registerContributions(opts: RegisterContributionsOptions): void {
|
|
36
|
+
const { pluginName, logger, exports: ex, registry, hooks, agentDir } = opts
|
|
37
|
+
|
|
38
|
+
if (ex.tools) {
|
|
39
|
+
for (const [toolName, tool] of Object.entries(ex.tools)) {
|
|
40
|
+
assertNotEmpty('tool name', toolName, pluginName)
|
|
41
|
+
const conflict = registry.tools.find((t) => t.toolName === toolName)
|
|
42
|
+
if (conflict) {
|
|
43
|
+
throw new Error(`plugin ${pluginName}: tool "${toolName}" already registered by plugin ${conflict.pluginName}`)
|
|
44
|
+
}
|
|
45
|
+
registry.tools.push({ pluginName, toolName, tool, logger })
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (ex.subagents) {
|
|
50
|
+
for (const [subagentName, subagent] of Object.entries(ex.subagents)) {
|
|
51
|
+
assertNotEmpty('subagent name', subagentName, pluginName)
|
|
52
|
+
const conflict = registry.subagents.find((s) => s.subagentName === subagentName)
|
|
53
|
+
if (conflict) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`plugin ${pluginName}: subagent "${subagentName}" already registered by plugin ${conflict.pluginName}`,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
registry.subagents.push({ pluginName, subagentName, subagent })
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (ex.cronJobs) {
|
|
63
|
+
for (const [localId, spec] of Object.entries(ex.cronJobs)) {
|
|
64
|
+
assertNotEmpty('cron job id', localId, pluginName)
|
|
65
|
+
const globalId = buildPluginCronGlobalId(pluginName, localId)
|
|
66
|
+
const conflict = registry.cronJobs.find((j) => j.globalId === globalId)
|
|
67
|
+
if (conflict) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`plugin ${pluginName}: cron job "${localId}" globalId "${globalId}" conflicts with plugin ${conflict.pluginName}`,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
const job = toCronJob(globalId, spec)
|
|
73
|
+
registry.cronJobs.push({ pluginName, localId, globalId, job })
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (ex.skills) {
|
|
78
|
+
for (const [localName, skill] of Object.entries(ex.skills)) {
|
|
79
|
+
assertNotEmpty('skill name', localName, pluginName)
|
|
80
|
+
const conflict = registry.skills.find((s) => s.localName === localName)
|
|
81
|
+
if (conflict) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`plugin ${pluginName}: skill "${localName}" already registered by plugin ${conflict.pluginName}`,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
registry.skills.push({ pluginName, localName, skill })
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (ex.skillsDirs) {
|
|
91
|
+
for (const path of ex.skillsDirs) {
|
|
92
|
+
if (!existsSync(path)) {
|
|
93
|
+
logger.warn(`skillsDirs entry does not exist on disk: ${path}`)
|
|
94
|
+
}
|
|
95
|
+
registry.skillsDirs.push({ pluginName, path })
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (ex.hooks) {
|
|
100
|
+
hooks.registerAll(pluginName, agentDir, logger, ex.hooks)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function discardRegistrationsBy(pluginName: string, registry: PluginRegistry, hooks: HookBus): void {
|
|
105
|
+
registry.tools = registry.tools.filter((t) => t.pluginName !== pluginName)
|
|
106
|
+
registry.subagents = registry.subagents.filter((s) => s.pluginName !== pluginName)
|
|
107
|
+
registry.cronJobs = registry.cronJobs.filter((j) => j.pluginName !== pluginName)
|
|
108
|
+
registry.skills = registry.skills.filter((s) => s.pluginName !== pluginName)
|
|
109
|
+
registry.skillsDirs = registry.skillsDirs.filter((d) => d.pluginName !== pluginName)
|
|
110
|
+
hooks.unregisterAll(pluginName)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function emptyRegistry(): PluginRegistry {
|
|
114
|
+
return { tools: [], subagents: [], cronJobs: [], skills: [], skillsDirs: [] }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function assertNotEmpty(kind: string, value: string, pluginName: string): void {
|
|
118
|
+
if (value.length === 0) {
|
|
119
|
+
throw new Error(`plugin ${pluginName}: empty ${kind}`)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function toCronJob(globalId: string, spec: PluginCronJob): CronJob {
|
|
124
|
+
if (spec.kind === 'prompt') {
|
|
125
|
+
const job: PromptJob = {
|
|
126
|
+
id: globalId,
|
|
127
|
+
schedule: spec.schedule,
|
|
128
|
+
enabled: spec.enabled ?? true,
|
|
129
|
+
kind: 'prompt',
|
|
130
|
+
prompt: spec.prompt,
|
|
131
|
+
...(spec.timezone !== undefined ? { timezone: spec.timezone } : {}),
|
|
132
|
+
...(spec.subagent !== undefined ? { subagent: spec.subagent } : {}),
|
|
133
|
+
...(spec.payload !== undefined ? { payload: spec.payload } : {}),
|
|
134
|
+
}
|
|
135
|
+
return job
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
id: globalId,
|
|
139
|
+
schedule: spec.schedule,
|
|
140
|
+
enabled: spec.enabled ?? true,
|
|
141
|
+
kind: 'exec',
|
|
142
|
+
command: spec.command,
|
|
143
|
+
...(spec.timezone !== undefined ? { timezone: spec.timezone } : {}),
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import type { PluginSkill } from './types'
|
|
6
|
+
|
|
7
|
+
export type SkillEntry = { pluginName: string; localName: string; skill: PluginSkill }
|
|
8
|
+
|
|
9
|
+
export type MaterializedSkills = {
|
|
10
|
+
dir: string
|
|
11
|
+
dispose: () => Promise<void>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-_]*$/
|
|
15
|
+
|
|
16
|
+
export async function materializeSkills(skills: SkillEntry[]): Promise<MaterializedSkills> {
|
|
17
|
+
const root = await mkdtemp(join(tmpdir(), 'typeclaw-plugin-skills-'))
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const seen = new Set<string>()
|
|
21
|
+
for (const entry of skills) {
|
|
22
|
+
const sanitized = sanitizeSkillName(entry.localName)
|
|
23
|
+
if (seen.has(sanitized)) {
|
|
24
|
+
throw new Error(`plugin ${entry.pluginName}: duplicate skill name after sanitization: ${entry.localName}`)
|
|
25
|
+
}
|
|
26
|
+
seen.add(sanitized)
|
|
27
|
+
|
|
28
|
+
const skillDir = join(root, sanitized)
|
|
29
|
+
await mkdir(skillDir, { recursive: true })
|
|
30
|
+
const body = renderSkillFile(entry.skill)
|
|
31
|
+
await writeFile(join(skillDir, 'SKILL.md'), body, 'utf8')
|
|
32
|
+
}
|
|
33
|
+
} catch (err) {
|
|
34
|
+
await rm(root, { recursive: true, force: true })
|
|
35
|
+
throw err
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
dir: root,
|
|
40
|
+
dispose: async () => {
|
|
41
|
+
await rm(root, { recursive: true, force: true })
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sanitizeSkillName(name: string): string {
|
|
47
|
+
if (SKILL_NAME_PATTERN.test(name)) return name
|
|
48
|
+
return name.toLowerCase().replace(/[^a-z0-9_-]/g, '-')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderSkillFile(skill: PluginSkill): string {
|
|
52
|
+
const fm = skill.frontmatter ?? {}
|
|
53
|
+
const fmEntries = Object.entries({ ...fm, description: skill.description })
|
|
54
|
+
if (fmEntries.length === 0) return skill.content
|
|
55
|
+
const lines = ['---']
|
|
56
|
+
for (const [key, value] of fmEntries) {
|
|
57
|
+
lines.push(`${key}: ${JSON.stringify(value)}`)
|
|
58
|
+
}
|
|
59
|
+
lines.push('---', '')
|
|
60
|
+
lines.push(skill.content)
|
|
61
|
+
return lines.join('\n')
|
|
62
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import type { SessionOrigin } from '@/agent/session-origin'
|
|
4
|
+
|
|
5
|
+
export type ContentPart = { type: 'text'; text: string } | { type: 'image'; mimeType: string; data: string }
|
|
6
|
+
|
|
7
|
+
export type ToolResult = {
|
|
8
|
+
content: ContentPart[]
|
|
9
|
+
details?: unknown
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ToolLogger = {
|
|
13
|
+
info: (msg: string) => void
|
|
14
|
+
warn: (msg: string) => void
|
|
15
|
+
error: (msg: string) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type ToolContext = {
|
|
19
|
+
signal: AbortSignal | undefined
|
|
20
|
+
sessionId: string
|
|
21
|
+
agentDir: string
|
|
22
|
+
logger: ToolLogger
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type Tool<P = unknown> = {
|
|
26
|
+
description: string
|
|
27
|
+
parameters: z.ZodType<P>
|
|
28
|
+
execute: (args: P, ctx: ToolContext) => Promise<ToolResult>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type BuiltinToolRef = { readonly __builtinTool: string }
|
|
32
|
+
|
|
33
|
+
export type SubagentContext<P = unknown> = {
|
|
34
|
+
userPrompt: string
|
|
35
|
+
agentDir: string
|
|
36
|
+
payload: P
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type RunSession = (override?: { userPrompt?: string }) => Promise<void>
|
|
40
|
+
|
|
41
|
+
export type Subagent<P = unknown> = {
|
|
42
|
+
systemPrompt: string
|
|
43
|
+
tools?: BuiltinToolRef[]
|
|
44
|
+
customTools?: Tool<any>[]
|
|
45
|
+
payloadSchema?: z.ZodType<P>
|
|
46
|
+
handler?: (ctx: SubagentContext<P>, runSession: RunSession) => Promise<void>
|
|
47
|
+
// Coalescing key for the SubagentConsumer's in-flight set. Default is the
|
|
48
|
+
// subagent name alone (only one instance of the subagent runs at a time).
|
|
49
|
+
// Override to allow per-payload concurrency, e.g. memory-logger keyed by
|
|
50
|
+
// parentSessionId so different parent sessions run in parallel while
|
|
51
|
+
// duplicate runs against the same session deduplicate.
|
|
52
|
+
inFlightKey?: (payload: P) => string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Cron job map keys are local; the runtime prefixes with `__plugin_<plugin-name>_`
|
|
56
|
+
// to form the global cron id, guaranteeing no collision with cron.json user
|
|
57
|
+
// jobs (no underscore prefix) or across plugins.
|
|
58
|
+
export type PluginPromptCronJob = {
|
|
59
|
+
schedule: string
|
|
60
|
+
kind: 'prompt'
|
|
61
|
+
prompt: string
|
|
62
|
+
enabled?: boolean
|
|
63
|
+
timezone?: string
|
|
64
|
+
subagent?: string
|
|
65
|
+
payload?: unknown
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type PluginExecCronJob = {
|
|
69
|
+
schedule: string
|
|
70
|
+
kind: 'exec'
|
|
71
|
+
command: string[]
|
|
72
|
+
enabled?: boolean
|
|
73
|
+
timezone?: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type PluginCronJob = PluginPromptCronJob | PluginExecCronJob
|
|
77
|
+
|
|
78
|
+
export type PluginSkill = {
|
|
79
|
+
description: string
|
|
80
|
+
content: string
|
|
81
|
+
frontmatter?: Record<string, unknown>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type SessionStartEvent = {
|
|
85
|
+
sessionId: string
|
|
86
|
+
agentDir: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type SessionEndEvent = {
|
|
90
|
+
sessionId: string
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type SessionIdleEvent = {
|
|
94
|
+
sessionId: string
|
|
95
|
+
parentTranscriptPath: string | undefined
|
|
96
|
+
idleMs: number
|
|
97
|
+
origin?: SessionOrigin
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Provider prompt caching requires byte-identical prefixes. Mutations near the
|
|
101
|
+
// end of `event.prompt` preserve cache hits across sessions; mutations near
|
|
102
|
+
// the start invalidate the cache on every LLM call.
|
|
103
|
+
export type SessionPromptEvent = {
|
|
104
|
+
prompt: string
|
|
105
|
+
sessionId: string
|
|
106
|
+
agentDir: string
|
|
107
|
+
origin?: SessionOrigin
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Fired for plugin-defined tools and TypeClaw-exposed system tools, including
|
|
111
|
+
// built-in pi tools (read/bash/edit/write/grep/find/ls) when plugins are wired.
|
|
112
|
+
export type ToolBeforeEvent = {
|
|
113
|
+
tool: string
|
|
114
|
+
sessionId: string
|
|
115
|
+
callId: string
|
|
116
|
+
args: Record<string, unknown>
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type ToolBeforeResult = void | undefined | { block: true; reason: string }
|
|
120
|
+
|
|
121
|
+
export type ToolAfterEvent = {
|
|
122
|
+
tool: string
|
|
123
|
+
sessionId: string
|
|
124
|
+
callId: string
|
|
125
|
+
result: ToolResult
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export type HookContext = {
|
|
129
|
+
agentDir: string
|
|
130
|
+
pluginName: string
|
|
131
|
+
logger: PluginLogger
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type Hooks = {
|
|
135
|
+
'session.start'?: (event: SessionStartEvent, ctx: HookContext) => Promise<void> | void
|
|
136
|
+
'session.end'?: (event: SessionEndEvent, ctx: HookContext) => Promise<void> | void
|
|
137
|
+
'session.idle'?: (event: SessionIdleEvent, ctx: HookContext) => Promise<void> | void
|
|
138
|
+
'session.prompt'?: (event: SessionPromptEvent, ctx: HookContext) => Promise<void> | void
|
|
139
|
+
'tool.before'?: (event: ToolBeforeEvent, ctx: HookContext) => Promise<ToolBeforeResult> | ToolBeforeResult
|
|
140
|
+
'tool.after'?: (event: ToolAfterEvent, ctx: HookContext) => Promise<void> | void
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export type HookName = keyof Hooks
|
|
144
|
+
|
|
145
|
+
export type PluginLogger = {
|
|
146
|
+
info: (msg: string) => void
|
|
147
|
+
warn: (msg: string) => void
|
|
148
|
+
error: (msg: string) => void
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type PluginContext<TConfig = never> = {
|
|
152
|
+
readonly name: string
|
|
153
|
+
readonly version: string | undefined
|
|
154
|
+
readonly agentDir: string
|
|
155
|
+
readonly config: TConfig
|
|
156
|
+
readonly logger: PluginLogger
|
|
157
|
+
spawnSubagent: (name: string, payload?: unknown) => Promise<void>
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export type PluginExports = {
|
|
161
|
+
tools?: Record<string, Tool<any>>
|
|
162
|
+
subagents?: Record<string, Subagent<any>>
|
|
163
|
+
cronJobs?: Record<string, PluginCronJob>
|
|
164
|
+
skills?: Record<string, PluginSkill>
|
|
165
|
+
skillsDirs?: string[]
|
|
166
|
+
hooks?: Hooks
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type DefinedPlugin<TConfig = never> = {
|
|
170
|
+
readonly configSchema?: z.ZodType<TConfig>
|
|
171
|
+
readonly plugin: (ctx: PluginContext<TConfig>) => Promise<PluginExports>
|
|
172
|
+
}
|