@dotdrelle/wiki-manager 0.6.30 → 0.6.34
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/README.md +3 -2
- package/bin/wiki-manager +39 -1
- package/bin/wiki-manager.js +62 -3
- package/package.json +8 -7
- package/src/agent/graph.js +5 -10
- package/src/cli/wiki-manager.js +17 -2
- package/src/commands/slash.js +105 -27
- package/src/commands/slash.test.js +68 -0
- package/src/core/cacert.js +66 -0
- package/src/core/compose.js +10 -8
- package/src/core/env.js +13 -3
- package/src/core/mcp.js +1 -1
- package/src/core/modelFetch.js +97 -0
- package/src/core/modelFetch.test.js +38 -0
- package/src/core/startupCheck.js +130 -0
- package/src/core/wikiSetup.js +156 -0
- package/src/core/wikirc.js +80 -1
- package/src/core/wikirc.test.js +111 -0
- package/src/core/workspaces.js +1 -1
- package/src/shell/LeftPane.tsx +54 -28
- package/src/shell/RightPane.tsx +25 -2
- package/src/shell/SetupWizard.tsx +806 -0
- package/src/shell/SlashDialog.tsx +4 -3
- package/src/shell/repl.js +20 -8
- package/src/shell/tui.tsx +85 -13
- package/src/shell/useSession.ts +15 -7
- package/wiki-workspace +116 -23
package/README.md
CHANGED
|
@@ -551,8 +551,9 @@ The TUI uses a two-pane layout:
|
|
|
551
551
|
Useful primitives:
|
|
552
552
|
|
|
553
553
|
```text
|
|
554
|
-
/
|
|
555
|
-
/new <name> [path]
|
|
554
|
+
/workspace list
|
|
555
|
+
/new <name> [path] # interactive TUI wizard
|
|
556
|
+
/workspace init <name> [path] # low-level non-interactive creation
|
|
556
557
|
/use <workspace>
|
|
557
558
|
/config list
|
|
558
559
|
/config use <name>
|
package/bin/wiki-manager
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
+
CACERT_PATH="${WIKI_MANAGER_CACERT_PATH:-}"
|
|
5
|
+
ARGS=()
|
|
6
|
+
while [[ $# -gt 0 ]]; do
|
|
7
|
+
case "$1" in
|
|
8
|
+
--cacert)
|
|
9
|
+
[[ $# -ge 2 ]] || { printf 'Error: --cacert requires a file path\n' >&2; exit 2; }
|
|
10
|
+
CACERT_PATH="$2"
|
|
11
|
+
shift 2
|
|
12
|
+
;;
|
|
13
|
+
*)
|
|
14
|
+
ARGS+=("$1")
|
|
15
|
+
shift
|
|
16
|
+
;;
|
|
17
|
+
esac
|
|
18
|
+
done
|
|
19
|
+
|
|
20
|
+
absolute_file_path() {
|
|
21
|
+
local value="$1"
|
|
22
|
+
local dir base
|
|
23
|
+
if [[ "$value" = /* ]]; then
|
|
24
|
+
printf '%s\n' "$value"
|
|
25
|
+
return
|
|
26
|
+
fi
|
|
27
|
+
dir="$(dirname "$value")"
|
|
28
|
+
base="$(basename "$value")"
|
|
29
|
+
printf '%s/%s\n' "$(cd "$dir" && pwd)" "$base"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if [[ -n "$CACERT_PATH" ]]; then
|
|
33
|
+
CACERT_PATH="$(absolute_file_path "$CACERT_PATH")"
|
|
34
|
+
[[ -f "$CACERT_PATH" ]] || { printf 'Error: --cacert file not found: %s\n' "$CACERT_PATH" >&2; exit 2; }
|
|
35
|
+
export WIKI_MANAGER_CACERT_PATH="$CACERT_PATH"
|
|
36
|
+
export NODE_EXTRA_CA_CERTS="$CACERT_PATH"
|
|
37
|
+
export SSL_CERT_FILE="$CACERT_PATH"
|
|
38
|
+
export REQUESTS_CA_BUNDLE="$CACERT_PATH"
|
|
39
|
+
export CURL_CA_BUNDLE="$CACERT_PATH"
|
|
40
|
+
fi
|
|
41
|
+
|
|
4
42
|
BUN_BIN="$(command -v bun 2>/dev/null || true)"
|
|
5
43
|
if [[ -z "$BUN_BIN" && -n "${BUN_INSTALL:-}" && -x "$BUN_INSTALL/bin/bun" ]]; then
|
|
6
44
|
BUN_BIN="$BUN_INSTALL/bin/bun"
|
|
@@ -27,4 +65,4 @@ while [[ -L "$SCRIPT_SOURCE" ]]; do
|
|
|
27
65
|
done
|
|
28
66
|
BIN_DIR="$(cd "$(dirname "$SCRIPT_SOURCE")" && pwd)"
|
|
29
67
|
|
|
30
|
-
exec "$BUN_BIN" "$BIN_DIR/wiki-manager.js" "
|
|
68
|
+
exec "$BUN_BIN" "$BIN_DIR/wiki-manager.js" "${ARGS[@]}"
|
package/bin/wiki-manager.js
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
function valueAfter(argv, flag) {
|
|
6
|
+
const index = argv.indexOf(flag);
|
|
7
|
+
if (index === -1) return undefined;
|
|
8
|
+
return argv[index + 1];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function stripOptionWithValue(argv, flag) {
|
|
12
|
+
const result = [];
|
|
13
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
14
|
+
if (argv[index] === flag) {
|
|
15
|
+
index += 1;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
result.push(argv[index]);
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cacertEnvVars(cacert) {
|
|
24
|
+
return {
|
|
25
|
+
WIKI_MANAGER_CACERT_PATH: cacert,
|
|
26
|
+
NODE_EXTRA_CA_CERTS: cacert,
|
|
27
|
+
SSL_CERT_FILE: cacert,
|
|
28
|
+
REQUESTS_CA_BUNDLE: cacert,
|
|
29
|
+
CURL_CA_BUNDLE: cacert,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveCacert(argv) {
|
|
34
|
+
const cacert = valueAfter(argv, '--cacert');
|
|
35
|
+
if (!cacert) return { argv, cacert: null };
|
|
36
|
+
const absolute = resolve(cacert);
|
|
37
|
+
if (!existsSync(absolute)) {
|
|
38
|
+
throw new Error(`--cacert file not found: ${absolute}`);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
argv: stripOptionWithValue(argv, '--cacert'),
|
|
42
|
+
cacert: absolute,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function reexecWithCacertIfNeeded(argv, cacert) {
|
|
47
|
+
if (!cacert || process.env.WIKI_MANAGER_CACERT_BOOTSTRAPPED === '1') return;
|
|
48
|
+
const { spawnSync } = await import('node:child_process');
|
|
49
|
+
const env = { ...process.env, WIKI_MANAGER_CACERT_BOOTSTRAPPED: '1', ...cacertEnvVars(cacert) };
|
|
50
|
+
const result = spawnSync(process.execPath, [process.argv[1], ...argv], {
|
|
51
|
+
env,
|
|
52
|
+
stdio: 'inherit',
|
|
53
|
+
});
|
|
54
|
+
if (result.error) throw result.error;
|
|
55
|
+
process.exit(result.status ?? 1);
|
|
56
|
+
}
|
|
4
57
|
|
|
5
58
|
function formatStartupError(err) {
|
|
6
59
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -21,8 +74,14 @@ function formatStartupError(err) {
|
|
|
21
74
|
}
|
|
22
75
|
|
|
23
76
|
async function main() {
|
|
24
|
-
const
|
|
25
|
-
const
|
|
77
|
+
const parsed = resolveCacert(process.argv.slice(2));
|
|
78
|
+
const argv = parsed.argv;
|
|
79
|
+
await reexecWithCacertIfNeeded(argv, parsed.cacert);
|
|
80
|
+
// Fallback for already-bootstrapped direct invocations; the shell wrapper
|
|
81
|
+
// exports these before Bun starts.
|
|
82
|
+
if (parsed.cacert) Object.assign(process.env, cacertEnvVars(parsed.cacert));
|
|
83
|
+
await import('@opentui/solid/preload');
|
|
84
|
+
const interactive = process.stdout.isTTY && process.stdin.isTTY && !argv.includes('--setup-wizard') && !argv.includes('--headless') && !argv.includes('--once') && !argv.includes('--version') && !argv.includes('-v') && !argv.includes('--help') && !argv.includes('-h');
|
|
26
85
|
if (interactive) process.stdout.write('Starting wiki-manager…\r');
|
|
27
86
|
const { runCli } = await import('../src/cli/wiki-manager.js');
|
|
28
87
|
await runCli(argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdrelle/wiki-manager",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.34",
|
|
4
4
|
"description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
|
|
5
5
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
6
6
|
"author": "dotrelle",
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
"wiki-manager": "bin/wiki-manager",
|
|
10
10
|
"wiki-workspace": "wiki-workspace"
|
|
11
11
|
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "bun ./bin/wiki-manager.js",
|
|
14
|
+
"test": "node --test src/core/activity.test.js src/core/agentEvents.test.js src/core/plan.test.js src/core/mcp.test.js src/core/documentIntake.test.js src/core/wikirc.test.js src/core/modelFetch.test.js src/commands/slash.test.js",
|
|
15
|
+
"check": "bun ./bin/wiki-manager.js --version && bun ./bin/wiki-manager.js --help && bun ./bin/wiki-manager.js --once \"verifie le mode agent\""
|
|
16
|
+
},
|
|
12
17
|
"engines": {
|
|
13
18
|
"node": ">=22.0.0",
|
|
14
19
|
"bun": ">=1.2.0"
|
|
@@ -35,6 +40,7 @@
|
|
|
35
40
|
"orchestration",
|
|
36
41
|
"cli"
|
|
37
42
|
],
|
|
43
|
+
"packageManager": "pnpm@10.29.2",
|
|
38
44
|
"dependencies": {
|
|
39
45
|
"@langchain/langgraph": "^1.3.2",
|
|
40
46
|
"@opentui/core": "^0.3.2",
|
|
@@ -47,10 +53,5 @@
|
|
|
47
53
|
},
|
|
48
54
|
"devDependencies": {
|
|
49
55
|
"@types/node": "^26.0.0"
|
|
50
|
-
},
|
|
51
|
-
"scripts": {
|
|
52
|
-
"start": "bun ./bin/wiki-manager.js",
|
|
53
|
-
"test": "node --test src/core/activity.test.js src/core/agentEvents.test.js src/core/plan.test.js src/core/mcp.test.js src/core/documentIntake.test.js",
|
|
54
|
-
"check": "bun ./bin/wiki-manager.js --version && bun ./bin/wiki-manager.js --help && bun ./bin/wiki-manager.js --once \"verifie le mode agent\""
|
|
55
56
|
}
|
|
56
|
-
}
|
|
57
|
+
}
|
package/src/agent/graph.js
CHANGED
|
@@ -17,8 +17,6 @@ const MAX_SPINNER_ARG_LENGTH = 96;
|
|
|
17
17
|
const AGENT_SLASH_COMMANDS = new Set([
|
|
18
18
|
'help',
|
|
19
19
|
'version',
|
|
20
|
-
'workspaces',
|
|
21
|
-
'new',
|
|
22
20
|
'workspace',
|
|
23
21
|
'use',
|
|
24
22
|
'config',
|
|
@@ -36,8 +34,8 @@ const SHELL_RUN_COMMAND_TOOL = {
|
|
|
36
34
|
name: 'shell__run_command',
|
|
37
35
|
description: [
|
|
38
36
|
'Run a deterministic wiki-manager slash command inside the current shell session.',
|
|
39
|
-
'Allowed commands: /
|
|
40
|
-
'Do not use for arbitrary system shell commands, /mcp call, /wiki run, /start, /stop, /logs, or /exit.',
|
|
37
|
+
'Allowed commands: /workspace list, /workspace init <name> [path], /use <workspace>, /config, /status, /services, /skills, /skills show <name>, /skills run <name>, /upload <path>, /upload convert <id|pending>, /uploads.',
|
|
38
|
+
'Do not use for arbitrary system shell commands, /workspace delete, /mcp call, /wiki run, /start, /stop, /logs, or /exit.',
|
|
41
39
|
].join(' '),
|
|
42
40
|
parameters: {
|
|
43
41
|
type: 'object',
|
|
@@ -45,7 +43,7 @@ const SHELL_RUN_COMMAND_TOOL = {
|
|
|
45
43
|
properties: {
|
|
46
44
|
command: {
|
|
47
45
|
type: 'string',
|
|
48
|
-
description: 'Slash command to run, for example "/
|
|
46
|
+
description: 'Slash command to run, for example "/workspace list", "/workspace init demo", or "/use juno".',
|
|
49
47
|
},
|
|
50
48
|
},
|
|
51
49
|
required: ['command'],
|
|
@@ -217,9 +215,6 @@ function assertAgentSlashCommandAllowed(commandLine) {
|
|
|
217
215
|
if (!AGENT_SLASH_COMMANDS.has(command)) {
|
|
218
216
|
throw new Error(`Command is not available to the agent: /${command}`);
|
|
219
217
|
}
|
|
220
|
-
if (command === 'new' && parts.length < 2) {
|
|
221
|
-
throw new Error('Usage: /new <name> [path].');
|
|
222
|
-
}
|
|
223
218
|
if (command === 'workspace' && parts[1] !== 'init') {
|
|
224
219
|
throw new Error('Only /workspace init is available to the agent.');
|
|
225
220
|
}
|
|
@@ -324,7 +319,7 @@ export function buildAgentSystemPrompt(state) {
|
|
|
324
319
|
'When the user asks for an action that can be performed with connected MCP tools or safe primitives, do not answer with future intent such as "I will call...", "I am going to run...", or "launching..." unless you also call the tool in the same turn. Either call the tool now, ask for the exact missing required arguments, or explain the concrete blocker.',
|
|
325
320
|
'For connector configuration/setup/update requests, if a matching setup/configuration tool is connected and the required arguments are known, call it immediately. If the connector or tool is not connected, say which concrete capability is missing and recommend the exact service/status primitive to inspect it. Do not invent a pending connector action in plain text.',
|
|
326
321
|
'For workspace-scoped external MCP tools, the orchestrator enforces workspace injection. Use the active workspace for configuration, source, import, export, conversion, and generation tools unless a tool is explicitly job-scoped and only requires a job id.',
|
|
327
|
-
'You can call shell__run_command for safe manager slash commands such as /
|
|
322
|
+
'You can call shell__run_command for safe manager slash commands such as /workspace list, /workspace init <name> [path], /use <workspace>, /config, /status, /services, /skills, /skills show <name>, and /skills run <name>.',
|
|
328
323
|
'Skills are workflow instructions, not executable code. When a user asks to run a skill, inspect it, propose the concrete primitive/tool plan, and ask for confirmation before costly or mutating actions.',
|
|
329
324
|
[
|
|
330
325
|
state.session.headless ? 'HEADLESS MODE ACTIVE. Execute the requested skill or task autonomously using available safe primitives and MCP tools. Do not ask for interactive confirmation unless the request is genuinely ambiguous or outside the loaded workspace.' : null,
|
|
@@ -361,7 +356,7 @@ export function buildAgentSystemPrompt(state) {
|
|
|
361
356
|
'For ingest/build/export/polish/pipeline workflows, use production MCP tools. Do not route these through direct /wiki shortcuts. To chain multiple sequential steps (e.g. build then polish), always use a single production_start_job call with type="pipeline" and steps=["build","polish"] — never start them as separate jobs: the first job is asynchronous and the second would run before it completes. For existing deliverables where content stability matters, pass stabilize:true so the build step preserves unchanged sections; keep polish in the pipeline when publication output is requested. Do not ask the user to confirm between steps; start the pipeline call directly.',
|
|
362
357
|
'Long-running MCP jobs: do not call the same status tool more than once consecutively. When chaining jobs sequentially: (1) start the job, report job/activity id and status; (2) check status once — if done, proceed to the next step immediately; (3) if still running, report status, list the remaining steps, and return control; (4) when re-invoked, check status first, then proceed. Do not spin-poll (status → status → status with no new action between). The shell activity panel monitors non-terminal jobs automatically.',
|
|
363
358
|
'If production_start_job is returned as queued/waiting by the manager, report that it is waiting in the local queue and return control. Do not continue as if the production job has started.',
|
|
364
|
-
'For diagnostics, use /wiki run doctor when the user asks for doctor. Use /
|
|
359
|
+
'For diagnostics, use /wiki run doctor when the user asks for doctor. Use /workspace init <name> [path] for low-level non-interactive workspace creation. In the interactive TUI, /new <name> opens the setup wizard. Use /wiki for index, or /wiki run index through the explicit backup hatch. Use /wiki run init only for explicit current-workspace llm-wiki init.',
|
|
365
360
|
'If an action requires tools or skills not available yet, explain the limitation and name the expected primitive.',
|
|
366
361
|
].join('\n');
|
|
367
362
|
|
package/src/cli/wiki-manager.js
CHANGED
|
@@ -7,6 +7,7 @@ loadManagerEnv();
|
|
|
7
7
|
import { createAgentGraph, buildAgentSystemPrompt, buildLimitedAgentResponse } from '../agent/graph.js';
|
|
8
8
|
import { handleSlashCommand, printHelp, printVersion } from '../commands/slash.js';
|
|
9
9
|
import { runShell } from '../shell/repl.js';
|
|
10
|
+
import { runChecks } from '../core/startupCheck.js';
|
|
10
11
|
import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
|
|
11
12
|
import { extractActivity, parseJsonText, sessionActivities } from '../core/activity.js';
|
|
12
13
|
import { extractHeadlessPlan, syncActivitiesToPlan, formatPlanStatus, formatCompletedActivities } from '../core/plan.js';
|
|
@@ -15,7 +16,7 @@ import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
|
15
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
17
|
const packageJsonPath = resolve(__dirname, '../../package.json');
|
|
17
18
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
18
|
-
const SHELL_COMMANDS = ['help', 'version', 'exit', '
|
|
19
|
+
const SHELL_COMMANDS = ['help', 'version', 'exit', 'workspace', 'new', 'use', 'config', 'status', 'services', 'start', 'stop', 'logs', 'mcp', 'wiki', 'skills', 'clear', 'chat', 'agent'];
|
|
19
20
|
|
|
20
21
|
function valueAfter(argv, flag) {
|
|
21
22
|
const index = argv.indexOf(flag);
|
|
@@ -345,6 +346,18 @@ async function runHeadless(argv, agent) {
|
|
|
345
346
|
}
|
|
346
347
|
|
|
347
348
|
export async function runCli(argv) {
|
|
349
|
+
if (argv.includes('--setup-wizard')) {
|
|
350
|
+
if (!process.versions.bun) {
|
|
351
|
+
throw new Error('Setup wizard requires Bun. Run: bun ./bin/wiki-manager.js --setup-wizard');
|
|
352
|
+
}
|
|
353
|
+
const { runSetupWizard } = await import('../shell/tui.tsx');
|
|
354
|
+
await runSetupWizard({
|
|
355
|
+
workspaceName: valueAfter(argv, '--workspace-name'),
|
|
356
|
+
workspacePath: valueAfter(argv, '--workspace-path') ?? null,
|
|
357
|
+
});
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
348
361
|
if (argv.includes('--version') || argv.includes('-v')) {
|
|
349
362
|
printVersion(packageJson);
|
|
350
363
|
return;
|
|
@@ -380,7 +393,9 @@ export async function runCli(argv) {
|
|
|
380
393
|
if (!process.versions.bun) {
|
|
381
394
|
throw new Error('Interactive TUI requires Bun. Run: bun ./bin/wiki-manager.js');
|
|
382
395
|
}
|
|
383
|
-
const { runOpenTuiShell } = await import('../shell/tui.tsx');
|
|
396
|
+
const { runOpenTuiShell, runStartupWizard } = await import('../shell/tui.tsx');
|
|
397
|
+
const gaps = await runChecks();
|
|
398
|
+
if (gaps.length > 0) await runStartupWizard(gaps);
|
|
384
399
|
await runOpenTuiShell({ agent, packageJson });
|
|
385
400
|
return;
|
|
386
401
|
}
|
package/src/commands/slash.js
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
resolveWikircProfile,
|
|
31
31
|
summarizeWikircConfig,
|
|
32
32
|
} from '../core/wikirc.js';
|
|
33
|
+
import { deleteWorkspaceAndFiles, startAgents, stopAgents } from '../core/wikiSetup.js';
|
|
33
34
|
import {
|
|
34
35
|
cleanDocumentUploads,
|
|
35
36
|
convertPendingDocumentUploads,
|
|
@@ -433,8 +434,7 @@ async function createWorkspaceCommand(context, workspaceName, targetPath) {
|
|
|
433
434
|
output: [
|
|
434
435
|
'Usage: /new <name> [path]',
|
|
435
436
|
'',
|
|
436
|
-
'Creates
|
|
437
|
-
'Legacy form: /workspace init <name> [path].',
|
|
437
|
+
'Creates and registers a workspace via wiki-workspace config.',
|
|
438
438
|
'For llm-wiki init inside the current workspace, use /wiki run init.',
|
|
439
439
|
].join('\n'),
|
|
440
440
|
};
|
|
@@ -456,6 +456,7 @@ async function createWorkspaceCommand(context, workspaceName, targetPath) {
|
|
|
456
456
|
}
|
|
457
457
|
}
|
|
458
458
|
|
|
459
|
+
|
|
459
460
|
function formatMcpCallActivity(serverName, toolName, resultText) {
|
|
460
461
|
if (serverName === 'production') return null;
|
|
461
462
|
return formatActivitySummary(serverName, toolName, resultText);
|
|
@@ -551,6 +552,47 @@ function loadSessionWikirc(session, profileName = 'default') {
|
|
|
551
552
|
return summarizeWikircConfig(loaded.profile, loaded.config);
|
|
552
553
|
}
|
|
553
554
|
|
|
555
|
+
function clearWorkspaceSession(session) {
|
|
556
|
+
session.workspace = null;
|
|
557
|
+
session.workspacePath = null;
|
|
558
|
+
session.workspaceEnv = null;
|
|
559
|
+
session.workspaceEnvFile = null;
|
|
560
|
+
session.wikirc = null;
|
|
561
|
+
session.wikircConfig = null;
|
|
562
|
+
session.language = null;
|
|
563
|
+
session.llm = null;
|
|
564
|
+
session.mcp = null;
|
|
565
|
+
session.systemPrompt = null;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function formatWorkspaceList(workspaces, session = null) {
|
|
569
|
+
if (workspaces.length === 0) return 'No workspace configured.';
|
|
570
|
+
return [
|
|
571
|
+
'Workspaces',
|
|
572
|
+
'',
|
|
573
|
+
...workspaces.flatMap((workspace) => {
|
|
574
|
+
const active = workspace.name === session?.workspace ? 'active' : 'available';
|
|
575
|
+
return [
|
|
576
|
+
`${workspace.name}\t${active}`,
|
|
577
|
+
` path\t${workspace.workspacePath}`,
|
|
578
|
+
` use\t/use ${workspace.name}`,
|
|
579
|
+
` delete\t/workspace delete ${workspace.name}`,
|
|
580
|
+
'',
|
|
581
|
+
];
|
|
582
|
+
}),
|
|
583
|
+
].join('\n').trimEnd();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function workspaceDeletePrompt(workspaces) {
|
|
587
|
+
if (workspaces.length === 0) return 'No workspace configured.';
|
|
588
|
+
return [
|
|
589
|
+
'Delete a workspace:',
|
|
590
|
+
...workspaces.map((workspace) => ` /workspace delete ${workspace.name}\t${workspace.workspacePath}`),
|
|
591
|
+
'',
|
|
592
|
+
'The next step asks for confirmation before deleting files.',
|
|
593
|
+
].join('\n');
|
|
594
|
+
}
|
|
595
|
+
|
|
554
596
|
export function helpText(packageJson) {
|
|
555
597
|
return `wiki-manager ${packageJson.version}
|
|
556
598
|
|
|
@@ -562,6 +604,7 @@ Usage:
|
|
|
562
604
|
Options:
|
|
563
605
|
-v, --version Print version
|
|
564
606
|
-h, --help Print help
|
|
607
|
+
--cacert <path> Trust a local CA; Docker must be able to read this host path
|
|
565
608
|
--once <prompt> Run one agent turn and exit
|
|
566
609
|
--headless Run a workspace task non-interactively
|
|
567
610
|
--workspace <name> Workspace for --headless
|
|
@@ -575,12 +618,12 @@ Options:
|
|
|
575
618
|
|
|
576
619
|
Interactive shell:
|
|
577
620
|
${helpPair('/help', 'Help', '/version', 'Version')}
|
|
578
|
-
${helpPair('/
|
|
621
|
+
${helpPair('/workspace list', 'Workspaces', '/new <n> [path]', 'New workspace')}
|
|
579
622
|
${helpPair('/use <workspace>', 'Use workspace', '/status', 'Session status')}
|
|
580
623
|
${helpPair('/config list', 'Config profiles', '/config use <n>', 'Use config')}
|
|
581
|
-
${helpPair('/config edit <n>', 'Edit config', '/
|
|
582
|
-
${helpPair('/services', 'Services', '/start [service]', 'Start service(s)')}
|
|
583
|
-
${helpPair('/stop [service]', 'Stop service(s)', '/logs <service>', 'Service logs')}
|
|
624
|
+
${helpPair('/config edit <n>', 'Edit config', '/workspace delete <n>', 'Delete workspace')}
|
|
625
|
+
${helpPair('/services', 'Services', '/start [service|agents]', 'Start service(s)')}
|
|
626
|
+
${helpPair('/stop [service|agents]', 'Stop service(s)', '/logs <service>', 'Service logs')}
|
|
584
627
|
${helpPair('/skills', 'List skills', '/skills show <n>', 'Show skill')}
|
|
585
628
|
${helpPair('/skills run <n>', 'Run skill guide', '/skills edit <n>', 'Edit skill')}
|
|
586
629
|
${helpPair('/mcp status', 'MCP status', '/mcp endpoints', 'MCP endpoints')}
|
|
@@ -617,6 +660,16 @@ export async function handleSlashCommand(line, context) {
|
|
|
617
660
|
const args = line.slice(1).trim().split(/\s+/).filter(Boolean);
|
|
618
661
|
const [command] = args;
|
|
619
662
|
const step = context.onStep ?? (() => {});
|
|
663
|
+
const runAgentCommand = async (fn, verb) => {
|
|
664
|
+
try {
|
|
665
|
+
step(`Agents: ${verb}ing external agents…`);
|
|
666
|
+
const output = await fn();
|
|
667
|
+
return { output: output || `Agents ${verb}ed.` };
|
|
668
|
+
} catch (err) {
|
|
669
|
+
step(formatActivityError('agents', verb, err));
|
|
670
|
+
return { output: err instanceof Error ? err.message : String(err) };
|
|
671
|
+
}
|
|
672
|
+
};
|
|
620
673
|
|
|
621
674
|
switch (command) {
|
|
622
675
|
case '':
|
|
@@ -630,17 +683,6 @@ export async function handleSlashCommand(line, context) {
|
|
|
630
683
|
case 'agent':
|
|
631
684
|
context.session.chatMode = false;
|
|
632
685
|
return { setMode: 'agent', output: 'Mode: agent' };
|
|
633
|
-
case 'workspaces': {
|
|
634
|
-
const workspaces = listWorkspaces();
|
|
635
|
-
if (workspaces.length === 0) {
|
|
636
|
-
return { output: 'No workspace configured.' };
|
|
637
|
-
}
|
|
638
|
-
return {
|
|
639
|
-
output: workspaces
|
|
640
|
-
.map((workspace) => `${workspace.name}\t${workspace.workspacePath}`)
|
|
641
|
-
.join('\n'),
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
686
|
case 'status': {
|
|
645
687
|
step('Shell: refreshing workspace, services and MCP status…');
|
|
646
688
|
return { output: await statusText(context.session) };
|
|
@@ -654,15 +696,11 @@ export async function handleSlashCommand(line, context) {
|
|
|
654
696
|
if (!workspace) {
|
|
655
697
|
return { output: `Workspace not found: ${workspaceName}` };
|
|
656
698
|
}
|
|
699
|
+
clearWorkspaceSession(context.session);
|
|
657
700
|
context.session.workspace = workspace.name;
|
|
658
701
|
context.session.workspacePath = workspace.workspacePath;
|
|
659
702
|
context.session.workspaceEnv = workspace.env;
|
|
660
703
|
context.session.workspaceEnvFile = workspace.envFile;
|
|
661
|
-
context.session.wikirc = null;
|
|
662
|
-
context.session.wikircConfig = null;
|
|
663
|
-
context.session.language = null;
|
|
664
|
-
context.session.llm = null;
|
|
665
|
-
context.session.mcp = null;
|
|
666
704
|
context.session.systemPrompt = loadWorkspaceSystemPrompt(workspace.workspacePath);
|
|
667
705
|
try {
|
|
668
706
|
step(`Workspace: loading ${workspace.name} config…`);
|
|
@@ -777,6 +815,7 @@ export async function handleSlashCommand(line, context) {
|
|
|
777
815
|
}
|
|
778
816
|
case 'start': {
|
|
779
817
|
const service = args[1];
|
|
818
|
+
if (service === 'agents') return runAgentCommand(startAgents, 'start');
|
|
780
819
|
try {
|
|
781
820
|
step(`Services: starting ${service ?? 'workspace services'}…`);
|
|
782
821
|
const output = await startService(context.session, service);
|
|
@@ -791,6 +830,7 @@ export async function handleSlashCommand(line, context) {
|
|
|
791
830
|
}
|
|
792
831
|
case 'stop': {
|
|
793
832
|
const service = args[1];
|
|
833
|
+
if (service === 'agents') return runAgentCommand(stopAgents, 'stop');
|
|
794
834
|
try {
|
|
795
835
|
step(`Services: stopping ${service ?? 'workspace services'}…`);
|
|
796
836
|
const output = await stopService(context.session, service);
|
|
@@ -954,12 +994,50 @@ export async function handleSlashCommand(line, context) {
|
|
|
954
994
|
case 'new': {
|
|
955
995
|
return createWorkspaceCommand(context, args[1], args[2] ?? null);
|
|
956
996
|
}
|
|
957
|
-
case 'workspace':
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
997
|
+
case 'workspace':
|
|
998
|
+
case 'workplace': {
|
|
999
|
+
const subcommand = args[1] ?? 'list';
|
|
1000
|
+
if (subcommand === 'list') {
|
|
1001
|
+
return { output: formatWorkspaceList(listWorkspaces(), context.session) };
|
|
1002
|
+
}
|
|
1003
|
+
if (subcommand === 'delete') {
|
|
1004
|
+
const workspaceName = args[2];
|
|
1005
|
+
const confirmed = args.includes('--confirm');
|
|
1006
|
+
const workspaces = listWorkspaces();
|
|
1007
|
+
if (!workspaceName) return { output: workspaceDeletePrompt(workspaces) };
|
|
1008
|
+
const workspace = workspaces.find((item) => item.name === workspaceName);
|
|
1009
|
+
if (!workspace) return { output: `Workspace not found: ${workspaceName}` };
|
|
1010
|
+
if (!confirmed) {
|
|
1011
|
+
return {
|
|
1012
|
+
output: [
|
|
1013
|
+
`Confirm workspace deletion: ${workspace.name}`,
|
|
1014
|
+
`Path: ${workspace.workspacePath}`,
|
|
1015
|
+
'This removes the registry entry and deletes the workspace files.',
|
|
1016
|
+
'',
|
|
1017
|
+
`Run: /workspace delete ${workspace.name} --confirm`,
|
|
1018
|
+
].join('\n'),
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
try {
|
|
1022
|
+
step(`Workspace: deleting ${workspace.name}…`);
|
|
1023
|
+
const result = await deleteWorkspaceAndFiles(workspace, workspace.workspacePath);
|
|
1024
|
+
const wasCurrent = context.session.workspace === workspace.name
|
|
1025
|
+
|| context.session.workspacePath === workspace.workspacePath;
|
|
1026
|
+
if (wasCurrent) clearWorkspaceSession(context.session);
|
|
1027
|
+
return {
|
|
1028
|
+
output: [
|
|
1029
|
+
`Deleted workspace: ${workspace.name}`,
|
|
1030
|
+
`Removed registry entry and files at: ${result.deletedPath}`,
|
|
1031
|
+
wasCurrent ? 'Current session cleared. Use /use <workspace> or /workspace init <name> [path].' : null,
|
|
1032
|
+
].filter(Boolean).join('\n'),
|
|
1033
|
+
};
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1036
|
+
return { output: message };
|
|
1037
|
+
}
|
|
961
1038
|
}
|
|
962
|
-
return createWorkspaceCommand(context, args[2], args[3] ?? null);
|
|
1039
|
+
if (subcommand === 'init') return createWorkspaceCommand(context, args[2], args[3] ?? null);
|
|
1040
|
+
return { output: 'Usage: /workspace <list|delete <name> --confirm|init <name> [path]>' };
|
|
963
1041
|
}
|
|
964
1042
|
case 'wiki': {
|
|
965
1043
|
const subcommand = args[1];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { mkdtemp } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import { handleSlashCommand } from './slash.js';
|
|
8
|
+
|
|
9
|
+
test('/workspace delete removes files and clears current session context after confirmation', async () => {
|
|
10
|
+
const root = await mkdtemp(join(tmpdir(), 'wiki-manager-delete-workspace-'));
|
|
11
|
+
const registryRoot = join(root, 'registry');
|
|
12
|
+
const registryPath = join(registryRoot, 'demo');
|
|
13
|
+
const workspacePath = join(root, 'workspace');
|
|
14
|
+
mkdirSync(registryPath, { recursive: true });
|
|
15
|
+
mkdirSync(workspacePath, { recursive: true });
|
|
16
|
+
writeFileSync(join(registryPath, '.env'), [
|
|
17
|
+
'WORKSPACE_NAME=demo',
|
|
18
|
+
`WIKI_WORKSPACE_PATH=${workspacePath}`,
|
|
19
|
+
'',
|
|
20
|
+
].join('\n'), 'utf8');
|
|
21
|
+
|
|
22
|
+
const previousDir = process.env.WIKI_WORKSPACES_DIR;
|
|
23
|
+
process.env.WIKI_WORKSPACES_DIR = registryRoot;
|
|
24
|
+
const session = {
|
|
25
|
+
workspace: 'demo',
|
|
26
|
+
workspacePath,
|
|
27
|
+
workspaceEnv: { WORKSPACE_NAME: 'demo' },
|
|
28
|
+
workspaceEnvFile: join(registryPath, '.env'),
|
|
29
|
+
wikirc: { profile: 'default' },
|
|
30
|
+
wikircConfig: {},
|
|
31
|
+
language: 'en-US',
|
|
32
|
+
llm: {},
|
|
33
|
+
mcp: {},
|
|
34
|
+
systemPrompt: 'prompt',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const prompt = await handleSlashCommand('/workspace delete demo', {
|
|
39
|
+
packageJson: { version: 'test' },
|
|
40
|
+
session,
|
|
41
|
+
});
|
|
42
|
+
assert.match(prompt.output, /Confirm workspace deletion: demo/);
|
|
43
|
+
assert.equal(existsSync(workspacePath), true);
|
|
44
|
+
assert.equal(session.workspace, 'demo');
|
|
45
|
+
|
|
46
|
+
const result = await handleSlashCommand('/workspace delete demo --confirm', {
|
|
47
|
+
packageJson: { version: 'test' },
|
|
48
|
+
session,
|
|
49
|
+
});
|
|
50
|
+
assert.match(result.output, /Deleted workspace: demo/);
|
|
51
|
+
assert.equal(session.workspace, null);
|
|
52
|
+
assert.equal(session.workspacePath, null);
|
|
53
|
+
assert.equal(session.llm, null);
|
|
54
|
+
assert.equal(session.mcp, null);
|
|
55
|
+
} finally {
|
|
56
|
+
if (previousDir === undefined) delete process.env.WIKI_WORKSPACES_DIR;
|
|
57
|
+
else process.env.WIKI_WORKSPACES_DIR = previousDir;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('/new without a name shows usage', async () => {
|
|
62
|
+
const result = await handleSlashCommand('/new', {
|
|
63
|
+
packageJson: { version: 'test' },
|
|
64
|
+
session: {},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
assert.match(result.output ?? '', /Usage/i);
|
|
68
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
import { managerRuntimeDir } from './env.js';
|
|
5
|
+
|
|
6
|
+
export const CACERT_CONTAINER_PATH = '/wiki-manager-ca.pem';
|
|
7
|
+
export const CACERT_ENV_KEYS = [
|
|
8
|
+
'NODE_EXTRA_CA_CERTS',
|
|
9
|
+
'SSL_CERT_FILE',
|
|
10
|
+
'REQUESTS_CA_BUNDLE',
|
|
11
|
+
'CURL_CA_BUNDLE',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export function activeCacertPath() {
|
|
15
|
+
const value = process.env.WIKI_MANAGER_CACERT_PATH;
|
|
16
|
+
return value ? resolve(value) : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function cacertEnv(path = activeCacertPath(), targetPath = path) {
|
|
20
|
+
if (!path) return {};
|
|
21
|
+
return Object.fromEntries(CACERT_ENV_KEYS.map((key) => [key, targetPath]));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseComposeServices(composeFilePath) {
|
|
25
|
+
const parsed = YAML.parse(readFileSync(composeFilePath, 'utf8')) ?? {};
|
|
26
|
+
return Object.keys(parsed.services ?? {}).filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function composeOverrideContent(cacertPath, services) {
|
|
30
|
+
const serviceEntries = Object.fromEntries(
|
|
31
|
+
services.map((service) => [
|
|
32
|
+
service,
|
|
33
|
+
{
|
|
34
|
+
volumes: [`${cacertPath}:${CACERT_CONTAINER_PATH}:ro`],
|
|
35
|
+
environment: cacertEnv(cacertPath, CACERT_CONTAINER_PATH),
|
|
36
|
+
},
|
|
37
|
+
]),
|
|
38
|
+
);
|
|
39
|
+
const doc = {
|
|
40
|
+
services: serviceEntries,
|
|
41
|
+
};
|
|
42
|
+
return [
|
|
43
|
+
'# Generated by wiki-manager. Safe to delete.',
|
|
44
|
+
'# Rewritten when --cacert is used with a Docker Compose command.',
|
|
45
|
+
YAML.stringify(doc).trimEnd(),
|
|
46
|
+
'',
|
|
47
|
+
].join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let _cacertOverrideCache;
|
|
51
|
+
|
|
52
|
+
export function ensureCacertComposeOverride(composeFilePath, fileName = 'cacert.compose.yml') {
|
|
53
|
+
if (_cacertOverrideCache !== undefined) return _cacertOverrideCache;
|
|
54
|
+
const cacertPath = activeCacertPath();
|
|
55
|
+
if (!cacertPath) return (_cacertOverrideCache = null);
|
|
56
|
+
if (!existsSync(cacertPath)) {
|
|
57
|
+
throw new Error(`--cacert file not found: ${cacertPath}`);
|
|
58
|
+
}
|
|
59
|
+
const services = parseComposeServices(composeFilePath);
|
|
60
|
+
if (services.length === 0) return null;
|
|
61
|
+
const runtimeDir = managerRuntimeDir();
|
|
62
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
63
|
+
const overridePath = join(runtimeDir, fileName);
|
|
64
|
+
writeFileSync(overridePath, composeOverrideContent(cacertPath, services), 'utf8');
|
|
65
|
+
return (_cacertOverrideCache = overridePath);
|
|
66
|
+
}
|