@dotdrelle/wiki-manager 0.6.31 → 0.6.47

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.
@@ -24,14 +24,15 @@ export function SlashDialog(props: { context: any }) {
24
24
  {(item: any, index) => (
25
25
  <text
26
26
  height={1}
27
- fg={index() === context().selected ? '#111318' : '#D6DEE8'}
28
- bg={index() === context().selected ? '#8BD5CA' : '#111318'}
27
+ fg={index() === context().visibleSelected ? '#111318' : '#D6DEE8'}
28
+ bg={index() === context().visibleSelected ? '#8BD5CA' : '#111318'}
29
29
  >
30
30
  {item.value.padEnd(16, ' ')} {item.description}
31
31
  </text>
32
32
  )}
33
33
  </For>
34
- <text fg="#7F8C8D">[up/down navigate Tab complete Enter confirm Esc clear]</text>
34
+ <text height={1} />
35
+ <text fg="#7F8C8D">[up/down navigate, Tab complete, Enter confirm, Esc clear]</text>
35
36
  </box>
36
37
  )}
37
38
  </Show>
package/src/shell/repl.js CHANGED
@@ -54,8 +54,8 @@ const COMMAND_COMPLETION_DESCRIPTIONS = {
54
54
  '/help': 'Show shell commands.',
55
55
  '/version': 'Print the wiki-manager version.',
56
56
  '/exit': 'Exit the shell.',
57
- '/workspaces': 'List configured workspaces.',
58
- '/new': 'Create or configure a new workspace.',
57
+ '/workspace': 'List, create, or delete workspaces.',
58
+ '/new': 'Open the setup wizard in the interactive TUI.',
59
59
  '/use': 'Load a workspace and its default config.',
60
60
  '/config': 'Inspect or switch .wikirc.yaml profiles.',
61
61
  '/status': 'Show the current workspace and session state.',
@@ -89,7 +89,9 @@ const SUBCOMMAND_COMPLETION_DESCRIPTIONS = {
89
89
  '/queue': 'Inspect or cancel queued MCP jobs.',
90
90
  '/queue:cancel': 'Cancel a queued or running queue item.',
91
91
  '/queue:clear': 'Clear finished queue items.',
92
- '/workspace:init': 'Legacy form of /new.',
92
+ '/workspace:init': 'Low-level workspace creation.',
93
+ '/workspace:list': 'List configured workspaces.',
94
+ '/workspace:delete': 'Delete one workspace after confirmation.',
93
95
  '/wiki:run': 'Use the low-level llm-wiki CLI fallback.',
94
96
  '/skills:edit': 'Edit one workspace skill file.',
95
97
  '/skills:list': 'List workspace skills.',
@@ -106,7 +108,7 @@ export function createSession() {
106
108
  wikircConfig: null,
107
109
  language: null,
108
110
  mcp: null,
109
- commands: ['help', 'version', 'exit', 'workspaces', 'new', 'use', 'config', 'status', 'services', 'start', 'stop', 'logs', 'mcp', 'wiki', 'skills', 'upload', 'uploads', 'clear', 'chat', 'agent', 'openui', 'queue'],
111
+ commands: ['help', 'version', 'exit', 'workspace', 'new', 'use', 'config', 'status', 'services', 'start', 'stop', 'logs', 'mcp', 'wiki', 'skills', 'upload', 'uploads', 'clear', 'chat', 'agent', 'openui', 'queue'],
110
112
  chatMode: true,
111
113
  llm: null,
112
114
  activities: {},
@@ -200,6 +202,7 @@ function completionValuesFor(parts, inputBuffer, session) {
200
202
  if (tokenIndex === 0) return slashCompletions(session);
201
203
  if (command === '/new' && tokenIndex === 1) return [];
202
204
  if (command === '/use' && tokenIndex === 1) return workspaceNames();
205
+ if (command === '/use' && tokenIndex === 2) return [];
203
206
  if (command === '/config' && tokenIndex === 1) return ['edit', 'list', 'status', 'use'];
204
207
  if (command === '/config' && (previousToken === 'use' || previousToken === 'edit')) return wikircProfileNames(session);
205
208
  if (command === '/mcp' && tokenIndex === 1) return ['call', 'endpoints', 'status', 'tools'];
@@ -216,11 +219,16 @@ function completionValuesFor(parts, inputBuffer, session) {
216
219
  .filter((item) => ['waiting', 'starting', 'running'].includes(item.status))
217
220
  .map((item) => item.id);
218
221
  }
219
- if (command === '/workspace' && tokenIndex === 1) return ['init'];
222
+ if (command === '/workspace') {
223
+ if (tokenIndex === 1) return ['delete', 'init', 'list'];
224
+ if (previousToken === 'delete') return workspaceNames();
225
+ if (parts[1] === 'delete' && tokenIndex === 3) return ['--confirm'];
226
+ }
220
227
  if (command === '/wiki' && tokenIndex === 1) return ['run'];
221
228
  if (command === '/skills' && tokenIndex === 1) return ['edit', 'list', 'run', 'show'];
222
229
  if (command === '/skills' && ['edit', 'run', 'show'].includes(previousToken ?? '')) return skillNames(session);
223
- if ((command === '/start' || command === '/stop' || command === '/logs') && tokenIndex === 1) return serviceNames();
230
+ if ((command === '/start' || command === '/stop') && tokenIndex === 1) return ['agents', ...serviceNames()];
231
+ if (command === '/logs' && tokenIndex === 1) return serviceNames();
224
232
  return [];
225
233
  }
226
234
 
@@ -289,6 +297,11 @@ export function completionDescription(value, parts) {
289
297
  if (command === '/start') return serviceDescription(value) ?? 'Start this Docker Compose service.';
290
298
  if (command === '/stop') return serviceDescription(value) ?? 'Stop this Docker Compose service.';
291
299
  if (command === '/logs') return serviceDescription(value) ?? 'Show logs for this Docker Compose service.';
300
+ if (command === '/workspace') {
301
+ if (parts[1] === 'delete' && value === '--confirm') return 'Confirm workspace deletion.';
302
+ if (parts.at(-1) === 'delete') return 'Delete this workspace.';
303
+ return 'Choose a workspace action.';
304
+ }
292
305
  if (command === '/mcp') return parts[1] === 'call' ? 'Use this MCP server.' : 'Filter tools to this MCP server.';
293
306
  if (command === '/skills') {
294
307
  if (parts.at(-1) === 'edit') return 'Edit this skill.';
@@ -1174,7 +1187,7 @@ async function runTuiShell({ agent, packageJson, session }) {
1174
1187
  rerender();
1175
1188
 
1176
1189
  try {
1177
- const { output: wsOutput } = await handleSlashCommand('/workspaces', { packageJson, session });
1190
+ const { output: wsOutput } = await handleSlashCommand('/workspace list', { packageJson, session });
1178
1191
  if (wsOutput) messages.push({ role: 'command', content: wsOutput });
1179
1192
  } finally {
1180
1193
  clearInterval(spinnerInterval);
@@ -1316,7 +1329,7 @@ async function runTuiShell({ agent, packageJson, session }) {
1316
1329
  }
1317
1330
  return;
1318
1331
  }
1319
- if (key?.ctrl && key.name === 'c') {
1332
+ if ((key?.ctrl || key?.meta) && key.name === 'c') {
1320
1333
  if (busy) {
1321
1334
  currentAbortController?.abort();
1322
1335
  spinnerLabel = 'Interrupting…';
package/src/shell/tui.tsx CHANGED
@@ -6,6 +6,7 @@ import { FileEditorDialog } from './FileEditorDialog';
6
6
  import { LeftPane } from './LeftPane';
7
7
  import { RightPane } from './RightPane';
8
8
  import { SlashDialog } from './SlashDialog';
9
+ import { SetupWizard } from './SetupWizard';
9
10
  import { useSession } from './useSession';
10
11
 
11
12
  const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
@@ -37,7 +38,10 @@ function copyToClipboard(text: string, renderer: unknown) {
37
38
  }
38
39
  }
39
40
 
40
- function App(props: { agent: unknown; packageJson: Record<string, unknown> }) {
41
+ function App(props: {
42
+ agent: unknown;
43
+ packageJson: Record<string, unknown>;
44
+ }) {
41
45
  const renderer = useRenderer();
42
46
  const dimensions = useTerminalDimensions();
43
47
  const [spinnerIndex, setSpinnerIndex] = createSignal(0);
@@ -100,11 +104,12 @@ function App(props: { agent: unknown; packageJson: Record<string, unknown> }) {
100
104
  });
101
105
 
102
106
  useKeyboard((key) => {
107
+ const keyName = String(key.name ?? '').toLowerCase();
103
108
  if (state.activeEditor()) {
104
- if (key.name === 'escape') state.closeEditor();
109
+ if (keyName === 'escape') state.closeEditor();
105
110
  return;
106
111
  }
107
- if (key.ctrl && key.name === 'c') {
112
+ if ((key.ctrl || key.meta) && keyName === 'c') {
108
113
  if (state.busy()) {
109
114
  state.abort();
110
115
  return;
@@ -121,19 +126,19 @@ function App(props: { agent: unknown; packageJson: Record<string, unknown> }) {
121
126
  }, 1600);
122
127
  return;
123
128
  }
124
- if (key.ctrl && key.name === 'q') {
129
+ if (key.ctrl && keyName === 'q') {
125
130
  state.toggleRightTab();
126
131
  return;
127
132
  }
128
133
  if (state.busy()) return;
129
- if (key.name === 'tab') state.completeSelected();
130
- if (key.name === 'pageup') state.scrollConversation(conversationRows());
131
- else if (key.name === 'pagedown') state.scrollConversation(-conversationRows());
132
- if (key.name === 'up' && state.slash()) state.moveCompletion(-1);
133
- else if (key.name === 'down' && state.slash()) state.moveCompletion(1);
134
- else if (key.name === 'up' && !state.input().includes('\n')) state.historyUp();
135
- else if (key.name === 'down' && !state.input().includes('\n')) state.historyDown();
136
- else if (key.name === 'escape') {
134
+ if (keyName === 'tab') state.completeSelected();
135
+ if (keyName === 'pageup') state.scrollConversation(conversationRows());
136
+ else if (keyName === 'pagedown') state.scrollConversation(-conversationRows());
137
+ if (keyName === 'up' && state.slash()) state.moveCompletion(-1);
138
+ else if (keyName === 'down' && state.slash()) state.moveCompletion(1);
139
+ else if (keyName === 'up' && !state.input().includes('\n')) state.historyUp();
140
+ else if (keyName === 'down' && !state.input().includes('\n')) state.historyDown();
141
+ else if (keyName === 'escape') {
137
142
  if (state.slash()) state.dismissSlash();
138
143
  else state.setInput('');
139
144
  }
@@ -196,10 +201,77 @@ function App(props: { agent: unknown; packageJson: Record<string, unknown> }) {
196
201
  );
197
202
  }
198
203
 
199
- export async function runOpenTuiShell({ agent, packageJson }: { agent: unknown; packageJson: Record<string, unknown> }) {
204
+ export async function runOpenTuiShell({
205
+ agent,
206
+ packageJson,
207
+ }: {
208
+ agent: unknown;
209
+ packageJson: Record<string, unknown>;
210
+ }) {
200
211
  await render(() => <App agent={agent} packageJson={packageJson} />, {
201
212
  exitOnCtrlC: false,
202
213
  useMouse: true,
203
214
  targetFps: 30,
204
215
  });
216
+ return {};
217
+ }
218
+
219
+ function WizardApp(props: {
220
+ mode: 'startup' | 'setup';
221
+ gaps?: any[];
222
+ initialWorkspaceName?: string;
223
+ initialWorkspacePath?: string | null;
224
+ onDone: () => void;
225
+ }) {
226
+ const renderer = useRenderer();
227
+ const dimensions = useTerminalDimensions();
228
+ const close = () => {
229
+ props.onDone();
230
+ renderer.destroy();
231
+ };
232
+ return (
233
+ <box width="100%" height="100%" backgroundColor="#0B0D12">
234
+ <SetupWizard
235
+ mode={props.mode}
236
+ session={{}}
237
+ gaps={props.gaps}
238
+ width={dimensions().width}
239
+ height={dimensions().height}
240
+ initialRoute={props.mode === 'setup' ? 'workspace-name' : undefined}
241
+ initialWorkspaceName={props.initialWorkspaceName}
242
+ initialWorkspacePath={props.initialWorkspacePath ?? null}
243
+ closeOnDone={props.mode === 'setup'}
244
+ onComplete={close}
245
+ onClose={close}
246
+ />
247
+ </box>
248
+ );
249
+ }
250
+
251
+ export async function runStartupWizard(gaps: any[]) {
252
+ if (!gaps.length) return;
253
+ await new Promise<void>((resolve, reject) => {
254
+ render(() => <WizardApp mode="startup" gaps={gaps} onDone={resolve} />, {
255
+ exitOnCtrlC: false,
256
+ useMouse: true,
257
+ targetFps: 30,
258
+ }).catch(reject);
259
+ });
260
+ }
261
+
262
+ export async function runSetupWizard(options: { workspaceName?: string; workspacePath?: string | null } = {}) {
263
+ await new Promise<void>((resolve, reject) => {
264
+ render(() => (
265
+ <WizardApp
266
+ mode="setup"
267
+ initialWorkspaceName={options.workspaceName}
268
+ initialWorkspacePath={options.workspacePath ?? null}
269
+ onDone={resolve}
270
+ />
271
+ ), {
272
+ exitOnCtrlC: false,
273
+ useMouse: true,
274
+ targetFps: 30,
275
+ }).catch(reject);
276
+ });
205
277
  }
@@ -86,14 +86,22 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
86
86
  session.llm ? 'llm ready' : 'llm limited',
87
87
  ].join(' ');
88
88
  });
89
- const slash = createMemo(() => {
89
+ const matchContext = createMemo(() => {
90
90
  if (input() === dismissedSlashInput()) return null;
91
- const context = completionContext(input(), session);
91
+ return completionContext(input(), session);
92
+ });
93
+ const slash = createMemo(() => {
94
+ const context = matchContext();
92
95
  if (!context) return null;
96
+ const selected = Math.min(selectedCompletion(), Math.max(0, context.matches.length - 1));
97
+ const visibleCount = 10;
98
+ const start = Math.max(0, Math.min(selected - Math.floor(visibleCount / 2), Math.max(0, context.matches.length - visibleCount)));
99
+ const visibleMatches = context.matches.slice(start, start + visibleCount);
93
100
  return {
94
101
  ...context,
95
- selected: Math.min(selectedCompletion(), Math.max(0, context.matches.length - 1)),
96
- items: context.matches.slice(0, 10).map((value: string) => ({
102
+ selected,
103
+ visibleSelected: selected - start,
104
+ items: visibleMatches.map((value: string) => ({
97
105
  value,
98
106
  description: completionDescription(value, context.parts),
99
107
  })),
@@ -249,8 +257,8 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
249
257
 
250
258
  function completeSelected() {
251
259
  const context = slash();
252
- if (!context || context.items.length === 0) return;
253
- const selected = context.items[context.selected]?.value;
260
+ if (!context || context.matches.length === 0) return;
261
+ const selected = context.matches[context.selected];
254
262
  if (!selected) return;
255
263
  const lastSpace = input().lastIndexOf(' ');
256
264
  const base = input().endsWith(' ') ? input() : input().slice(0, lastSpace + 1);
@@ -259,7 +267,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
259
267
  }
260
268
 
261
269
  function moveCompletion(delta: number) {
262
- const count = slash()?.items.length ?? 0;
270
+ const count = slash()?.matches.length ?? 0;
263
271
  if (!count) return;
264
272
  setSelectedCompletion((value) => (value + delta + count) % count);
265
273
  }
package/wiki-workspace CHANGED
@@ -211,7 +211,7 @@ agents_compose() {
211
211
  read_lines_into_array cacert_args cacert_compose_args "$agents_compose_file" "agents.cacert.compose.yml"
212
212
  WORKSPACES_ROOT="$workspaces_root" AGENTS_DATA_DIR="$agents_data_dir" \
213
213
  docker compose --project-directory "$DEFAULT_MANAGER_DIR" \
214
- "${compose_env_args[@]}" -f "$agents_compose_file" "${cacert_args[@]}" -p wiki-agents "$@"
214
+ ${compose_env_args[@]+"${compose_env_args[@]}"} -f "$agents_compose_file" ${cacert_args[@]+"${cacert_args[@]}"} -p wiki-agents "$@"
215
215
  }
216
216
 
217
217
  case "$subcommand" in
@@ -225,9 +225,14 @@ agents_compose() {
225
225
  mkdir -p "$agents_data_dir/cme" "$agents_data_dir/documents/input" "$agents_data_dir/documents/output" "$agents_data_dir/documents/uploads"
226
226
  mkdir -p "$workspaces_root"
227
227
  $do_pull && _agents_dc pull
228
- _agents_dc up -d
228
+ local up_args=(up -d)
229
+ if [[ -n "$CACERT_PATH" ]]; then
230
+ up_args+=(--force-recreate)
231
+ fi
232
+ _agents_dc "${up_args[@]}"
229
233
  printf 'Workspaces root: %s\n' "$workspaces_root"
230
234
  printf 'Agents data: %s\n' "$agents_data_dir"
235
+ [[ -z "$CACERT_PATH" ]] || printf 'CA cert: %s -> /wiki-manager-ca.pem\n' "$CACERT_PATH"
231
236
  printf 'Agents: cme=:%s documents=:%s mailer=:%s\n' \
232
237
  "${CME_MCP_PORT:-3336}" "${DOCUMENTS_MCP_PORT:-3337}" "${MAILER_MCP_PORT:-3335}"
233
238
  ;;
@@ -496,7 +501,7 @@ compose_for_workspace() {
496
501
  WIKI_MCP_PORT="$mcp_port" \
497
502
  PRODUCTION_MCP_PORT="$prod_port" \
498
503
  docker compose --project-directory "$DEFAULT_MANAGER_DIR" \
499
- "${compose_env_args[@]}" -f "$ROOT_DIR/docker-compose.yml" "${cacert_args[@]}" -p "$project" "$@"
504
+ ${compose_env_args[@]+"${compose_env_args[@]}"} -f "$ROOT_DIR/docker-compose.yml" ${cacert_args[@]+"${cacert_args[@]}"} -p "$project" "$@"
500
505
  }
501
506
 
502
507
  logs_args() {
@@ -524,20 +529,19 @@ cacert_compose_args() {
524
529
  local compose_file="$1"
525
530
  local override_name="$2"
526
531
  [[ -n "$CACERT_PATH" ]] || return 0
527
- command -v node >/dev/null 2>&1 || die "--cacert requires node to generate Docker Compose overrides"
528
532
 
529
533
  local services
530
534
  services="$(
531
- ROOT_DIR="$ROOT_DIR" _WIKI_COMPOSE_FILE_PATH="$compose_file" node <<'NODE'
532
- const { createRequire } = require('node:module');
533
- const fs = require('node:fs');
534
- const requireFromPackage = createRequire(`${process.env.ROOT_DIR}/package.json`);
535
- const YAML = requireFromPackage('yaml');
536
- const parsed = YAML.parse(fs.readFileSync(process.env._WIKI_COMPOSE_FILE_PATH, 'utf8')) ?? {};
537
- for (const service of Object.keys(parsed.services ?? {})) {
538
- console.log(service);
539
- }
540
- NODE
535
+ awk '
536
+ /^[[:space:]]*services:[[:space:]]*$/ { in_services=1; next }
537
+ in_services && /^[^[:space:]#]/ { exit }
538
+ in_services && /^ [A-Za-z0-9_.-]+:[[:space:]]*$/ {
539
+ line=$0
540
+ sub(/^ /, "", line)
541
+ sub(/:[[:space:]]*$/, "", line)
542
+ print line
543
+ }
544
+ ' "$compose_file"
541
545
  )"
542
546
  [[ -n "$services" ]] || return 0
543
547
 
@@ -721,7 +725,7 @@ config_workspace() {
721
725
  WIKI_SERVE_PORT="$serve_port" \
722
726
  WIKI_MCP_PORT="$mcp_port" \
723
727
  docker compose --project-directory "$DEFAULT_MANAGER_DIR" \
724
- "${compose_env_args[@]}" -f "$ROOT_DIR/docker-compose.yml" "${cacert_args[@]}" run --rm wiki init
728
+ ${compose_env_args[@]+"${compose_env_args[@]}"} -f "$ROOT_DIR/docker-compose.yml" ${cacert_args[@]+"${cacert_args[@]}"} run --rm wiki init
725
729
 
726
730
  printf 'Workspace ready: %s\n' "$target_path"
727
731
  printf 'Start with: wiki-workspace up %s\n' "$workspace"