@nemus-cli/nemus 0.2.1
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/CHANGELOG.md +86 -0
- package/LICENSE +21 -0
- package/README.md +275 -0
- package/assets/README.md +53 -0
- package/assets/banner.png +0 -0
- package/assets/banner.svg +66 -0
- package/assets/favicon-32.png +0 -0
- package/assets/favicon-64.png +0 -0
- package/assets/icon-256.png +0 -0
- package/assets/icon.svg +23 -0
- package/assets/logo-256.png +0 -0
- package/assets/logo-512.png +0 -0
- package/assets/logo-wordmark.svg +35 -0
- package/assets/logo.svg +32 -0
- package/bin/workspace.js +130 -0
- package/dist/cli/ai-prompt.js +421 -0
- package/dist/cli/dashboard/AgentList.js +61 -0
- package/dist/cli/dashboard/DashboardSidebar.js +280 -0
- package/dist/cli/dashboard/HelpFooter.js +73 -0
- package/dist/cli/dashboard/StatusBadge.js +50 -0
- package/dist/cli/dashboard/index.js +11 -0
- package/dist/cli/dashboard/types.js +2 -0
- package/dist/cli/tui.js +48 -0
- package/dist/commands/ai-prompt.js +22 -0
- package/dist/commands/analyze-deps.js +138 -0
- package/dist/commands/archive.js +113 -0
- package/dist/commands/branch/create.js +119 -0
- package/dist/commands/branch/index.js +104 -0
- package/dist/commands/branch/merge.js +90 -0
- package/dist/commands/branch/rebase.js +84 -0
- package/dist/commands/branch/switch.js +165 -0
- package/dist/commands/cache/index.js +80 -0
- package/dist/commands/cache/manager.js +198 -0
- package/dist/commands/cleanup.js +153 -0
- package/dist/commands/configure-claude.js +55 -0
- package/dist/commands/configure.js +231 -0
- package/dist/commands/create.js +204 -0
- package/dist/commands/dashboard/hook-handler.js +162 -0
- package/dist/commands/dashboard/index.js +258 -0
- package/dist/commands/dashboard/launcher.js +40 -0
- package/dist/commands/dashboard/session-picker.js +106 -0
- package/dist/commands/dashboard/sidebar.js +39 -0
- package/dist/commands/dashboard/workspace-picker.js +95 -0
- package/dist/commands/delete.js +194 -0
- package/dist/commands/deprecated-aliases.js +44 -0
- package/dist/commands/diff.js +129 -0
- package/dist/commands/doctor.js +141 -0
- package/dist/commands/generate-docs.js +77 -0
- package/dist/commands/ghq-status.js +72 -0
- package/dist/commands/go.js +154 -0
- package/dist/commands/history.js +124 -0
- package/dist/commands/list.js +191 -0
- package/dist/commands/mcp/index.js +69 -0
- package/dist/commands/migrate.js +296 -0
- package/dist/commands/remove-repo.js +217 -0
- package/dist/commands/report-bug.js +112 -0
- package/dist/commands/run.js +147 -0
- package/dist/commands/save-context.js +171 -0
- package/dist/commands/sessions.js +129 -0
- package/dist/commands/status.js +138 -0
- package/dist/commands/suite/create.js +212 -0
- package/dist/commands/suite/delete.js +64 -0
- package/dist/commands/suite/export.js +126 -0
- package/dist/commands/suite/import.js +128 -0
- package/dist/commands/suite/index.js +94 -0
- package/dist/commands/suite/list.js +49 -0
- package/dist/commands/suite/use.js +227 -0
- package/dist/commands/sync.js +117 -0
- package/dist/commands/update.js +203 -0
- package/dist/mcp/install.js +550 -0
- package/dist/mcp/server.js +398 -0
- package/dist/mcp/tools.js +922 -0
- package/dist/pi-extensions/permission-sync.ts.template +41 -0
- package/dist/pi-extensions/permissions-gate.ts.template +69 -0
- package/dist/pi-extensions/pr-fix-trigger.ts.template +124 -0
- package/dist/pi-extensions/review-reminder.ts.template +58 -0
- package/dist/pi-extensions/workspace-input-status.ts.template +919 -0
- package/dist/program.js +136 -0
- package/dist/scripts/workspace-table.py +394 -0
- package/dist/types/dashboard.js +11 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/agent-config.js +263 -0
- package/dist/utils/agent-state.js +178 -0
- package/dist/utils/banner.js +24 -0
- package/dist/utils/branch-operations.js +232 -0
- package/dist/utils/bug-report.js +355 -0
- package/dist/utils/cache.js +168 -0
- package/dist/utils/claude-integration.js +617 -0
- package/dist/utils/claude-sessions.js +260 -0
- package/dist/utils/cleanup-operations.js +120 -0
- package/dist/utils/colors.js +26 -0
- package/dist/utils/command-helpers.js +44 -0
- package/dist/utils/config.js +150 -0
- package/dist/utils/context-file.js +33 -0
- package/dist/utils/dashboard-hooks.js +172 -0
- package/dist/utils/dependency-analyzer.js +234 -0
- package/dist/utils/diff-operations.js +80 -0
- package/dist/utils/doc-generator.js +184 -0
- package/dist/utils/ghq-integration.js +332 -0
- package/dist/utils/git-operations.js +237 -0
- package/dist/utils/git-status.js +187 -0
- package/dist/utils/github.js +68 -0
- package/dist/utils/health-checks.js +345 -0
- package/dist/utils/history.js +150 -0
- package/dist/utils/hooks.js +100 -0
- package/dist/utils/logger.js +40 -0
- package/dist/utils/permission-sync.js +681 -0
- package/dist/utils/pi-extensions.js +224 -0
- package/dist/utils/progress.js +92 -0
- package/dist/utils/prompts.js +279 -0
- package/dist/utils/repo-resolver.js +260 -0
- package/dist/utils/retry.js +65 -0
- package/dist/utils/run-operations.js +30 -0
- package/dist/utils/suite.js +159 -0
- package/dist/utils/sync-operations.js +107 -0
- package/dist/utils/tmux-dashboard.js +184 -0
- package/dist/utils/validation.js +153 -0
- package/dist/utils/version-check.js +121 -0
- package/dist/utils/workspace-meta.js +185 -0
- package/install-shell-integration.sh +532 -0
- package/install.sh +401 -0
- package/package.json +90 -0
- package/remove-shell-block.awk +44 -0
- package/scripts/postinstall.js +85 -0
- package/skills/analyze-deps.md +23 -0
- package/skills/archive-workspace.md +26 -0
- package/skills/branch-create.md +18 -0
- package/skills/create-workspace.md +39 -0
- package/skills/delete-workspace.md +37 -0
- package/skills/list-org-repos.md +26 -0
- package/skills/list-suites.md +24 -0
- package/skills/list-workspaces.md +27 -0
- package/skills/refresh-workspace-docs.md +89 -0
- package/skills/remove-repo.md +25 -0
- package/skills/run-command.md +23 -0
- package/skills/save-context.md +68 -0
- package/skills/search-repos.md +31 -0
- package/skills/snapshot-list.md +18 -0
- package/skills/snapshot-save.md +12 -0
- package/skills/switch-branch.md +18 -0
- package/skills/update-cache.md +21 -0
- package/skills/update-workspace.md +66 -0
- package/skills/workspace-cleanup.md +18 -0
- package/skills/workspace-diff.md +23 -0
- package/skills/workspace-doctor.md +23 -0
- package/skills/workspace-info.md +45 -0
- package/skills/workspace-manager/SKILL.md +201 -0
- package/skills/workspace-manager/references/ai-prompt.md +44 -0
- package/skills/workspace-manager/references/analyze-deps.md +45 -0
- package/skills/workspace-manager/references/archive-workspace.md +38 -0
- package/skills/workspace-manager/references/branch-create.md +31 -0
- package/skills/workspace-manager/references/branch-merge.md +22 -0
- package/skills/workspace-manager/references/branch-rebase.md +22 -0
- package/skills/workspace-manager/references/cache-clear.md +18 -0
- package/skills/workspace-manager/references/cache-info.md +22 -0
- package/skills/workspace-manager/references/configure-claude.md +31 -0
- package/skills/workspace-manager/references/configure.md +35 -0
- package/skills/workspace-manager/references/create-workspace.md +45 -0
- package/skills/workspace-manager/references/dashboard.md +61 -0
- package/skills/workspace-manager/references/delete-workspace.md +33 -0
- package/skills/workspace-manager/references/generate-docs.md +36 -0
- package/skills/workspace-manager/references/ghq-status.md +28 -0
- package/skills/workspace-manager/references/go.md +33 -0
- package/skills/workspace-manager/references/history.md +41 -0
- package/skills/workspace-manager/references/list-org-repos.md +26 -0
- package/skills/workspace-manager/references/list-suites.md +21 -0
- package/skills/workspace-manager/references/list-workspaces.md +27 -0
- package/skills/workspace-manager/references/mcp.md +49 -0
- package/skills/workspace-manager/references/remove-repo.md +26 -0
- package/skills/workspace-manager/references/run-command.md +44 -0
- package/skills/workspace-manager/references/search-repos.md +24 -0
- package/skills/workspace-manager/references/sessions.md +27 -0
- package/skills/workspace-manager/references/suite-create.md +22 -0
- package/skills/workspace-manager/references/suite-delete.md +21 -0
- package/skills/workspace-manager/references/suite-export.md +19 -0
- package/skills/workspace-manager/references/suite-import.md +19 -0
- package/skills/workspace-manager/references/suite-use.md +27 -0
- package/skills/workspace-manager/references/switch-branch.md +25 -0
- package/skills/workspace-manager/references/update-cache.md +22 -0
- package/skills/workspace-manager/references/update-workspace.md +50 -0
- package/skills/workspace-manager/references/workspace-cleanup.md +33 -0
- package/skills/workspace-manager/references/workspace-diff.md +27 -0
- package/skills/workspace-manager/references/workspace-doctor.md +27 -0
- package/skills/workspace-manager/references/workspace-info.md +33 -0
- package/skills/workspace-manager/references/workspace-status.md +43 -0
- package/skills/workspace-manager/references/workspace-sync.md +26 -0
- package/skills/workspace-status.md +26 -0
- package/skills/workspace-sync.md +23 -0
- package/src/cli/ai-prompt.test.ts +667 -0
- package/src/cli/ai-prompt.ts +421 -0
- package/src/cli/dashboard/AgentList.tsx +44 -0
- package/src/cli/dashboard/DashboardSidebar.tsx +291 -0
- package/src/cli/dashboard/HelpFooter.tsx +22 -0
- package/src/cli/dashboard/StatusBadge.tsx +23 -0
- package/src/cli/dashboard/index.ts +5 -0
- package/src/cli/dashboard/types.ts +16 -0
- package/src/cli/tui.tsx +51 -0
- package/src/cli/workspace-cli.test.ts +30 -0
- package/src/commands/ai-prompt.ts +21 -0
- package/src/commands/analyze-deps.ts +119 -0
- package/src/commands/archive.ts +114 -0
- package/src/commands/branch/create.ts +94 -0
- package/src/commands/branch/index.ts +74 -0
- package/src/commands/branch/merge.ts +61 -0
- package/src/commands/branch/rebase.ts +54 -0
- package/src/commands/branch/switch.ts +150 -0
- package/src/commands/cache/index.ts +51 -0
- package/src/commands/cache/manager.ts +168 -0
- package/src/commands/cleanup.ts +132 -0
- package/src/commands/configure-claude.ts +55 -0
- package/src/commands/configure.ts +204 -0
- package/src/commands/create.ts +196 -0
- package/src/commands/dashboard/hook-handler.ts +138 -0
- package/src/commands/dashboard/index.ts +248 -0
- package/src/commands/dashboard/launcher.ts +42 -0
- package/src/commands/dashboard/session-picker.ts +73 -0
- package/src/commands/dashboard/sidebar.js +39 -0
- package/src/commands/dashboard/workspace-picker.ts +62 -0
- package/src/commands/delete.test.ts +250 -0
- package/src/commands/delete.ts +171 -0
- package/src/commands/deprecated-aliases.ts +55 -0
- package/src/commands/diff.ts +106 -0
- package/src/commands/doctor.ts +110 -0
- package/src/commands/generate-docs.ts +45 -0
- package/src/commands/ghq-status.ts +70 -0
- package/src/commands/go.ts +125 -0
- package/src/commands/history.ts +136 -0
- package/src/commands/list.test.ts +219 -0
- package/src/commands/list.ts +176 -0
- package/src/commands/mcp/index.ts +39 -0
- package/src/commands/migrate.ts +277 -0
- package/src/commands/remove-repo.ts +194 -0
- package/src/commands/report-bug.ts +88 -0
- package/src/commands/run.ts +125 -0
- package/src/commands/save-context.test.ts +113 -0
- package/src/commands/save-context.ts +141 -0
- package/src/commands/sessions.ts +99 -0
- package/src/commands/status.ts +111 -0
- package/src/commands/suite/create.ts +240 -0
- package/src/commands/suite/delete.ts +65 -0
- package/src/commands/suite/export.ts +97 -0
- package/src/commands/suite/import.ts +100 -0
- package/src/commands/suite/index.ts +66 -0
- package/src/commands/suite/list.ts +54 -0
- package/src/commands/suite/use.ts +232 -0
- package/src/commands/sync.ts +97 -0
- package/src/commands/update.ts +197 -0
- package/src/mcp/install.ts +529 -0
- package/src/mcp/server.ts +579 -0
- package/src/mcp/tools.test.ts +545 -0
- package/src/mcp/tools.ts +1019 -0
- package/src/mcp/update-workspace.test.ts +220 -0
- package/src/pi-extensions/permission-sync.ts.template +41 -0
- package/src/pi-extensions/permissions-gate.ts.template +69 -0
- package/src/pi-extensions/pr-fix-trigger.ts.template +124 -0
- package/src/pi-extensions/review-reminder.ts.template +58 -0
- package/src/pi-extensions/workspace-input-status.ts.template +919 -0
- package/src/program.ts +112 -0
- package/src/scripts/workspace-table.py +394 -0
- package/src/types/dashboard.ts +29 -0
- package/src/types/index.ts +109 -0
- package/src/types/ink.d.ts +171 -0
- package/src/utils/agent-config.test.ts +303 -0
- package/src/utils/agent-config.ts +264 -0
- package/src/utils/agent-state.test.ts +166 -0
- package/src/utils/agent-state.ts +135 -0
- package/src/utils/banner.ts +24 -0
- package/src/utils/branch-operations.ts +237 -0
- package/src/utils/bug-report.test.ts +174 -0
- package/src/utils/bug-report.ts +355 -0
- package/src/utils/cache.ts +150 -0
- package/src/utils/claude-integration.test.ts +381 -0
- package/src/utils/claude-integration.ts +649 -0
- package/src/utils/claude-sessions.test.ts +262 -0
- package/src/utils/claude-sessions.ts +251 -0
- package/src/utils/cleanup-operations.ts +94 -0
- package/src/utils/colors.ts +25 -0
- package/src/utils/command-helpers.ts +46 -0
- package/src/utils/config.test.ts +203 -0
- package/src/utils/config.ts +133 -0
- package/src/utils/context-file.ts +36 -0
- package/src/utils/dashboard-hooks.test.ts +168 -0
- package/src/utils/dashboard-hooks.ts +163 -0
- package/src/utils/dependency-analyzer.ts +243 -0
- package/src/utils/diff-operations.ts +59 -0
- package/src/utils/doc-generator.ts +187 -0
- package/src/utils/ghq-integration.test.ts +195 -0
- package/src/utils/ghq-integration.ts +317 -0
- package/src/utils/git-operations.test.ts +265 -0
- package/src/utils/git-operations.ts +233 -0
- package/src/utils/git-status.ts +171 -0
- package/src/utils/github.ts +74 -0
- package/src/utils/health-checks.ts +359 -0
- package/src/utils/history.ts +140 -0
- package/src/utils/hooks.ts +92 -0
- package/src/utils/logger.ts +37 -0
- package/src/utils/permission-sync.test.ts +593 -0
- package/src/utils/permission-sync.ts +720 -0
- package/src/utils/pi-extensions.test.ts +182 -0
- package/src/utils/pi-extensions.ts +226 -0
- package/src/utils/progress.ts +73 -0
- package/src/utils/prompts.test.ts +169 -0
- package/src/utils/prompts.ts +296 -0
- package/src/utils/repo-resolver.test.ts +313 -0
- package/src/utils/repo-resolver.ts +294 -0
- package/src/utils/retry.test.ts +124 -0
- package/src/utils/retry.ts +85 -0
- package/src/utils/run-operations.ts +38 -0
- package/src/utils/suite.test.ts +256 -0
- package/src/utils/suite.ts +144 -0
- package/src/utils/sync-operations.ts +87 -0
- package/src/utils/tmux-dashboard.ts +193 -0
- package/src/utils/validation.test.ts +139 -0
- package/src/utils/validation.ts +128 -0
- package/src/utils/version-check.test.ts +108 -0
- package/src/utils/version-check.ts +96 -0
- package/src/utils/workspace-meta.ts +173 -0
- package/sync-permissions.sh +137 -0
- package/tsconfig.json +20 -0
- package/uninstall.sh +162 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { spawn, exec, execFileSync } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import { WORKSPACES_DIR, getUserConfig } from '../utils/config';
|
|
7
|
+
import { logInfo, logError, logStep } from '../utils/logger';
|
|
8
|
+
import { colorize } from '../utils/colors';
|
|
9
|
+
import { getPrimaryAgent } from '../utils/agent-config';
|
|
10
|
+
import { sanitizeWorkspaceName, checkWorkspaceExists, resolveWorkspaceNameConflict } from '../utils/validation';
|
|
11
|
+
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
|
|
14
|
+
export const AI_PROMPT_FILE = path.join(os.homedir(), '.workspace-ai-prompt');
|
|
15
|
+
|
|
16
|
+
const EXTRACT_SCHEMA = JSON.stringify({
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
workspaceName: { type: 'string', description: 'Workspace name (kebab-case, e.g. "payments-team")' },
|
|
20
|
+
repos: {
|
|
21
|
+
type: 'array',
|
|
22
|
+
items: { type: 'string' },
|
|
23
|
+
description: 'Repository names to clone (e.g. ["api", "web"]). Empty array when the repos cannot be known without investigation first.',
|
|
24
|
+
},
|
|
25
|
+
investigateFirst: {
|
|
26
|
+
type: 'boolean',
|
|
27
|
+
description: 'True when the user does NOT name concrete repos and instead wants the agent to investigate first (e.g. "search the logs and figure out which repos") and add the relevant repos itself. When true, repos must be an empty array.',
|
|
28
|
+
},
|
|
29
|
+
remainingIntent: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'What the user wants done AFTER workspace creation (empty string if nothing beyond creating the workspace)',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ['workspaceName', 'repos', 'investigateFirst', 'remainingIntent'],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build the extraction prompt for parsing workspace details from natural language.
|
|
39
|
+
*/
|
|
40
|
+
export function buildExtractionPrompt(userPrompt: string): string {
|
|
41
|
+
return `Extract the workspace name and repository names from this request. If the user didn't specify a workspace name, generate a short descriptive kebab-case name. Repository names should be exact GitHub repo names (without org prefix). If the user's request includes things to do AFTER creating the workspace (like fixing code, creating branches, etc.), put that in remainingIntent.
|
|
42
|
+
|
|
43
|
+
IMPORTANT — investigate-first requests: if the user does NOT name concrete repositories and instead wants the agent to figure out which repos are relevant by investigating (e.g. "search the logs for X and open the repos involved", "look at this trace and pull the services' code", "find which repo owns this error"), then set "investigateFirst": true and "repos": [] (empty), and put the full investigation task in remainingIntent. Only list repos explicitly when the user actually names them or they're unambiguous.
|
|
44
|
+
|
|
45
|
+
Respond with ONLY a raw JSON object (no markdown, no backticks, no explanation). The JSON must have exactly these fields:
|
|
46
|
+
{"workspaceName": "short-kebab-name", "repos": ["repo-name-1", "repo-name-2"], "investigateFirst": false, "remainingIntent": "what to do after creation or empty string"}
|
|
47
|
+
|
|
48
|
+
User request: ${userPrompt}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build the in-session task for an investigate-first workspace: the workspace
|
|
53
|
+
* starts empty, and the agent must discover which repos are relevant and add
|
|
54
|
+
* them itself before reading any code.
|
|
55
|
+
*
|
|
56
|
+
* Deliberately GENERIC (works for log/trace searches, GitHub search, stack
|
|
57
|
+
* traces, etc.). The discovery step adapts to the active agent: MCP tools when
|
|
58
|
+
* available, otherwise the always-present `gh` CLI (honouring a configured org).
|
|
59
|
+
*/
|
|
60
|
+
export function buildInvestigationPreamble(
|
|
61
|
+
workspaceName: string,
|
|
62
|
+
task: string,
|
|
63
|
+
opts: { useMcpTools?: boolean; githubOrg?: string } = {}
|
|
64
|
+
): string {
|
|
65
|
+
const org = (opts.githubOrg || '').trim();
|
|
66
|
+
const ownerFlag = org ? ` --owner ${org}` : '';
|
|
67
|
+
const repoListCmd = org ? `gh repo list ${org} --limit 200` : `gh repo list --limit 200`;
|
|
68
|
+
// Discovery differs by agent capability: "search-repos"/"list-org-repos" exist
|
|
69
|
+
// ONLY as Nemus MCP tools (there are no `nemus search-repos` CLI commands).
|
|
70
|
+
// When the active agent can't use MCP (e.g. Pi) or MCP is disabled, point it
|
|
71
|
+
// at the always-available `gh` CLI so we never name tools it doesn't have.
|
|
72
|
+
const discoveryStep = opts.useMcpTools
|
|
73
|
+
? `3. Map each service to its GitHub repository using the Nemus MCP tools: try the "search-repos" tool (fuzzy) and "list-org-repos" to confirm the real repo name. Do not guess — verify the repo exists.`
|
|
74
|
+
: `3. Map each service to its GitHub repository using the \`gh\` CLI: \`gh search repos <name>${ownerFlag} --limit 20\` (or \`${repoListCmd}\`) to find and confirm the real repo name. Do not guess — verify the repo exists.`;
|
|
75
|
+
return [
|
|
76
|
+
`You are in a NEW, EMPTY workspace named "${workspaceName}" — it has no repositories cloned yet.`,
|
|
77
|
+
`Your job is to investigate first, then add the repositories you discover, and only then dig into the code.`,
|
|
78
|
+
``,
|
|
79
|
+
`Follow this workflow:`,
|
|
80
|
+
`1. Investigate the request below using whatever sources it points to (e.g. logs/traces, a stack trace, GitHub search). Use your available tools/skills.`,
|
|
81
|
+
`2. From the investigation, identify the SERVICE / component names involved (e.g. from a trace's spans). Strip environment prefixes/suffixes like "production-", "-prod", "staging-" to get the base service name.`,
|
|
82
|
+
discoveryStep,
|
|
83
|
+
`4. Add the repositories you identified to THIS workspace by running: \`nemus update --workspace ${workspaceName} --repos <repo1,repo2,...> --yes\`. This clones them into the workspace.`,
|
|
84
|
+
`5. Briefly tell me which repos you added and why (which service/evidence pointed to each).`,
|
|
85
|
+
`6. THEN read the relevant code in those repos to complete the task.`,
|
|
86
|
+
``,
|
|
87
|
+
`If the investigation points to no repos, say so instead of adding unrelated ones.`,
|
|
88
|
+
``,
|
|
89
|
+
`--- Task ---`,
|
|
90
|
+
task,
|
|
91
|
+
].join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if the primary agent CLI is available on the system.
|
|
96
|
+
*/
|
|
97
|
+
export async function isPrimaryAgentAvailable(): Promise<boolean> {
|
|
98
|
+
const agent = getPrimaryAgent();
|
|
99
|
+
try {
|
|
100
|
+
await execAsync(`which ${agent.launchCommand}`);
|
|
101
|
+
return true;
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Backward compat alias
|
|
108
|
+
export const isClaudeAvailable = isPrimaryAgentAvailable;
|
|
109
|
+
|
|
110
|
+
interface ExtractedIntent {
|
|
111
|
+
workspaceName: string;
|
|
112
|
+
repos: string[];
|
|
113
|
+
remainingIntent: string;
|
|
114
|
+
/** True when repos aren't known yet and the agent should investigate then add them itself. */
|
|
115
|
+
investigateFirst?: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Use the configured AI agent to extract structured workspace details from a natural language prompt.
|
|
120
|
+
*/
|
|
121
|
+
// Extraction is a tiny JSON task, but the underlying model call can stall
|
|
122
|
+
// intermittently — e.g. a slow/large default model (Opus) plus provider
|
|
123
|
+
// retries/backoff (Bedrock rate-limits), or a cold agent startup. 60s was
|
|
124
|
+
// too tight and produced flaky `spawnSync ETIMEDOUT`. 120s gives those
|
|
125
|
+
// transient cases room while still bounding a genuine hang.
|
|
126
|
+
const EXTRACTION_TIMEOUT_MS = 120_000;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Run an agent CLI for intent extraction, turning a timeout or non-zero exit
|
|
130
|
+
* into a clear, actionable error (instead of a cryptic `spawnSync ETIMEDOUT`).
|
|
131
|
+
*
|
|
132
|
+
* `fullArgs` is the preferred (lean) invocation. `fallbackArgs`, when provided,
|
|
133
|
+
* is a plainer invocation retried ONCE if the lean run fails fast (i.e. not a
|
|
134
|
+
* timeout or buffer overflow) — this covers older agent versions that reject
|
|
135
|
+
* newer speed flags like `--bare` / `--no-extensions` with an instant non-zero
|
|
136
|
+
* exit. Older versions then still work (just without the speedup).
|
|
137
|
+
*/
|
|
138
|
+
function runExtraction(cmd: string, fullArgs: string[], fallbackArgs?: string[]): string {
|
|
139
|
+
const exec = (args: string[]) =>
|
|
140
|
+
execFileSync(cmd, args, {
|
|
141
|
+
encoding: 'utf-8',
|
|
142
|
+
timeout: EXTRACTION_TIMEOUT_MS,
|
|
143
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const classify = (err: any) => {
|
|
147
|
+
const message = String(err?.message ?? '');
|
|
148
|
+
return {
|
|
149
|
+
timedOut: err?.code === 'ETIMEDOUT' || /ETIMEDOUT/.test(message),
|
|
150
|
+
bufferOverflow: err?.code === 'ENOBUFS' || /maxBuffer/i.test(message),
|
|
151
|
+
stderr: typeof err?.stderr === 'string' ? err.stderr.trim() : '',
|
|
152
|
+
stdout: typeof err?.stdout === 'string' ? err.stdout.trim() : '',
|
|
153
|
+
message,
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
return exec(fullArgs);
|
|
159
|
+
} catch (err: any) {
|
|
160
|
+
const c = classify(err);
|
|
161
|
+
|
|
162
|
+
// Fast failure (unsupported flag, etc.) on the lean invocation — retry
|
|
163
|
+
// once with the plainer args so older agent versions still work.
|
|
164
|
+
if (fallbackArgs && !c.timedOut && !c.bufferOverflow) {
|
|
165
|
+
try {
|
|
166
|
+
return exec(fallbackArgs);
|
|
167
|
+
} catch (err2: any) {
|
|
168
|
+
return throwExtractionError(cmd, classify(err2));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return throwExtractionError(cmd, c);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function throwExtractionError(
|
|
176
|
+
cmd: string,
|
|
177
|
+
c: { timedOut: boolean; bufferOverflow: boolean; stderr: string; stdout: string; message: string },
|
|
178
|
+
): never {
|
|
179
|
+
const testCmd = cmd === 'opencode'
|
|
180
|
+
? `opencode run "reply with OK"`
|
|
181
|
+
: `${cmd} -p "reply with OK"`;
|
|
182
|
+
// claude -p --output-format json emits error details on stdout, not stderr.
|
|
183
|
+
const detail = c.stderr || c.stdout;
|
|
184
|
+
|
|
185
|
+
if (c.timedOut) {
|
|
186
|
+
const secs = Math.round(EXTRACTION_TIMEOUT_MS / 1000);
|
|
187
|
+
let msg =
|
|
188
|
+
`${cmd} did not respond within ${secs}s while parsing your request.\n` +
|
|
189
|
+
` This is usually a transient model/provider stall, not a workspace-manager bug.\n` +
|
|
190
|
+
` • Verify the agent itself responds quickly: time ${testCmd}\n` +
|
|
191
|
+
` • If it's slow, your default model may be heavy (e.g. Opus) or the provider\n` +
|
|
192
|
+
` may be rate-limiting/refreshing credentials. Retry, or switch to a faster model.\n` +
|
|
193
|
+
` • You can still create the workspace manually: nemus create --workspace <name> --repos <repos>`;
|
|
194
|
+
if (detail) msg += `\n agent output: ${detail.slice(0, 500)}`;
|
|
195
|
+
throw new Error(msg);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (c.bufferOverflow) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`${cmd} produced more output than expected while parsing your request ` +
|
|
201
|
+
`(maxBuffer exceeded). Try a shorter request, or create the workspace ` +
|
|
202
|
+
`manually: nemus create --workspace <name> --repos <repos>` +
|
|
203
|
+
(detail ? `\n agent output: ${detail.slice(0, 500)}` : ''),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
throw new Error(
|
|
208
|
+
`${cmd} failed while parsing your request.\n` +
|
|
209
|
+
` • Verify the agent works: ${testCmd}\n` +
|
|
210
|
+
` • Or create the workspace manually: nemus create --workspace <name> --repos <repos>` +
|
|
211
|
+
(detail ? `\n agent output: ${detail.slice(0, 800)}` : ` (${c.message || 'unknown error'})`),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function extractIntent(prompt: string): Promise<ExtractedIntent> {
|
|
216
|
+
const extractionPrompt = buildExtractionPrompt(prompt);
|
|
217
|
+
const agent = getPrimaryAgent();
|
|
218
|
+
|
|
219
|
+
let result: string;
|
|
220
|
+
if (agent.type === 'claude') {
|
|
221
|
+
// Preferred: structured + lean. --output-format/--json-schema give clean
|
|
222
|
+
// structured output; --bare/--strict-mcp-config/--disable-slash-commands
|
|
223
|
+
// skip the heavy ~/.claude (hooks, MCP servers, skills, plugins, context)
|
|
224
|
+
// so a bloated setup can't time out.
|
|
225
|
+
const preferred = [
|
|
226
|
+
'-p', extractionPrompt,
|
|
227
|
+
'--output-format', 'json',
|
|
228
|
+
'--json-schema', EXTRACT_SCHEMA,
|
|
229
|
+
'--bare',
|
|
230
|
+
'--strict-mcp-config',
|
|
231
|
+
'--disable-slash-commands',
|
|
232
|
+
];
|
|
233
|
+
// Fallback: the most basic invocation that works on ANY claude version.
|
|
234
|
+
// If the user's claude rejects ANY of the flags above (it exits instantly
|
|
235
|
+
// non-zero), retry with plain `-p <prompt>` and parse the raw JSON text
|
|
236
|
+
// (same approach as pi). The extraction prompt already asks for raw JSON.
|
|
237
|
+
const fallback = ['-p', extractionPrompt];
|
|
238
|
+
result = runExtraction('claude', preferred, fallback);
|
|
239
|
+
} else if (agent.type === 'opencode') {
|
|
240
|
+
// OpenCode: use 'run' subcommand with positional message argument
|
|
241
|
+
result = runExtraction('opencode', ['run', extractionPrompt]);
|
|
242
|
+
} else {
|
|
243
|
+
// Pi: run as LEAN as possible. Intent extraction is a pure text->JSON
|
|
244
|
+
// transform that needs no extensions, skills, prompt-templates, context
|
|
245
|
+
// files, tools, or saved session. Loading the user's full environment
|
|
246
|
+
// (especially many MCP tools/skills or a large AGENTS.md context) is what
|
|
247
|
+
// made `pi -p` slow enough to blow past the timeout on bloated setups.
|
|
248
|
+
// These flags also avoid spawning MCP servers / credential-refresh
|
|
249
|
+
// extensions that can hang a non-interactive subprocess.
|
|
250
|
+
const piCore = ['-p', extractionPrompt];
|
|
251
|
+
const piLean = [
|
|
252
|
+
'--no-extensions',
|
|
253
|
+
'--no-skills',
|
|
254
|
+
'--no-prompt-templates',
|
|
255
|
+
'--no-context-files',
|
|
256
|
+
'--no-tools',
|
|
257
|
+
'--no-session',
|
|
258
|
+
];
|
|
259
|
+
result = runExtraction('pi', [...piLean, ...piCore], piCore);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Strip markdown code fences if present (Pi may wrap JSON in ```json...```)
|
|
263
|
+
let jsonStr = result.trim();
|
|
264
|
+
const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
265
|
+
if (fenceMatch) {
|
|
266
|
+
jsonStr = fenceMatch[1].trim();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const parsed = JSON.parse(jsonStr);
|
|
270
|
+
|
|
271
|
+
// Handle different output formats:
|
|
272
|
+
// - Claude: { structured_output: {...} } or { result: "..." }
|
|
273
|
+
// - Pi: may return the object directly or wrap it
|
|
274
|
+
let intent: ExtractedIntent | undefined;
|
|
275
|
+
if (parsed.structured_output) {
|
|
276
|
+
intent = parsed.structured_output;
|
|
277
|
+
} else if (typeof parsed.result === 'string' && parsed.result) {
|
|
278
|
+
try { intent = JSON.parse(parsed.result); } catch { /* ignore */ }
|
|
279
|
+
} else if (typeof parsed.result === 'object' && parsed.result !== null) {
|
|
280
|
+
intent = parsed.result;
|
|
281
|
+
} else if (parsed.workspaceName || parsed.repos || parsed.remainingIntent !== undefined) {
|
|
282
|
+
// Pi may return the extracted object directly
|
|
283
|
+
intent = parsed;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!intent) {
|
|
287
|
+
throw new Error(`Could not extract intent from agent response. Parsed: ${JSON.stringify(parsed).slice(0, 200)}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Coerce/validate field types so malformed model output can't crash the
|
|
291
|
+
// downstream .trim()/sanitize path (a stringly-typed workspaceName or a
|
|
292
|
+
// non-array repos would otherwise throw). Non-conforming values are dropped
|
|
293
|
+
// to their safe empty form, so run()'s existing "could not determine" guard
|
|
294
|
+
// handles them cleanly instead of an unhandled exception.
|
|
295
|
+
const normalized: ExtractedIntent = {
|
|
296
|
+
workspaceName: typeof intent.workspaceName === 'string' ? intent.workspaceName : '',
|
|
297
|
+
repos: Array.isArray(intent.repos)
|
|
298
|
+
? intent.repos.filter((r): r is string => typeof r === 'string')
|
|
299
|
+
: [],
|
|
300
|
+
remainingIntent: typeof intent.remainingIntent === 'string' ? intent.remainingIntent : '',
|
|
301
|
+
investigateFirst: intent.investigateFirst === true,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
return normalized;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Run the AI prompt command.
|
|
309
|
+
*
|
|
310
|
+
* 1. Extract workspace name + repos from the prompt (fast, no tool use).
|
|
311
|
+
* 2. Create the workspace using the CLI directly (shows progress).
|
|
312
|
+
* 3. Shell integration CDs to workspace and launches interactive Claude.
|
|
313
|
+
*/
|
|
314
|
+
export async function run(prompt: string): Promise<number> {
|
|
315
|
+
const agentAvailable = await isClaudeAvailable();
|
|
316
|
+
if (!agentAvailable) {
|
|
317
|
+
const agent = getPrimaryAgent();
|
|
318
|
+
logError(`${agent.type} CLI not found. Install it first.`);
|
|
319
|
+
return 1;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const displayPrompt = prompt.length > 80 ? prompt.slice(0, 77) + '...' : prompt;
|
|
323
|
+
logInfo(`AI prompt: ${colorize(displayPrompt, 'cyan')}`);
|
|
324
|
+
|
|
325
|
+
fs.mkdirSync(WORKSPACES_DIR, { recursive: true });
|
|
326
|
+
|
|
327
|
+
// Step 1: Extract workspace details from natural language
|
|
328
|
+
logStep(1, 2, 'Understanding request...');
|
|
329
|
+
let intent: ExtractedIntent;
|
|
330
|
+
try {
|
|
331
|
+
intent = await extractIntent(prompt);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
logError('Failed to parse workspace request');
|
|
334
|
+
if (err instanceof Error) logError(err.message);
|
|
335
|
+
return 1;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const repos = Array.isArray(intent.repos) ? intent.repos : [];
|
|
339
|
+
// Investigate-first: no concrete repos yet — create an empty workspace and let
|
|
340
|
+
// the in-session agent discover + add the repos. This only applies when the
|
|
341
|
+
// model produced NO repos: if it already identified some, we must clone them
|
|
342
|
+
// (never silently discard them for an empty workspace), even if it also set
|
|
343
|
+
// the investigateFirst flag. An empty repo list with a real task is treated
|
|
344
|
+
// as investigate-mode too, even if the model forgot the flag.
|
|
345
|
+
const investigateFirst = repos.length === 0
|
|
346
|
+
&& (intent.investigateFirst === true || !!(intent.remainingIntent || '').trim());
|
|
347
|
+
|
|
348
|
+
if (!intent.workspaceName || (repos.length === 0 && !investigateFirst)) {
|
|
349
|
+
logError('Could not determine workspace name or repos from prompt');
|
|
350
|
+
logInfo(`Parsed: name=${intent.workspaceName || '(none)'}, repos=${repos.join(', ') || '(none)'}`);
|
|
351
|
+
return 1;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// In investigate-first mode the preamble tells the agent to run
|
|
355
|
+
// `nemus update --workspace <name> ...`, so that name MUST match the workspace
|
|
356
|
+
// `nemus create` actually produces. Pre-resolve it here (same sanitize +
|
|
357
|
+
// conflict resolution create uses) and pass the SAME resolved name to both
|
|
358
|
+
// the preamble and create, so a sanitized/de-duplicated name can't drift.
|
|
359
|
+
let workspaceName = intent.workspaceName;
|
|
360
|
+
if (investigateFirst) {
|
|
361
|
+
workspaceName = sanitizeWorkspaceName(intent.workspaceName);
|
|
362
|
+
if (await checkWorkspaceExists(workspaceName)) {
|
|
363
|
+
workspaceName = await resolveWorkspaceNameConflict(workspaceName, []);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (investigateFirst) {
|
|
368
|
+
logInfo(`Workspace: ${colorize(workspaceName, 'cyan')} ${colorize('(investigate-first — no repos yet)', 'gray')}`);
|
|
369
|
+
} else {
|
|
370
|
+
logInfo(`Workspace: ${colorize(workspaceName, 'cyan')}, Repos: ${colorize(repos.join(', '), 'cyan')}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Save the task for the follow-up interactive session. In investigate-first
|
|
374
|
+
// mode, wrap it with a workflow preamble so the agent discovers repos and
|
|
375
|
+
// adds them itself before reading code.
|
|
376
|
+
const baseTask = intent.remainingIntent || prompt;
|
|
377
|
+
// The "search-repos"/"list-org-repos" discovery helpers are MCP-only, so the
|
|
378
|
+
// preamble may reference them only when the active agent supports MCP AND MCP
|
|
379
|
+
// is enabled; otherwise it falls back to the `gh` CLI.
|
|
380
|
+
const cfg = getUserConfig();
|
|
381
|
+
const useMcpTools = getPrimaryAgent().supportsMcp && cfg.installMcp;
|
|
382
|
+
const remaining = investigateFirst
|
|
383
|
+
? buildInvestigationPreamble(workspaceName, baseTask, { useMcpTools, githubOrg: cfg.githubOrg })
|
|
384
|
+
: baseTask;
|
|
385
|
+
try { fs.writeFileSync(AI_PROMPT_FILE, remaining, 'utf-8'); } catch {}
|
|
386
|
+
|
|
387
|
+
// Step 2: Create workspace using the CLI directly
|
|
388
|
+
logStep(2, 2, 'Creating workspace...');
|
|
389
|
+
const createArgs = investigateFirst
|
|
390
|
+
? ['create', '--workspace', workspaceName, '--allow-empty', '--prompt', prompt, '--yes']
|
|
391
|
+
: ['create', '--workspace', workspaceName, '--repos', repos.join(','), '--prompt', prompt, '--yes'];
|
|
392
|
+
return new Promise<number>((resolve) => {
|
|
393
|
+
const child = spawn('nemus', createArgs, {
|
|
394
|
+
stdio: 'inherit',
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
child.on('error', (err) => {
|
|
398
|
+
logError(`Failed to create workspace: ${err.message}`);
|
|
399
|
+
resolve(1);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
child.on('exit', (code) => {
|
|
403
|
+
resolve(code || 0);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export async function main(): Promise<void> {
|
|
409
|
+
const prompt = process.argv.slice(2).join(' ').trim();
|
|
410
|
+
|
|
411
|
+
if (!prompt) {
|
|
412
|
+
logError('No prompt provided');
|
|
413
|
+
console.log(`\n Usage: ${colorize('nemus -- <prompt>', 'green')}`);
|
|
414
|
+
console.log(` Example: ${colorize('nemus -- create a workspace for the payments team', 'gray')}\n`);
|
|
415
|
+
process.exit(1);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const exitCode = await run(prompt);
|
|
420
|
+
process.exit(exitCode);
|
|
421
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { AgentState, InkComponents } from './types';
|
|
3
|
+
import { StatusBadge } from './StatusBadge';
|
|
4
|
+
|
|
5
|
+
interface AgentListProps extends InkComponents {
|
|
6
|
+
agents: AgentState[];
|
|
7
|
+
selectedIndex: number;
|
|
8
|
+
onSelect: (index: number) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const AgentList: React.FC<AgentListProps> = ({ agents, selectedIndex, onSelect, Box, Text }) => {
|
|
12
|
+
if (agents.length === 0) {
|
|
13
|
+
return (
|
|
14
|
+
<Box flexDirection="column">
|
|
15
|
+
<Text dimColor>No agents running</Text>
|
|
16
|
+
<Text dimColor>Press n to launch</Text>
|
|
17
|
+
</Box>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Box flexDirection="column">
|
|
23
|
+
{agents.map((agent, i) => {
|
|
24
|
+
const isSelected = i === selectedIndex;
|
|
25
|
+
const ws = agent.workspace.length > 13
|
|
26
|
+
? agent.workspace.slice(0, 12) + '…'
|
|
27
|
+
: agent.workspace;
|
|
28
|
+
const num = `${i + 1}`;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Box key={agent.sessionId} gap={1}>
|
|
32
|
+
{isSelected ? (
|
|
33
|
+
<Text backgroundColor="blue" color="white" bold>▸{num}</Text>
|
|
34
|
+
) : (
|
|
35
|
+
<Text dimColor> {num}</Text>
|
|
36
|
+
)}
|
|
37
|
+
<Text color="cyan" bold={isSelected}>{ws.padEnd(13)}</Text>
|
|
38
|
+
<StatusBadge status={agent.status} Box={Box} Text={Text} />
|
|
39
|
+
</Box>
|
|
40
|
+
);
|
|
41
|
+
})}
|
|
42
|
+
</Box>
|
|
43
|
+
);
|
|
44
|
+
};
|