@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,56 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getConfigSectionValueAtPath,
|
|
5
|
+
hasConfigSectionValue,
|
|
6
|
+
parseConfigSectionPath,
|
|
7
|
+
resolveConfigSectionPath
|
|
8
|
+
} from '@oneworks/config'
|
|
9
|
+
|
|
10
|
+
import { loadCommandState, resolveSourceConfig, resolveSourceSections } from './shared'
|
|
11
|
+
import type { ConfigReadSource, LoadedConfigCommandState } from './shared'
|
|
12
|
+
|
|
13
|
+
export interface ResolvedReadState {
|
|
14
|
+
cwd: string
|
|
15
|
+
source: ConfigReadSource
|
|
16
|
+
state: LoadedConfigCommandState
|
|
17
|
+
resolvedPath?: ReturnType<typeof resolveConfigSectionPath>
|
|
18
|
+
value: unknown
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const resolveReadState = async (
|
|
22
|
+
pathInput: string | undefined,
|
|
23
|
+
sourceInput: ConfigReadSource | undefined
|
|
24
|
+
): Promise<ResolvedReadState> => {
|
|
25
|
+
const cwd = process.cwd()
|
|
26
|
+
const source = sourceInput ?? 'merged'
|
|
27
|
+
const state = await loadCommandState(cwd)
|
|
28
|
+
const sourceConfig = resolveSourceConfig(state, source)
|
|
29
|
+
|
|
30
|
+
if (source !== 'merged' && sourceConfig == null) {
|
|
31
|
+
throw new Error(`No ${source} config found for workspace "${cwd}".`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (pathInput == null || pathInput.trim() === '') {
|
|
35
|
+
return {
|
|
36
|
+
cwd,
|
|
37
|
+
source,
|
|
38
|
+
state,
|
|
39
|
+
value: resolveSourceSections(state, source)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const resolvedPath = resolveConfigSectionPath(parseConfigSectionPath(pathInput))
|
|
44
|
+
const result = getConfigSectionValueAtPath(resolveSourceSections(state, source), resolvedPath)
|
|
45
|
+
if (!result.exists || (resolvedPath.sectionPath.length === 0 && !hasConfigSectionValue(result.value))) {
|
|
46
|
+
throw new Error(`Config path "${resolvedPath.normalizedPath}" was not found in ${source} config.`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
cwd,
|
|
51
|
+
source,
|
|
52
|
+
state,
|
|
53
|
+
resolvedPath,
|
|
54
|
+
value: result.value
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { CONFIG_SECTION_KEYS, hasConfigSectionValue } from '@oneworks/config'
|
|
2
|
+
import type { ConfigSectionKey, ConfigSections } from '@oneworks/config'
|
|
3
|
+
|
|
4
|
+
import { CONFIG_READ_SOURCES } from './shared'
|
|
5
|
+
import type { ConfigListSource, LoadedConfigCommandState } from './shared'
|
|
6
|
+
|
|
7
|
+
export const resolveListOutput = (
|
|
8
|
+
state: LoadedConfigCommandState,
|
|
9
|
+
source: ConfigListSource
|
|
10
|
+
) => {
|
|
11
|
+
if (source !== 'all') {
|
|
12
|
+
return Object.fromEntries(
|
|
13
|
+
CONFIG_SECTION_KEYS.map(section => [
|
|
14
|
+
section,
|
|
15
|
+
hasConfigSectionValue(state.sections[source][section])
|
|
16
|
+
])
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const sources = source === 'all' ? CONFIG_READ_SOURCES : [source]
|
|
21
|
+
|
|
22
|
+
return Object.fromEntries(
|
|
23
|
+
CONFIG_SECTION_KEYS.map((section) => [
|
|
24
|
+
section,
|
|
25
|
+
Object.fromEntries(
|
|
26
|
+
sources.map(currentSource => [
|
|
27
|
+
currentSource,
|
|
28
|
+
hasConfigSectionValue(state.sections[currentSource][section])
|
|
29
|
+
])
|
|
30
|
+
)
|
|
31
|
+
])
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const resolveTextListRows = (
|
|
36
|
+
state: LoadedConfigCommandState,
|
|
37
|
+
source: ConfigListSource
|
|
38
|
+
) => {
|
|
39
|
+
if (source !== 'all') {
|
|
40
|
+
return CONFIG_SECTION_KEYS.map((section) => ({
|
|
41
|
+
Section: section,
|
|
42
|
+
Present: hasConfigSectionValue(state.sections[source][section]) ? 'yes' : ''
|
|
43
|
+
}))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const sources = source === 'all' ? CONFIG_READ_SOURCES : [source]
|
|
47
|
+
|
|
48
|
+
return CONFIG_SECTION_KEYS.map((section) => (
|
|
49
|
+
Object.fromEntries([
|
|
50
|
+
['Section', section],
|
|
51
|
+
...sources.map(currentSource => [
|
|
52
|
+
currentSource[0]!.toUpperCase() + currentSource.slice(1),
|
|
53
|
+
hasConfigSectionValue(state.sections[currentSource][section]) ? 'yes' : ''
|
|
54
|
+
])
|
|
55
|
+
])
|
|
56
|
+
))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const resolveClearedSectionValue = (section: ConfigSectionKey): ConfigSections[ConfigSectionKey] => {
|
|
60
|
+
switch (section) {
|
|
61
|
+
case 'general':
|
|
62
|
+
return {
|
|
63
|
+
baseDir: undefined,
|
|
64
|
+
disableGlobalConfig: undefined,
|
|
65
|
+
effort: undefined,
|
|
66
|
+
defaultAdapter: undefined,
|
|
67
|
+
defaultModelService: undefined,
|
|
68
|
+
defaultModel: undefined,
|
|
69
|
+
recommendedModels: undefined,
|
|
70
|
+
interfaceLanguage: undefined,
|
|
71
|
+
modelLanguage: undefined,
|
|
72
|
+
announcements: undefined,
|
|
73
|
+
permissions: undefined,
|
|
74
|
+
env: undefined,
|
|
75
|
+
notifications: undefined,
|
|
76
|
+
messageLinks: undefined,
|
|
77
|
+
skills: undefined,
|
|
78
|
+
skillsMeta: undefined,
|
|
79
|
+
skillRegistries: undefined,
|
|
80
|
+
webAuth: undefined,
|
|
81
|
+
shortcuts: undefined
|
|
82
|
+
}
|
|
83
|
+
case 'plugins':
|
|
84
|
+
return {
|
|
85
|
+
plugins: undefined,
|
|
86
|
+
marketplaces: undefined
|
|
87
|
+
}
|
|
88
|
+
case 'mcp':
|
|
89
|
+
return {
|
|
90
|
+
mcpServers: undefined,
|
|
91
|
+
defaultIncludeMcpServers: undefined,
|
|
92
|
+
defaultExcludeMcpServers: undefined,
|
|
93
|
+
noDefaultOneworksMcpServer: undefined
|
|
94
|
+
}
|
|
95
|
+
case 'conversation':
|
|
96
|
+
case 'models':
|
|
97
|
+
case 'modelServices':
|
|
98
|
+
case 'workspaces':
|
|
99
|
+
case 'channels':
|
|
100
|
+
case 'adapters':
|
|
101
|
+
case 'appearance':
|
|
102
|
+
case 'desktop':
|
|
103
|
+
case 'auth':
|
|
104
|
+
case 'shortcuts':
|
|
105
|
+
case 'experiments':
|
|
106
|
+
case 'diagnostics':
|
|
107
|
+
return {}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildConfigJsonVariables,
|
|
5
|
+
buildConfigSections,
|
|
6
|
+
formatConfigValueAsYaml,
|
|
7
|
+
loadConfigState,
|
|
8
|
+
resolveConfigSectionWriteError
|
|
9
|
+
} from '@oneworks/config'
|
|
10
|
+
import type { ConfigSectionKey } from '@oneworks/config'
|
|
11
|
+
import type { Config, ConfigSource } from '@oneworks/types'
|
|
12
|
+
|
|
13
|
+
export type ConfigReadSource = ConfigSource | 'merged'
|
|
14
|
+
export type ConfigListSource = ConfigReadSource | 'all'
|
|
15
|
+
|
|
16
|
+
export const CONFIG_READ_SOURCES = ['global', 'project', 'user', 'merged'] as const
|
|
17
|
+
export const CONFIG_SET_SOURCES = ['global', 'project', 'user'] as const
|
|
18
|
+
export const CONFIG_LIST_SOURCES = ['all', ...CONFIG_READ_SOURCES] as const
|
|
19
|
+
export const CONFIG_VALUE_TYPES = ['auto', 'string', 'json', 'number', 'boolean', 'null'] as const
|
|
20
|
+
|
|
21
|
+
export type ConfigValueType = typeof CONFIG_VALUE_TYPES[number]
|
|
22
|
+
|
|
23
|
+
export interface ConfigListOptions {
|
|
24
|
+
json?: boolean
|
|
25
|
+
source?: ConfigListSource
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ConfigGetOptions {
|
|
29
|
+
json?: boolean
|
|
30
|
+
source?: ConfigReadSource
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ConfigSetOptions {
|
|
34
|
+
json?: boolean
|
|
35
|
+
source?: ConfigSource
|
|
36
|
+
type?: ConfigValueType
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ConfigUnsetOptions {
|
|
40
|
+
json?: boolean
|
|
41
|
+
source?: ConfigSource
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface LoadedConfigCommandState {
|
|
45
|
+
workspaceFolder: string
|
|
46
|
+
effectiveProjectConfig?: Config
|
|
47
|
+
globalConfig?: Config
|
|
48
|
+
globalSourceConfig?: Config
|
|
49
|
+
projectSourceConfig?: Config
|
|
50
|
+
projectConfig?: Config
|
|
51
|
+
userSourceConfig?: Config
|
|
52
|
+
userConfig?: Config
|
|
53
|
+
mergedConfig: Config
|
|
54
|
+
sections: {
|
|
55
|
+
global: ReturnType<typeof buildConfigSections>
|
|
56
|
+
project: ReturnType<typeof buildConfigSections>
|
|
57
|
+
user: ReturnType<typeof buildConfigSections>
|
|
58
|
+
merged: ReturnType<typeof buildConfigSections>
|
|
59
|
+
}
|
|
60
|
+
present: Record<ConfigReadSource, boolean>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const isInteractiveTerminal = () => process.stdin.isTTY && process.stdout.isTTY
|
|
64
|
+
|
|
65
|
+
export const formatDisplayValue = (value: unknown) => formatConfigValueAsYaml(value)
|
|
66
|
+
|
|
67
|
+
export const formatErrorMessage = (error: unknown) => error instanceof Error ? error.message : String(error)
|
|
68
|
+
|
|
69
|
+
export const printJsonResult = (value: unknown) => {
|
|
70
|
+
console.log(JSON.stringify(value, null, 2))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const loadCommandState = async (cwd: string): Promise<LoadedConfigCommandState> => {
|
|
74
|
+
const configState = await loadConfigState({
|
|
75
|
+
cwd,
|
|
76
|
+
jsonVariables: buildConfigJsonVariables(cwd, process.env)
|
|
77
|
+
})
|
|
78
|
+
const globalSourceConfig = configState.globalSource?.rawConfig
|
|
79
|
+
const projectSourceConfig = configState.projectSource?.rawConfig
|
|
80
|
+
const userSourceConfig = configState.userSource?.rawConfig
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
workspaceFolder: cwd,
|
|
84
|
+
globalSourceConfig,
|
|
85
|
+
projectSourceConfig,
|
|
86
|
+
userSourceConfig,
|
|
87
|
+
...configState,
|
|
88
|
+
sections: {
|
|
89
|
+
global: buildConfigSections(globalSourceConfig),
|
|
90
|
+
project: buildConfigSections(projectSourceConfig),
|
|
91
|
+
user: buildConfigSections(userSourceConfig),
|
|
92
|
+
merged: buildConfigSections(configState.mergedConfig)
|
|
93
|
+
},
|
|
94
|
+
present: {
|
|
95
|
+
global: configState.globalSource?.configPath != null,
|
|
96
|
+
project: configState.projectSource?.configPath != null,
|
|
97
|
+
user: configState.userSource?.configPath != null,
|
|
98
|
+
merged: true
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const resolveSourceConfig = (
|
|
104
|
+
state: LoadedConfigCommandState,
|
|
105
|
+
source: ConfigReadSource
|
|
106
|
+
) => {
|
|
107
|
+
switch (source) {
|
|
108
|
+
case 'global':
|
|
109
|
+
return state.globalSourceConfig
|
|
110
|
+
case 'project':
|
|
111
|
+
return state.projectSourceConfig
|
|
112
|
+
case 'user':
|
|
113
|
+
return state.userSourceConfig
|
|
114
|
+
case 'merged':
|
|
115
|
+
return state.mergedConfig
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const resolveSourceSections = (
|
|
120
|
+
state: LoadedConfigCommandState,
|
|
121
|
+
source: ConfigReadSource
|
|
122
|
+
) => state.sections[source]
|
|
123
|
+
|
|
124
|
+
export const assertWritableConfigSection = (source: ConfigSource, section: ConfigSectionKey) => {
|
|
125
|
+
const writeError = resolveConfigSectionWriteError(source, section)
|
|
126
|
+
if (writeError != null) {
|
|
127
|
+
throw new Error(writeError)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const parseConfigValueInput = (
|
|
132
|
+
rawValue: string | undefined,
|
|
133
|
+
type: ConfigValueType
|
|
134
|
+
): unknown => {
|
|
135
|
+
if (type === 'null') {
|
|
136
|
+
return null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (rawValue == null) {
|
|
140
|
+
throw new TypeError('A config value is required.')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (type === 'string') {
|
|
144
|
+
return rawValue
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const trimmed = rawValue.trim()
|
|
148
|
+
|
|
149
|
+
if (type === 'json') {
|
|
150
|
+
return JSON.parse(trimmed)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (type === 'number') {
|
|
154
|
+
const parsed = Number(trimmed)
|
|
155
|
+
if (!Number.isFinite(parsed)) {
|
|
156
|
+
throw new TypeError(`Invalid number value "${rawValue}".`)
|
|
157
|
+
}
|
|
158
|
+
return parsed
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (type === 'boolean') {
|
|
162
|
+
if (trimmed === 'true') return true
|
|
163
|
+
if (trimmed === 'false') return false
|
|
164
|
+
throw new TypeError(`Invalid boolean value "${rawValue}". Expected true or false.`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (trimmed === '') {
|
|
168
|
+
return ''
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const looksLikeJsonLiteral = trimmed.startsWith('{') ||
|
|
172
|
+
trimmed.startsWith('[') ||
|
|
173
|
+
trimmed.startsWith('"') ||
|
|
174
|
+
trimmed === 'true' ||
|
|
175
|
+
trimmed === 'false' ||
|
|
176
|
+
trimmed === 'null' ||
|
|
177
|
+
/^[+-]?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/i.test(trimmed)
|
|
178
|
+
|
|
179
|
+
return looksLikeJsonLiteral ? JSON.parse(trimmed) : rawValue
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export const formatValidationIssues = (
|
|
183
|
+
error: {
|
|
184
|
+
issues: Array<{
|
|
185
|
+
path: Array<string | number>
|
|
186
|
+
message: string
|
|
187
|
+
}>
|
|
188
|
+
}
|
|
189
|
+
) =>
|
|
190
|
+
error.issues
|
|
191
|
+
.map((issue) => {
|
|
192
|
+
const path = issue.path.length > 0 ? issue.path.join('.') : '<root>'
|
|
193
|
+
return `${path}: ${issue.message}`
|
|
194
|
+
})
|
|
195
|
+
.join('\n')
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import type { Command } from 'commander'
|
|
4
|
+
|
|
5
|
+
import { formatListCommand, formatResumeCommand } from '#~/session-cache.js'
|
|
6
|
+
import { resolveCliWorkspaceCwd } from '#~/workspace.js'
|
|
7
|
+
|
|
8
|
+
import { signalCliSession } from './session-control'
|
|
9
|
+
|
|
10
|
+
export function registerKillCommand(program: Command) {
|
|
11
|
+
program
|
|
12
|
+
.command('kill <sessionId>')
|
|
13
|
+
.description('Force kill a running CLI session')
|
|
14
|
+
.addHelpText(
|
|
15
|
+
'after',
|
|
16
|
+
`
|
|
17
|
+
Examples:
|
|
18
|
+
oneworks list --running
|
|
19
|
+
oneworks kill <sessionId>
|
|
20
|
+
`
|
|
21
|
+
)
|
|
22
|
+
.action(async (sessionId: string) => {
|
|
23
|
+
try {
|
|
24
|
+
const result = await signalCliSession({
|
|
25
|
+
cwd: resolveCliWorkspaceCwd(),
|
|
26
|
+
sessionId,
|
|
27
|
+
signal: 'SIGKILL'
|
|
28
|
+
})
|
|
29
|
+
console.log(result.message)
|
|
30
|
+
console.log(
|
|
31
|
+
`Tips:\n Check running sessions: ${formatListCommand({ running: true })}\n Resume later: ${
|
|
32
|
+
formatResumeCommand(sessionId)
|
|
33
|
+
}`
|
|
34
|
+
)
|
|
35
|
+
} catch (error) {
|
|
36
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
37
|
+
console.error(message)
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import type { TaskDetail } from '@oneworks/types'
|
|
4
|
+
import { Option } from 'commander'
|
|
5
|
+
import type { Command } from 'commander'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
formatKillCommand,
|
|
9
|
+
formatListCommand,
|
|
10
|
+
formatResumeCommand,
|
|
11
|
+
formatStopCommand,
|
|
12
|
+
listCliSessions,
|
|
13
|
+
resolveCliSessionAdapter,
|
|
14
|
+
resolveCliSessionCtxId,
|
|
15
|
+
resolveCliSessionDescription,
|
|
16
|
+
resolveCliSessionId,
|
|
17
|
+
resolveCliSessionModel,
|
|
18
|
+
resolveCliSessionUpdatedAt
|
|
19
|
+
} from '#~/session-cache.js'
|
|
20
|
+
import { resolveCliWorkspaceCwd } from '#~/workspace.js'
|
|
21
|
+
|
|
22
|
+
interface ListOptions {
|
|
23
|
+
all?: boolean
|
|
24
|
+
json?: boolean
|
|
25
|
+
limit?: string
|
|
26
|
+
running?: boolean
|
|
27
|
+
status?: string[]
|
|
28
|
+
verbose?: boolean
|
|
29
|
+
view?: ListView
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ListRow {
|
|
33
|
+
sessionId: string
|
|
34
|
+
ctxId: string
|
|
35
|
+
status: TaskDetail['status'] | 'unknown'
|
|
36
|
+
adapter: string
|
|
37
|
+
model: string
|
|
38
|
+
updatedAt: number
|
|
39
|
+
pid?: number
|
|
40
|
+
description: string
|
|
41
|
+
resumeCommand: string
|
|
42
|
+
stopCommand: string
|
|
43
|
+
killCommand: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const TASK_STATUSES = ['pending', 'running', 'completed', 'failed', 'stopped'] as const
|
|
47
|
+
const LIST_VIEWS = ['compact', 'default', 'full'] as const
|
|
48
|
+
|
|
49
|
+
type TaskStatus = (typeof TASK_STATUSES)[number]
|
|
50
|
+
type ListView = (typeof LIST_VIEWS)[number]
|
|
51
|
+
|
|
52
|
+
const isTaskStatus = (value: string): value is TaskStatus => (TASK_STATUSES as readonly string[]).includes(value)
|
|
53
|
+
|
|
54
|
+
const truncate = (value: string | undefined, maxLength: number) => {
|
|
55
|
+
if (value == null || value === '') return ''
|
|
56
|
+
return value.length > maxLength ? `${value.slice(0, maxLength - 3)}...` : value
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const formatUpdatedAt = (updatedAt: number) => updatedAt === 0 ? '' : new Date(updatedAt).toLocaleString()
|
|
60
|
+
|
|
61
|
+
const resolveListView = (view: ListView | undefined, verbose: boolean | undefined): ListView =>
|
|
62
|
+
verbose ? 'full' : (view ?? 'compact')
|
|
63
|
+
|
|
64
|
+
const buildListRows = (records: Awaited<ReturnType<typeof listCliSessions>>) => (
|
|
65
|
+
records.map((record) => {
|
|
66
|
+
const sessionId = resolveCliSessionId(record)
|
|
67
|
+
const status = record.detail?.status ?? 'unknown'
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
sessionId,
|
|
71
|
+
ctxId: resolveCliSessionCtxId(record),
|
|
72
|
+
status,
|
|
73
|
+
adapter: resolveCliSessionAdapter(record),
|
|
74
|
+
model: resolveCliSessionModel(record),
|
|
75
|
+
updatedAt: resolveCliSessionUpdatedAt(record),
|
|
76
|
+
pid: record.detail?.pid,
|
|
77
|
+
description: resolveCliSessionDescription(record),
|
|
78
|
+
resumeCommand: sessionId === '' ? '' : formatResumeCommand(sessionId),
|
|
79
|
+
stopCommand: status === 'running' && sessionId !== '' ? formatStopCommand(sessionId) : '',
|
|
80
|
+
killCommand: status === 'running' && sessionId !== '' ? formatKillCommand(sessionId) : ''
|
|
81
|
+
} satisfies ListRow
|
|
82
|
+
})
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
const renderListRows = (rows: ListRow[], view: ListView) => {
|
|
86
|
+
switch (view) {
|
|
87
|
+
case 'compact':
|
|
88
|
+
return rows.map((row) => ({
|
|
89
|
+
Session: row.sessionId,
|
|
90
|
+
Status: row.status,
|
|
91
|
+
Updated: formatUpdatedAt(row.updatedAt),
|
|
92
|
+
Description: truncate(row.description, 72)
|
|
93
|
+
}))
|
|
94
|
+
case 'default':
|
|
95
|
+
return rows.map((row) => ({
|
|
96
|
+
Session: row.sessionId,
|
|
97
|
+
Status: row.status,
|
|
98
|
+
Adapter: row.adapter,
|
|
99
|
+
Model: row.model,
|
|
100
|
+
Updated: formatUpdatedAt(row.updatedAt),
|
|
101
|
+
Description: truncate(row.description, 72)
|
|
102
|
+
}))
|
|
103
|
+
case 'full':
|
|
104
|
+
return rows.map((row) => ({
|
|
105
|
+
Session: row.sessionId,
|
|
106
|
+
Context: row.ctxId,
|
|
107
|
+
Status: row.status,
|
|
108
|
+
Adapter: row.adapter,
|
|
109
|
+
Model: row.model,
|
|
110
|
+
Updated: formatUpdatedAt(row.updatedAt),
|
|
111
|
+
PID: row.pid ?? '',
|
|
112
|
+
Description: truncate(row.description, 72),
|
|
113
|
+
Resume: row.resumeCommand,
|
|
114
|
+
Stop: row.stopCommand,
|
|
115
|
+
Kill: row.killCommand
|
|
116
|
+
}))
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const buildListHints = (rows: ListRow[], view: ListView) => {
|
|
121
|
+
const hints: string[] = []
|
|
122
|
+
const latest = rows[0]
|
|
123
|
+
const running = rows.find(row => row.stopCommand !== '')
|
|
124
|
+
|
|
125
|
+
if (latest?.resumeCommand) {
|
|
126
|
+
hints.push(`Resume latest: ${latest.resumeCommand}`)
|
|
127
|
+
}
|
|
128
|
+
if (running?.stopCommand) {
|
|
129
|
+
hints.push(`Stop a running session: ${running.stopCommand}`)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
switch (view) {
|
|
133
|
+
case 'compact':
|
|
134
|
+
hints.push(`More columns: ${formatListCommand({ view: 'default' })}`)
|
|
135
|
+
break
|
|
136
|
+
case 'default':
|
|
137
|
+
hints.push(`All columns: ${formatListCommand({ view: 'full' })}`)
|
|
138
|
+
break
|
|
139
|
+
case 'full':
|
|
140
|
+
break
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return hints
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const printListHints = (rows: ListRow[], view: ListView) => {
|
|
147
|
+
const hints = buildListHints(rows, view)
|
|
148
|
+
if (hints.length === 0) return
|
|
149
|
+
console.log(`Tips:\n ${hints.join('\n ')}`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function registerListCommand(program: Command) {
|
|
153
|
+
program
|
|
154
|
+
.command('list')
|
|
155
|
+
.alias('ls')
|
|
156
|
+
.description('List cached CLI sessions')
|
|
157
|
+
.option('--all', 'Show all sessions', false)
|
|
158
|
+
.option('--json', 'Print JSON output', false)
|
|
159
|
+
.option('--limit <count>', 'Limit displayed sessions')
|
|
160
|
+
.option('--running', 'Show only running sessions', false)
|
|
161
|
+
.option('--status <status...>', `Filter by status (${TASK_STATUSES.join(', ')})`)
|
|
162
|
+
.addOption(
|
|
163
|
+
new Option('--view <view>', 'Display view')
|
|
164
|
+
.choices([...LIST_VIEWS])
|
|
165
|
+
.default('compact')
|
|
166
|
+
)
|
|
167
|
+
.option('--verbose', 'Alias for --view full', false)
|
|
168
|
+
.addHelpText(
|
|
169
|
+
'after',
|
|
170
|
+
`
|
|
171
|
+
Examples:
|
|
172
|
+
oneworks list
|
|
173
|
+
oneworks list --view default
|
|
174
|
+
oneworks list --view full
|
|
175
|
+
oneworks list --running
|
|
176
|
+
`
|
|
177
|
+
)
|
|
178
|
+
.action(async (opts: ListOptions) => {
|
|
179
|
+
try {
|
|
180
|
+
const records = await listCliSessions(resolveCliWorkspaceCwd())
|
|
181
|
+
if (records.length === 0) {
|
|
182
|
+
console.log('No cached sessions found.')
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const requestedStatuses = new Set<TaskDetail['status']>()
|
|
187
|
+
if (opts.running) requestedStatuses.add('running')
|
|
188
|
+
for (const status of opts.status ?? []) {
|
|
189
|
+
if (!isTaskStatus(status)) {
|
|
190
|
+
throw new Error(`Unsupported status "${status}". Expected one of: ${TASK_STATUSES.join(', ')}`)
|
|
191
|
+
}
|
|
192
|
+
requestedStatuses.add(status)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const limit = opts.all
|
|
196
|
+
? records.length
|
|
197
|
+
: Math.max(1, Number.parseInt(opts.limit ?? '20', 10) || 20)
|
|
198
|
+
const view = resolveListView(opts.view, opts.verbose)
|
|
199
|
+
const rows = buildListRows(records)
|
|
200
|
+
.filter((record) => (
|
|
201
|
+
requestedStatuses.size === 0 ||
|
|
202
|
+
(record.status !== 'unknown' && requestedStatuses.has(record.status))
|
|
203
|
+
))
|
|
204
|
+
.slice(0, limit)
|
|
205
|
+
|
|
206
|
+
if (rows.length === 0) {
|
|
207
|
+
console.log('No cached sessions matched the requested filters.')
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (opts.json) {
|
|
212
|
+
console.log(JSON.stringify(rows, null, 2))
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
console.table(renderListRows(rows, view))
|
|
217
|
+
printListHints(rows, view)
|
|
218
|
+
} catch (error) {
|
|
219
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
220
|
+
console.error(message)
|
|
221
|
+
process.exit(1)
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
|
|
5
|
+
import { mergeProcessEnvWithProjectEnv, resolveProjectHomePath } from '@oneworks/utils'
|
|
6
|
+
|
|
7
|
+
import type { MemoryCommandOptions, MemoryContext } from './shared'
|
|
8
|
+
import { trimNonEmpty } from './shared'
|
|
9
|
+
|
|
10
|
+
const MEMORY_ROOT_ENV = '__ONEWORKS_PROJECT_CHANNEL_MEMORY_ROOT__'
|
|
11
|
+
const CHANNEL_TYPE_ENV = '__ONEWORKS_PROJECT_CHANNEL_TYPE__'
|
|
12
|
+
const CHANNEL_KEY_ENV = '__ONEWORKS_PROJECT_CHANNEL_KEY__'
|
|
13
|
+
const CHANNEL_ID_ENV = '__ONEWORKS_PROJECT_CHANNEL_ID__'
|
|
14
|
+
const CHANNEL_SESSION_TYPE_ENV = '__ONEWORKS_PROJECT_CHANNEL_SESSION_TYPE__'
|
|
15
|
+
const CHANNEL_SENDER_ID_ENV = '__ONEWORKS_PROJECT_CHANNEL_SENDER_ID__'
|
|
16
|
+
const CHANNEL_CONTEXT_PATH_ENV = '__ONEWORKS_PROJECT_CHANNEL_CONTEXT_PATH__'
|
|
17
|
+
const SESSION_ID_ENV = '__ONEWORKS_PROJECT_SESSION_ID__'
|
|
18
|
+
|
|
19
|
+
const resolveRoot = (cwd: string, env: NodeJS.ProcessEnv) => {
|
|
20
|
+
const explicitRoot = trimNonEmpty(env[MEMORY_ROOT_ENV])
|
|
21
|
+
if (explicitRoot != null) return path.resolve(explicitRoot)
|
|
22
|
+
|
|
23
|
+
const serverDataDir = trimNonEmpty(env.__ONEWORKS_PROJECT_SERVER_DATA_DIR__)
|
|
24
|
+
const dataRoot = serverDataDir == null
|
|
25
|
+
? resolveProjectHomePath(cwd, env, 'server', 'data')
|
|
26
|
+
: path.resolve(serverDataDir)
|
|
27
|
+
return path.resolve(dataRoot, 'channel-memory', 'v1')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const readChannelContext = (env: NodeJS.ProcessEnv) => {
|
|
31
|
+
const contextPath = trimNonEmpty(env[CHANNEL_CONTEXT_PATH_ENV])
|
|
32
|
+
if (contextPath == null) return undefined
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(readFileSync(contextPath, 'utf8')) as unknown
|
|
36
|
+
if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined
|
|
37
|
+
return parsed as Record<string, unknown>
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const resolveContext = (options: MemoryCommandOptions): MemoryContext => {
|
|
44
|
+
const cwd = options.cwd ?? process.cwd()
|
|
45
|
+
const env = mergeProcessEnvWithProjectEnv(options.env, { workspaceFolder: cwd }) as NodeJS.ProcessEnv
|
|
46
|
+
const channelContext = readChannelContext(env)
|
|
47
|
+
const contextChannelType = trimNonEmpty(channelContext?.channelType)
|
|
48
|
+
const contextChannelKey = trimNonEmpty(channelContext?.channelKey)
|
|
49
|
+
const contextSessionType = trimNonEmpty(channelContext?.sessionType)
|
|
50
|
+
const contextChannelId = trimNonEmpty(channelContext?.channelId)
|
|
51
|
+
const contextSenderId = trimNonEmpty(channelContext?.senderId)
|
|
52
|
+
const contextSessionId = trimNonEmpty(channelContext?.sessionId)
|
|
53
|
+
const channelRef = trimNonEmpty(options.channel) ?? contextChannelType ?? trimNonEmpty(env[CHANNEL_TYPE_ENV])
|
|
54
|
+
const channelParts = channelRef?.split(':') ?? []
|
|
55
|
+
const channelType = channelParts[0] || contextChannelType || trimNonEmpty(env[CHANNEL_TYPE_ENV])
|
|
56
|
+
const channelKey = channelParts[1] || contextChannelKey || trimNonEmpty(env[CHANNEL_KEY_ENV])
|
|
57
|
+
const channelId = contextChannelId ?? trimNonEmpty(env[CHANNEL_ID_ENV])
|
|
58
|
+
const channelSessionType = contextSessionType ?? trimNonEmpty(env[CHANNEL_SESSION_TYPE_ENV])
|
|
59
|
+
const senderId = contextSenderId ?? (
|
|
60
|
+
channelSessionType === 'group'
|
|
61
|
+
? undefined
|
|
62
|
+
: trimNonEmpty(env[CHANNEL_SENDER_ID_ENV])
|
|
63
|
+
) ??
|
|
64
|
+
(channelSessionType === 'direct' ? channelId : undefined)
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
channelId,
|
|
68
|
+
channelKey,
|
|
69
|
+
channelRef,
|
|
70
|
+
channelSessionType,
|
|
71
|
+
channelType,
|
|
72
|
+
root: resolveRoot(cwd, env),
|
|
73
|
+
senderId,
|
|
74
|
+
sessionId: contextSessionId ?? trimNonEmpty(env[SESSION_ID_ENV]) ?? trimNonEmpty(env.__ONEWORKS_PROJECT_CTX_ID__)
|
|
75
|
+
}
|
|
76
|
+
}
|