@oneworks/cli 0.1.0-alpha.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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/channel.js +7 -0
  3. package/cli.js +5 -0
  4. package/mem.js +7 -0
  5. package/package.json +59 -0
  6. package/postinstall.js +75 -0
  7. package/src/AGENTS.md +169 -0
  8. package/src/channel-cli.ts +19 -0
  9. package/src/cli-argv.ts +27 -0
  10. package/src/cli.ts +63 -0
  11. package/src/commands/@core/adapter-option.ts +85 -0
  12. package/src/commands/@core/extra-options.ts +12 -0
  13. package/src/commands/@core/plugin-install.ts +1 -0
  14. package/src/commands/@core/plugin-source.ts +1 -0
  15. package/src/commands/accounts.ts +204 -0
  16. package/src/commands/adapter/prepare-selection.ts +181 -0
  17. package/src/commands/adapter/prepare.ts +104 -0
  18. package/src/commands/adapter.ts +48 -0
  19. package/src/commands/agent/actions.ts +176 -0
  20. package/src/commands/agent/runtime-store-commands.ts +56 -0
  21. package/src/commands/agent/runtime-store-events.ts +23 -0
  22. package/src/commands/agent/runtime-store-session.ts +170 -0
  23. package/src/commands/agent/runtime-store-shared.ts +139 -0
  24. package/src/commands/agent/runtime-store.ts +4 -0
  25. package/src/commands/agent.ts +81 -0
  26. package/src/commands/benchmark.ts +198 -0
  27. package/src/commands/channel.ts +594 -0
  28. package/src/commands/clear.ts +140 -0
  29. package/src/commands/config/actions.ts +196 -0
  30. package/src/commands/config/display-state.ts +108 -0
  31. package/src/commands/config/index.ts +135 -0
  32. package/src/commands/config/interactive.ts +121 -0
  33. package/src/commands/config/read-state.ts +56 -0
  34. package/src/commands/config/section-state.ts +109 -0
  35. package/src/commands/config/shared.ts +195 -0
  36. package/src/commands/kill.ts +41 -0
  37. package/src/commands/list.ts +224 -0
  38. package/src/commands/memory/context.ts +76 -0
  39. package/src/commands/memory/entries.ts +131 -0
  40. package/src/commands/memory/shared.ts +89 -0
  41. package/src/commands/memory/store.ts +69 -0
  42. package/src/commands/memory/target.ts +54 -0
  43. package/src/commands/memory.ts +97 -0
  44. package/src/commands/plugin.ts +62 -0
  45. package/src/commands/report-targets.ts +149 -0
  46. package/src/commands/report.ts +232 -0
  47. package/src/commands/run/adapter-cli-version.ts +65 -0
  48. package/src/commands/run/command.ts +982 -0
  49. package/src/commands/run/input-bridge.ts +108 -0
  50. package/src/commands/run/input-control.ts +112 -0
  51. package/src/commands/run/input-decision.ts +88 -0
  52. package/src/commands/run/options.ts +104 -0
  53. package/src/commands/run/output.ts +179 -0
  54. package/src/commands/run/permission-decision.ts +19 -0
  55. package/src/commands/run/permission-recovery.ts +194 -0
  56. package/src/commands/run/permission-state.ts +177 -0
  57. package/src/commands/run/print-idle-timeout.ts +47 -0
  58. package/src/commands/run/protocol-envelope.ts +111 -0
  59. package/src/commands/run/protocol-stdio.ts +71 -0
  60. package/src/commands/run/protocol.ts +391 -0
  61. package/src/commands/run/runtime-command-bridge.ts +190 -0
  62. package/src/commands/run/runtime-event-sink.ts +560 -0
  63. package/src/commands/run/session-exit-controller.ts +45 -0
  64. package/src/commands/run/types.ts +65 -0
  65. package/src/commands/run.ts +62 -0
  66. package/src/commands/session-control.ts +133 -0
  67. package/src/commands/skills/add-command.ts +88 -0
  68. package/src/commands/skills/install-command.ts +105 -0
  69. package/src/commands/skills/install.ts +216 -0
  70. package/src/commands/skills/progress.ts +126 -0
  71. package/src/commands/skills/publish-command.ts +85 -0
  72. package/src/commands/skills/register.ts +17 -0
  73. package/src/commands/skills/remove-command.ts +102 -0
  74. package/src/commands/skills/shared.ts +117 -0
  75. package/src/commands/skills/sync.ts +571 -0
  76. package/src/commands/skills/types.ts +33 -0
  77. package/src/commands/skills.ts +1 -0
  78. package/src/commands/stop.ts +41 -0
  79. package/src/config.ts +1 -0
  80. package/src/default-skill-plugin.ts +29 -0
  81. package/src/env.ts +1 -0
  82. package/src/hooks/plugins/index.ts +66 -0
  83. package/src/mem-cli.ts +19 -0
  84. package/src/session-cache.ts +250 -0
  85. package/src/session-permission-cache.ts +40 -0
  86. package/src/utils.ts +25 -0
  87. package/src/workspace.ts +12 -0
