@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,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
+ }