@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.
- package/LICENSE +21 -0
- package/channel.js +7 -0
- package/cli.js +5 -0
- package/mem.js +7 -0
- package/package.json +59 -0
- package/postinstall.js +75 -0
- package/src/AGENTS.md +169 -0
- package/src/channel-cli.ts +19 -0
- package/src/cli-argv.ts +27 -0
- package/src/cli.ts +63 -0
- package/src/commands/@core/adapter-option.ts +85 -0
- package/src/commands/@core/extra-options.ts +12 -0
- package/src/commands/@core/plugin-install.ts +1 -0
- package/src/commands/@core/plugin-source.ts +1 -0
- package/src/commands/accounts.ts +204 -0
- package/src/commands/adapter/prepare-selection.ts +181 -0
- package/src/commands/adapter/prepare.ts +104 -0
- package/src/commands/adapter.ts +48 -0
- package/src/commands/agent/actions.ts +176 -0
- package/src/commands/agent/runtime-store-commands.ts +56 -0
- package/src/commands/agent/runtime-store-events.ts +23 -0
- package/src/commands/agent/runtime-store-session.ts +170 -0
- package/src/commands/agent/runtime-store-shared.ts +139 -0
- package/src/commands/agent/runtime-store.ts +4 -0
- package/src/commands/agent.ts +81 -0
- package/src/commands/benchmark.ts +198 -0
- package/src/commands/channel.ts +594 -0
- package/src/commands/clear.ts +140 -0
- package/src/commands/config/actions.ts +196 -0
- package/src/commands/config/display-state.ts +108 -0
- package/src/commands/config/index.ts +135 -0
- package/src/commands/config/interactive.ts +121 -0
- package/src/commands/config/read-state.ts +56 -0
- package/src/commands/config/section-state.ts +109 -0
- package/src/commands/config/shared.ts +195 -0
- package/src/commands/kill.ts +41 -0
- package/src/commands/list.ts +224 -0
- package/src/commands/memory/context.ts +76 -0
- package/src/commands/memory/entries.ts +131 -0
- package/src/commands/memory/shared.ts +89 -0
- package/src/commands/memory/store.ts +69 -0
- package/src/commands/memory/target.ts +54 -0
- package/src/commands/memory.ts +97 -0
- package/src/commands/plugin.ts +62 -0
- package/src/commands/report-targets.ts +149 -0
- package/src/commands/report.ts +232 -0
- package/src/commands/run/adapter-cli-version.ts +65 -0
- package/src/commands/run/command.ts +982 -0
- package/src/commands/run/input-bridge.ts +108 -0
- package/src/commands/run/input-control.ts +112 -0
- package/src/commands/run/input-decision.ts +88 -0
- package/src/commands/run/options.ts +104 -0
- package/src/commands/run/output.ts +179 -0
- package/src/commands/run/permission-decision.ts +19 -0
- package/src/commands/run/permission-recovery.ts +194 -0
- package/src/commands/run/permission-state.ts +177 -0
- package/src/commands/run/print-idle-timeout.ts +47 -0
- package/src/commands/run/protocol-envelope.ts +111 -0
- package/src/commands/run/protocol-stdio.ts +71 -0
- package/src/commands/run/protocol.ts +391 -0
- package/src/commands/run/runtime-command-bridge.ts +190 -0
- package/src/commands/run/runtime-event-sink.ts +560 -0
- package/src/commands/run/session-exit-controller.ts +45 -0
- package/src/commands/run/types.ts +65 -0
- package/src/commands/run.ts +62 -0
- package/src/commands/session-control.ts +133 -0
- package/src/commands/skills/add-command.ts +88 -0
- package/src/commands/skills/install-command.ts +105 -0
- package/src/commands/skills/install.ts +216 -0
- package/src/commands/skills/progress.ts +126 -0
- package/src/commands/skills/publish-command.ts +85 -0
- package/src/commands/skills/register.ts +17 -0
- package/src/commands/skills/remove-command.ts +102 -0
- package/src/commands/skills/shared.ts +117 -0
- package/src/commands/skills/sync.ts +571 -0
- package/src/commands/skills/types.ts +33 -0
- package/src/commands/skills.ts +1 -0
- package/src/commands/stop.ts +41 -0
- package/src/config.ts +1 -0
- package/src/default-skill-plugin.ts +29 -0
- package/src/env.ts +1 -0
- package/src/hooks/plugins/index.ts +66 -0
- package/src/mem-cli.ts +19 -0
- package/src/session-cache.ts +250 -0
- package/src/session-permission-cache.ts +40 -0
- package/src/utils.ts +25 -0
- 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
|
+
}
|