@@ -0,0 +1,131 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ import { resolveContext } from './context'
5
+ import { META_FILE_NAME, ensureRelativeMemoryPath, fromStorageSegment, normalizeScope, trimNonEmpty } from './shared'
6
+ import type { MemoryCommandOptions, MemoryScope } from './shared'
7
+
8
+ interface MemoryEntry {
9
+ channel?: string
10
+ id?: string
11
+ memoryPath: string
12
+ scope: MemoryScope
13
+ size: number
14
+ }
15
+
16
+ const decodeEntryParts = (scope: MemoryScope, parts: string[]) => {
17
+ if (scope === 'global') {
18
+ return { fileParts: parts, id: undefined, channel: undefined }
19
+ }
20
+ if (scope === 'session') {
21
+ return {
22
+ fileParts: parts.slice(1),
23
+ id: parts[0] == null ? undefined : fromStorageSegment(parts[0]),
24
+ channel: undefined
25
+ }
26
+ }
27
+ return {
28
+ fileParts: parts.slice(2),
29
+ id: parts[1] == null ? undefined : fromStorageSegment(parts[1]),
30
+ channel: parts[0] == null ? undefined : fromStorageSegment(parts[0])
31
+ }
32
+ }
33
+
34
+ const maybeAddEntry = async (
35
+ entries: MemoryEntry[],
36
+ filePath: string,
37
+ fileName: string,
38
+ options: {
39
+ channel?: string
40
+ id?: string
41
+ memoryPathFilter?: string
42
+ parts: string[]
43
+ scope: MemoryScope
44
+ }
45
+ ) => {
46
+ const decoded = decodeEntryParts(options.scope, options.parts)
47
+ if (options.channel != null && decoded.channel !== options.channel) return
48
+ if (options.id != null && decoded.id !== options.id) return
49
+ const memoryPath = decoded.fileParts.length === 0 ? fileName : [...decoded.fileParts, fileName].join('/')
50
+ if (options.memoryPathFilter != null && memoryPath !== options.memoryPathFilter) return
51
+
52
+ const stat = await fs.stat(filePath)
53
+ entries.push({
54
+ channel: decoded.channel,
55
+ id: decoded.id,
56
+ memoryPath,
57
+ scope: options.scope,
58
+ size: stat.size
59
+ })
60
+ }
61
+
62
+ const collectEntriesUnder = async (
63
+ root: string,
64
+ scope: MemoryScope,
65
+ channel: string | undefined,
66
+ id: string | undefined,
67
+ requestedPath: string | undefined
68
+ ) => {
69
+ const entries: MemoryEntry[] = []
70
+ const memoryPathFilter = requestedPath == null ? undefined : ensureRelativeMemoryPath(requestedPath)
71
+
72
+ const walk = async (dir: string, parts: string[]) => {
73
+ let items: import('node:fs').Dirent[]
74
+ try {
75
+ items = await fs.readdir(dir, { withFileTypes: true })
76
+ } catch (error) {
77
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return
78
+ throw error
79
+ }
80
+
81
+ for (const item of items) {
82
+ if (item.name === META_FILE_NAME) continue
83
+ const itemPath = path.resolve(dir, item.name)
84
+ if (item.isDirectory()) {
85
+ await walk(itemPath, [...parts, item.name])
86
+ } else if (item.isFile()) {
87
+ await maybeAddEntry(entries, itemPath, item.name, { channel, id, memoryPathFilter, parts, scope })
88
+ }
89
+ }
90
+ }
91
+
92
+ await walk(root, [])
93
+ return entries
94
+ }
95
+
96
+ const scopeRootName = (scope: MemoryScope) => {
97
+ if (scope === 'global') return 'global'
98
+ if (scope === 'channel') return 'channels'
99
+ if (scope === 'session') return 'sessions'
100
+ return 'users'
101
+ }
102
+
103
+ export const listMemoryEntries = async (options: MemoryCommandOptions) => {
104
+ const context = resolveContext(options)
105
+ const scope = normalizeScope(options.scope)
106
+ const entries = await collectEntriesUnder(
107
+ path.resolve(context.root, scopeRootName(scope)),
108
+ scope,
109
+ trimNonEmpty(options.channel),
110
+ trimNonEmpty(options.filter),
111
+ options.path
112
+ )
113
+ return entries.sort((left, right) =>
114
+ `${left.scope}:${left.channel ?? ''}:${left.id ?? ''}:${left.memoryPath}`.localeCompare(
115
+ `${right.scope}:${right.channel ?? ''}:${right.id ?? ''}:${right.memoryPath}`
116
+ )
117
+ )
118
+ }
119
+
120
+ export const formatEntries = (entries: MemoryEntry[]) => {
121
+ if (entries.length === 0) return 'No memories found.'
122
+ return entries.map(entry =>
123
+ [
124
+ entry.scope,
125
+ entry.channel,
126
+ entry.id,
127
+ entry.memoryPath,
128
+ `${entry.size}B`
129
+ ].filter(Boolean).join('\t')
130
+ ).join('\n')
131
+ }
@@ -0,0 +1,89 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import path from 'node:path'
3
+
4
+ export type MemoryScope = 'global' | 'channel' | 'session' | 'user'
5
+ export type MemoryAction = 'get' | 'list' | 'patch' | 'set'
6
+
7
+ export interface MemoryCommandOptions {
8
+ channel?: string
9
+ content?: string
10
+ cwd?: string
11
+ env?: NodeJS.ProcessEnv
12
+ filter?: string
13
+ path?: string
14
+ scope?: string
15
+ }
16
+
17
+ export interface MemoryCommandResult {
18
+ output: string
19
+ }
20
+
21
+ export interface MemoryContext {
22
+ channelId?: string
23
+ channelKey?: string
24
+ channelRef?: string
25
+ channelSessionType?: string
26
+ channelType?: string
27
+ root: string
28
+ senderId?: string
29
+ sessionId?: string
30
+ }
31
+
32
+ export interface MemoryTarget {
33
+ dir: string
34
+ displayId?: string
35
+ filePath: string
36
+ memoryPath: string
37
+ scope: MemoryScope
38
+ }
39
+
40
+ export const DEFAULT_MEMORY_PATH = 'README.md'
41
+ export const META_FILE_NAME = '.oneworks-mem.json'
42
+
43
+ const MEMORY_SCOPES = new Set<MemoryScope>(['global', 'channel', 'session', 'user'])
44
+
45
+ export const trimNonEmpty = (value: unknown) => {
46
+ if (typeof value !== 'string') return undefined
47
+ const trimmed = value.trim()
48
+ return trimmed === '' ? undefined : trimmed
49
+ }
50
+
51
+ export const normalizeScope = (value: string | undefined): MemoryScope => {
52
+ const scope = trimNonEmpty(value) ?? 'channel'
53
+ if (MEMORY_SCOPES.has(scope as MemoryScope)) return scope as MemoryScope
54
+ throw new Error(`Unsupported memory scope: ${scope}. Supported: global, channel, session, user.`)
55
+ }
56
+
57
+ export const ensureRelativeMemoryPath = (value: string | undefined) => {
58
+ const raw = trimNonEmpty(value) ?? DEFAULT_MEMORY_PATH
59
+ if (raw.includes('\0')) throw new Error('Memory path cannot contain NUL bytes.')
60
+ if (path.isAbsolute(raw)) throw new Error('Memory path must be relative.')
61
+
62
+ const normalized = path.posix.normalize(raw.replaceAll('\\', '/')).replace(/^\.\//u, '')
63
+ if (
64
+ normalized === '' ||
65
+ normalized === '.' ||
66
+ normalized === '..' ||
67
+ normalized.startsWith('../')
68
+ ) {
69
+ throw new Error('Memory path must stay inside the selected memory id.')
70
+ }
71
+ return normalized
72
+ }
73
+
74
+ export const toStorageSegment = (value: string) => Buffer.from(value, 'utf8').toString('base64url')
75
+
76
+ export const fromStorageSegment = (value: string) => {
77
+ try {
78
+ return Buffer.from(value, 'base64url').toString('utf8')
79
+ } catch {
80
+ return value
81
+ }
82
+ }
83
+
84
+ export const formatTargetLabel = (target: MemoryTarget) =>
85
+ [
86
+ target.scope,
87
+ target.displayId,
88
+ target.memoryPath
89
+ ].filter(Boolean).join(':')
@@ -0,0 +1,69 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ import { formatEntries, listMemoryEntries } from './entries'
5
+ import { META_FILE_NAME, formatTargetLabel } from './shared'
6
+ import type { MemoryCommandOptions, MemoryContext, MemoryTarget } from './shared'
7
+ import { resolveTarget } from './target'
8
+
9
+ const writeMeta = async (target: MemoryTarget, context: MemoryContext) => {
10
+ await fs.mkdir(target.dir, { recursive: true })
11
+ await fs.writeFile(
12
+ path.resolve(target.dir, META_FILE_NAME),
13
+ `${
14
+ JSON.stringify(
15
+ {
16
+ channel: context.channelRef,
17
+ channelId: context.channelId,
18
+ channelKey: context.channelKey,
19
+ channelSessionType: context.channelSessionType,
20
+ channelType: context.channelType,
21
+ id: target.displayId,
22
+ scope: target.scope,
23
+ senderId: context.senderId,
24
+ sessionId: context.sessionId,
25
+ updatedAt: Date.now()
26
+ },
27
+ null,
28
+ 2
29
+ )
30
+ }\n`
31
+ )
32
+ }
33
+
34
+ const withTrailingNewline = (value: string) => value.endsWith('\n') ? value : `${value}\n`
35
+
36
+ export const readFileIfPresent = async (filePath: string) => {
37
+ try {
38
+ return await fs.readFile(filePath, 'utf8')
39
+ } catch (error) {
40
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return ''
41
+ throw error
42
+ }
43
+ }
44
+
45
+ export const readMemory = async (options: MemoryCommandOptions) => {
46
+ const { target } = resolveTarget(options)
47
+ return await readFileIfPresent(target.filePath)
48
+ }
49
+
50
+ export const listMemory = async (options: MemoryCommandOptions) => formatEntries(await listMemoryEntries(options))
51
+
52
+ export const writeMemory = async (mode: 'patch' | 'set', options: MemoryCommandOptions) => {
53
+ const { context, target } = resolveTarget(options)
54
+ const content = options.content ?? ''
55
+ await fs.mkdir(path.dirname(target.filePath), { recursive: true })
56
+ await writeMeta(target, context)
57
+
58
+ if (mode === 'set') {
59
+ await fs.writeFile(target.filePath, withTrailingNewline(content))
60
+ } else {
61
+ const current = await readFileIfPresent(target.filePath)
62
+ const next = current === ''
63
+ ? withTrailingNewline(content)
64
+ : `${current}${current.endsWith('\n') ? '' : '\n'}${withTrailingNewline(content)}`
65
+ await fs.writeFile(target.filePath, next)
66
+ }
67
+
68
+ return `Memory ${mode === 'set' ? 'written' : 'patched'}: ${formatTargetLabel(target)}`
69
+ }
@@ -0,0 +1,54 @@
1
+ import path from 'node:path'
2
+
3
+ import { resolveContext } from './context'
4
+ import type { MemoryCommandOptions, MemoryContext, MemoryScope, MemoryTarget } from './shared'
5
+ import { ensureRelativeMemoryPath, normalizeScope, toStorageSegment, trimNonEmpty } from './shared'
6
+
7
+ const requireValue = (value: string | undefined, message: string) => {
8
+ if (value != null && value !== '') return value
9
+ throw new Error(message)
10
+ }
11
+
12
+ const resolveTargetId = (scope: MemoryScope, context: MemoryContext, filter: string | undefined) => {
13
+ const explicitId = trimNonEmpty(filter)
14
+ if (scope === 'global') return undefined
15
+ if (scope === 'channel') {
16
+ return requireValue(explicitId ?? context.channelId, 'Missing channel memory id. Pass -f/--filter.')
17
+ }
18
+ if (scope === 'session') {
19
+ return requireValue(explicitId ?? context.sessionId, 'Missing session memory id. Pass -f/--filter.')
20
+ }
21
+ return requireValue(explicitId ?? context.senderId, 'Missing user memory id. Pass -f/--filter.')
22
+ }
23
+
24
+ const resolveTargetDir = (scope: MemoryScope, context: MemoryContext, displayId?: string) => {
25
+ if (scope === 'global') return path.resolve(context.root, 'global')
26
+ if (scope === 'session') {
27
+ return path.resolve(context.root, 'sessions', toStorageSegment(requireValue(displayId, 'Missing session id.')))
28
+ }
29
+
30
+ const channelRef = requireValue(context.channelRef, 'Missing channel. Pass -c/--channel.')
31
+ const rootName = scope === 'channel' ? 'channels' : 'users'
32
+ return path.resolve(
33
+ context.root,
34
+ rootName,
35
+ toStorageSegment(channelRef),
36
+ toStorageSegment(requireValue(displayId, `Missing ${scope} memory id.`))
37
+ )
38
+ }
39
+
40
+ export const resolveTarget = (options: MemoryCommandOptions) => {
41
+ const context = resolveContext(options)
42
+ const scope = normalizeScope(options.scope)
43
+ const displayId = resolveTargetId(scope, context, options.filter)
44
+ const memoryPath = ensureRelativeMemoryPath(options.path)
45
+ const dir = resolveTargetDir(scope, context, displayId)
46
+ const filePath = path.resolve(dir, ...memoryPath.split('/'))
47
+ const relativePath = path.relative(dir, filePath)
48
+
49
+ if (relativePath === '..' || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) {
50
+ throw new Error('Memory path resolved outside the selected memory id.')
51
+ }
52
+
53
+ return { context, target: { dir, displayId, filePath, memoryPath, scope } satisfies MemoryTarget }
54
+ }
@@ -0,0 +1,97 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import process from 'node:process'
3
+
4
+ import type { Command } from 'commander'
5
+
6
+ import { DEFAULT_MEMORY_PATH } from './memory/shared'
7
+ import type { MemoryAction, MemoryCommandOptions, MemoryCommandResult } from './memory/shared'
8
+ import { listMemory, readMemory, writeMemory } from './memory/store'
9
+
10
+ export type { MemoryAction, MemoryCommandOptions, MemoryCommandResult, MemoryScope } from './memory/shared'
11
+
12
+ export const runMemoryCommand = async (
13
+ action: MemoryAction,
14
+ options: MemoryCommandOptions = {}
15
+ ): Promise<MemoryCommandResult> => {
16
+ if (action === 'list') {
17
+ return { output: await listMemory(options) }
18
+ }
19
+ if (action === 'get') {
20
+ return { output: await readMemory(options) }
21
+ }
22
+ return { output: await writeMemory(action, options) }
23
+ }
24
+
25
+ const readStdin = async () => {
26
+ if (process.stdin.isTTY) return ''
27
+
28
+ const chunks: Buffer[] = []
29
+ for await (const chunk of process.stdin) {
30
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)))
31
+ }
32
+ return Buffer.concat(chunks).toString('utf8')
33
+ }
34
+
35
+ const joinContent = async (parts: string[]) => {
36
+ const value = parts.join(' ')
37
+ if (value !== '') return value
38
+ return await readStdin()
39
+ }
40
+
41
+ const printResult = (output: string) => {
42
+ if (output === '') return
43
+ process.stdout.write(output.endsWith('\n') ? output : `${output}\n`)
44
+ }
45
+
46
+ const addCommonOptions = (command: Command, options: { defaultPath?: boolean } = {}) => {
47
+ const withPath = options.defaultPath === false
48
+ ? command.option('-p, --path <path>', 'Memory file path under the selected id')
49
+ : command.option('-p, --path <path>', 'Memory file path under the selected id', DEFAULT_MEMORY_PATH)
50
+ return withPath
51
+ .option('-c, --channel <channel>', 'Channel type or channel ref, for example wechat')
52
+ .option('-f, --filter <id>', 'Memory id to target or filter')
53
+ .option('-s, --scope <scope>', 'Memory scope: global, channel, session, or user', 'channel')
54
+ }
55
+
56
+ const run = async (action: MemoryAction, opts: MemoryCommandOptions) => {
57
+ const result = await runMemoryCommand(action, opts)
58
+ printResult(result.output)
59
+ }
60
+
61
+ export const registerMemorySubcommands = (command: Command) => {
62
+ addCommonOptions(command.command('set'))
63
+ .description('Overwrite a memory file')
64
+ .argument('[content...]', 'Content to write. Reads stdin when omitted.')
65
+ .action(async (content: string[], options: MemoryCommandOptions) => {
66
+ await run('set', { ...options, content: await joinContent(content) })
67
+ })
68
+
69
+ addCommonOptions(command.command('patch'))
70
+ .description('Append content to a memory file')
71
+ .argument('[content...]', 'Content to append. Reads stdin when omitted.')
72
+ .action(async (content: string[], options: MemoryCommandOptions) => {
73
+ await run('patch', { ...options, content: await joinContent(content) })
74
+ })
75
+
76
+ addCommonOptions(command.command('get'))
77
+ .description('Print a memory file')
78
+ .action(async (options: MemoryCommandOptions) => {
79
+ await run('get', options)
80
+ })
81
+
82
+ addCommonOptions(command.command('list'), { defaultPath: false })
83
+ .description('List available memory files')
84
+ .action(async (options: MemoryCommandOptions) => {
85
+ await run('list', options)
86
+ })
87
+
88
+ return command
89
+ }
90
+
91
+ export const registerMemoryCommand = (program: Command) => {
92
+ registerMemorySubcommands(
93
+ program
94
+ .command('mem')
95
+ .description('Read and write OneWorks memory from agent sessions')
96
+ )
97
+ }
@@ -0,0 +1,62 @@
1
+ import process from 'node:process'
2
+
3
+ import { buildConfigJsonVariables, loadConfig, mergeConfigs } from '@oneworks/config'
4
+ import { mergeProcessEnvWithProjectEnv } from '@oneworks/utils'
5
+ import type { Command } from 'commander'
6
+
7
+ import { resolveCliWorkspaceCwd } from '#~/workspace.js'
8
+ import { createAdapterOption, normalizeCliAdapterOptionValue } from './@core/adapter-option'
9
+ import { addAdapterPlugin } from './@core/plugin-install'
10
+
11
+ const normalizeManagedPluginAdapter = (adapter: string) => adapter === 'claude-code' ? 'claude' : adapter
12
+
13
+ export const resolvePluginCommandAdapter = async (
14
+ explicitAdapter: string | undefined,
15
+ cwd: string = process.cwd()
16
+ ) => {
17
+ const normalizedExplicitAdapter = explicitAdapter == null
18
+ ? undefined
19
+ : normalizeCliAdapterOptionValue(explicitAdapter)
20
+ if (normalizedExplicitAdapter) return normalizeManagedPluginAdapter(normalizedExplicitAdapter)
21
+
22
+ const env = mergeProcessEnvWithProjectEnv(undefined, { workspaceFolder: cwd })
23
+ const [projectConfig, userConfig] = await loadConfig({
24
+ cwd,
25
+ env,
26
+ jsonVariables: buildConfigJsonVariables(cwd, env)
27
+ })
28
+ return normalizeManagedPluginAdapter(
29
+ mergeConfigs(projectConfig, userConfig)?.defaultAdapter ?? 'claude'
30
+ )
31
+ }
32
+
33
+ export function registerPluginCommand(program: Command) {
34
+ const pluginCommand = program
35
+ .command('plugin')
36
+ .description('Install and manage adapter-native plugins')
37
+ .addOption(createAdapterOption('Plugin adapter type'))
38
+
39
+ pluginCommand
40
+ .command('add <source>')
41
+ .description('Install an adapter-native plugin from local sources, package registries, or configured marketplaces')
42
+ .option('--force', 'Replace the existing installed plugin if it already exists', false)
43
+ .option('--scope <scope>', 'Override the One Works scope used for converted assets')
44
+ .action(async (source: string, opts: { force?: boolean; scope?: string }, command: Command) => {
45
+ try {
46
+ const parentOptions = command.parent?.opts() as { adapter?: string } | undefined
47
+ const cwd = resolveCliWorkspaceCwd()
48
+ const env = mergeProcessEnvWithProjectEnv(undefined, { workspaceFolder: cwd })
49
+ const adapter = await resolvePluginCommandAdapter(parentOptions?.adapter, cwd)
50
+ await addAdapterPlugin(adapter, {
51
+ cwd,
52
+ env,
53
+ source,
54
+ force: opts.force,
55
+ scope: opts.scope
56
+ })
57
+ } catch (error) {
58
+ console.error(error instanceof Error ? error.message : String(error))
59
+ process.exit(1)
60
+ }
61
+ })
62
+ }
@@ -0,0 +1,149 @@
1
+ import { constants } from 'node:fs'
2
+ import type { Dirent } from 'node:fs'
3
+ import fs from 'node:fs/promises'
4
+ import path from 'node:path'
5
+
6
+ import {
7
+ mergeProcessEnvWithProjectEnv,
8
+ migrateProjectHomeSegments,
9
+ resolveProjectHomePath,
10
+ resolveProjectOoPath
11
+ } from '@oneworks/utils'
12
+
13
+ const REPORT_TARGETS = ['logs', 'caches'] as const
14
+ const REPORT_MOCK_TARGETS = [
15
+ '.mock/.claude',
16
+ '.mock/.claude-code-router',
17
+ '.mock/.config',
18
+ '.mock/.codex',
19
+ '.mock/.oneworks'
20
+ ] as const
21
+ const REPORT_MOCK_FILE_PREFIX = '.claude.json'
22
+
23
+ const collectExistingTargets = async (cwd: string, env: NodeJS.ProcessEnv, targets: readonly string[]) => {
24
+ const availableTargets: string[] = []
25
+
26
+ for (const target of targets) {
27
+ try {
28
+ const resolvedTarget = resolveProjectOoPath(cwd, env, ...target.split('/'))
29
+ await fs.access(resolvedTarget, constants.F_OK)
30
+ availableTargets.push(resolvedTarget)
31
+ } catch {
32
+ // ignore missing targets
33
+ }
34
+ }
35
+
36
+ return availableTargets
37
+ }
38
+
39
+ const collectMockReportTargets = async (cwd: string, env: NodeJS.ProcessEnv) => {
40
+ const availableTargets = await collectExistingTargets(cwd, env, REPORT_MOCK_TARGETS)
41
+ const mockRoot = resolveProjectOoPath(cwd, env, '.mock')
42
+
43
+ try {
44
+ const entries = await fs.readdir(mockRoot, { withFileTypes: true })
45
+ const mockFiles = entries
46
+ .filter(entry =>
47
+ entry.isFile() && (
48
+ entry.name === REPORT_MOCK_FILE_PREFIX ||
49
+ entry.name.startsWith(`${REPORT_MOCK_FILE_PREFIX}.backup`)
50
+ )
51
+ )
52
+ .map(entry => path.resolve(mockRoot, entry.name))
53
+ .sort((left, right) => left.localeCompare(right))
54
+
55
+ availableTargets.push(...mockFiles)
56
+ } catch {
57
+ // ignore missing mock root
58
+ }
59
+
60
+ return availableTargets
61
+ }
62
+
63
+ const isPathInside = (target: string, source: string) => {
64
+ const relativePath = path.relative(source, target)
65
+ return relativePath === '' || (
66
+ relativePath !== '..' &&
67
+ !relativePath.startsWith(`..${path.sep}`) &&
68
+ !path.isAbsolute(relativePath)
69
+ )
70
+ }
71
+
72
+ const isLoggerPayloadRefPath = (entryPath: string, logRoot: string) => (
73
+ path.relative(logRoot, entryPath)
74
+ .split(path.sep)
75
+ .some(part => part.endsWith('.payloads'))
76
+ )
77
+
78
+ const findLoggerPayloadRefTargets = async (params: {
79
+ logRoot: string
80
+ payloadStoreRoot: string
81
+ }): Promise<string[]> => {
82
+ const targets = new Set<string>()
83
+
84
+ const visit = async (current: string) => {
85
+ let entries: Dirent[]
86
+ try {
87
+ entries = await fs.readdir(current, { withFileTypes: true })
88
+ } catch {
89
+ return
90
+ }
91
+
92
+ await Promise.all(entries.map(async (entry) => {
93
+ const entryPath = path.resolve(current, entry.name)
94
+ if (entry.isDirectory()) {
95
+ await visit(entryPath)
96
+ return
97
+ }
98
+
99
+ if (!entry.isSymbolicLink() || !isLoggerPayloadRefPath(entryPath, params.logRoot)) return
100
+
101
+ try {
102
+ const linkTarget = await fs.readlink(entryPath)
103
+ const resolvedTarget = path.resolve(path.dirname(entryPath), linkTarget)
104
+ if (!isPathInside(resolvedTarget, params.payloadStoreRoot)) return
105
+ await fs.access(resolvedTarget, constants.F_OK)
106
+ targets.add(resolvedTarget)
107
+ } catch {
108
+ // ignore dangling symlinks
109
+ }
110
+ }))
111
+ }
112
+
113
+ await visit(params.logRoot)
114
+ return [...targets].sort((left, right) => left.localeCompare(right))
115
+ }
116
+
117
+ const collectLoggerPayloadReportTargets = async (cwd: string, env: NodeJS.ProcessEnv) => {
118
+ const projectHomeLogsDir = resolveProjectHomePath(cwd, env, 'logs')
119
+ const projectHomePayloadStoreDir = resolveProjectHomePath(cwd, env, 'caches', '.logger-payloads', 'sha256')
120
+ const logRoots = new Set<string>()
121
+
122
+ try {
123
+ await fs.access(projectHomeLogsDir, constants.F_OK)
124
+ logRoots.add(projectHomeLogsDir)
125
+ } catch {
126
+ // ignore missing project-home logs
127
+ }
128
+ const targets = await Promise.all([...logRoots].map(logRoot =>
129
+ findLoggerPayloadRefTargets({
130
+ logRoot,
131
+ payloadStoreRoot: projectHomePayloadStoreDir
132
+ })
133
+ ))
134
+ return [...new Set(targets.flat())].sort((left, right) => left.localeCompare(right))
135
+ }
136
+
137
+ export const collectReportTargets = async (
138
+ cwd: string,
139
+ env: NodeJS.ProcessEnv = mergeProcessEnvWithProjectEnv(undefined, { workspaceFolder: cwd })
140
+ ) => {
141
+ await migrateProjectHomeSegments(cwd, env, ['logs', 'caches', '.mock'])
142
+ const availableTargets = await collectExistingTargets(cwd, env, REPORT_TARGETS)
143
+ const loggerPayloadTargets = await collectLoggerPayloadReportTargets(cwd, env)
144
+ availableTargets.push(
145
+ ...loggerPayloadTargets.filter(target => !availableTargets.some(source => isPathInside(target, source)))
146
+ )
147
+ availableTargets.push(...await collectMockReportTargets(cwd, env))
148
+ return availableTargets
149
+ }