rovecode 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +662 -0
- package/README.md +737 -0
- package/THIRD_PARTY_NOTICES.md +268 -0
- package/bin/rovecode.ts +21 -0
- package/package.json +56 -0
- package/src/acp/server.ts +374 -0
- package/src/cli/auth-login.ts +122 -0
- package/src/cli/connect.ts +244 -0
- package/src/cli/context-cmd.ts +199 -0
- package/src/cli/dispatch.ts +82 -0
- package/src/cli/doctor.ts +362 -0
- package/src/cli/export.ts +276 -0
- package/src/cli/help.ts +293 -0
- package/src/cli/is-tui-invocation.ts +8 -0
- package/src/cli/main.ts +583 -0
- package/src/cli/market-cmd.ts +658 -0
- package/src/cli/mcp-login.ts +141 -0
- package/src/cli/mcp-market-cmd.ts +302 -0
- package/src/cli/output.ts +382 -0
- package/src/cli/repl.ts +250 -0
- package/src/cli/repomap-root.ts +14 -0
- package/src/cli/resume.ts +57 -0
- package/src/cli/run-flags.ts +43 -0
- package/src/cli/run-limits.ts +78 -0
- package/src/cli/runtime.ts +931 -0
- package/src/cli/session-arg.ts +30 -0
- package/src/cli/sessions-cmd.ts +145 -0
- package/src/cli/setup.ts +153 -0
- package/src/cli/skills-cmd.ts +194 -0
- package/src/cli/start-chat.ts +65 -0
- package/src/cli/trust-cmd.ts +52 -0
- package/src/coding/bash.ts +148 -0
- package/src/coding/checkpoints.ts +327 -0
- package/src/coding/diff.ts +138 -0
- package/src/coding/files.ts +341 -0
- package/src/coding/hashline.ts +274 -0
- package/src/coding/lsp-gate.ts +254 -0
- package/src/coding/lsp-servers.ts +147 -0
- package/src/coding/lsp.ts +283 -0
- package/src/coding/repomap-cache.ts +99 -0
- package/src/coding/repomap-files.ts +192 -0
- package/src/coding/repomap.ts +481 -0
- package/src/core/agents.ts +255 -0
- package/src/core/compaction.ts +259 -0
- package/src/core/config.ts +289 -0
- package/src/core/context-report.ts +228 -0
- package/src/core/context.ts +60 -0
- package/src/core/count-remote.ts +107 -0
- package/src/core/execpolicy-rules.ts +196 -0
- package/src/core/execpolicy.ts +385 -0
- package/src/core/executor.ts +454 -0
- package/src/core/guardrails.ts +400 -0
- package/src/core/hooks.ts +411 -0
- package/src/core/images.ts +230 -0
- package/src/core/intro.ts +266 -0
- package/src/core/loop.ts +567 -0
- package/src/core/modes.ts +372 -0
- package/src/core/orchestrator.ts +245 -0
- package/src/core/proc-group.ts +48 -0
- package/src/core/project-trust.ts +98 -0
- package/src/core/reflection.ts +165 -0
- package/src/core/sandbox-config.ts +186 -0
- package/src/core/session-id.ts +24 -0
- package/src/core/session-images.ts +73 -0
- package/src/core/session-ops.ts +183 -0
- package/src/core/session-text.ts +29 -0
- package/src/core/session.ts +469 -0
- package/src/core/settings.ts +170 -0
- package/src/core/tasks.ts +646 -0
- package/src/core/token-scale.ts +108 -0
- package/src/core/tools.ts +309 -0
- package/src/core/trust.ts +104 -0
- package/src/core/types.ts +330 -0
- package/src/core/update-check.ts +171 -0
- package/src/core/usage.ts +204 -0
- package/src/core/validate.ts +121 -0
- package/src/core/verify-gate.ts +159 -0
- package/src/core/verify.ts +236 -0
- package/src/core/voice.ts +158 -0
- package/src/core/win-job.ts +183 -0
- package/src/core/workspace.ts +184 -0
- package/src/design/audit.ts +797 -0
- package/src/design/direction.ts +190 -0
- package/src/design/rules.ts +157 -0
- package/src/eval/bench.ts +150 -0
- package/src/eval/gauntlet-runner.ts +215 -0
- package/src/eval/gauntlet-support.ts +84 -0
- package/src/eval/gauntlet-wave3.ts +269 -0
- package/src/eval/gauntlet-wave4.ts +217 -0
- package/src/eval/gauntlet.ts +253 -0
- package/src/index.ts +17 -0
- package/src/lanes/agy.ts +95 -0
- package/src/lanes/approval.ts +24 -0
- package/src/lanes/claude.ts +129 -0
- package/src/lanes/codex.ts +127 -0
- package/src/lanes/events.ts +130 -0
- package/src/lanes/job.ts +142 -0
- package/src/lanes/opencode.ts +122 -0
- package/src/lanes/process.ts +184 -0
- package/src/lanes/progress.ts +183 -0
- package/src/lanes/registry.ts +178 -0
- package/src/lanes/runner.ts +124 -0
- package/src/lanes/types.ts +112 -0
- package/src/market/catalogs/mcp-docs.json +111 -0
- package/src/market/catalogs/plugins.json +111 -0
- package/src/market/catalogs/skills.json +478 -0
- package/src/market/clone.ts +72 -0
- package/src/market/context-cost.ts +121 -0
- package/src/market/digest.ts +106 -0
- package/src/market/index.ts +22 -0
- package/src/market/install.ts +578 -0
- package/src/market/manifest.ts +187 -0
- package/src/market/prereq.ts +145 -0
- package/src/market/registry.ts +363 -0
- package/src/market/resolve.ts +111 -0
- package/src/market/types.ts +236 -0
- package/src/market/validate.ts +227 -0
- package/src/mcp/client.ts +449 -0
- package/src/mcp/config.ts +252 -0
- package/src/mcp/local-package.ts +211 -0
- package/src/mcp/market-catalog.ts +84 -0
- package/src/mcp/market-install.ts +289 -0
- package/src/mcp/market.ts +362 -0
- package/src/mcp/oauth.ts +251 -0
- package/src/mcp/prompts-resources.ts +249 -0
- package/src/mcp/shared.ts +149 -0
- package/src/mcp/status.ts +67 -0
- package/src/mcp/tools.ts +275 -0
- package/src/mcp/transport.ts +122 -0
- package/src/mcp/trust.ts +25 -0
- package/src/memory/blocks.ts +278 -0
- package/src/memory/recall.ts +355 -0
- package/src/memory/scope.ts +182 -0
- package/src/memory/store.ts +105 -0
- package/src/memory/tools.ts +99 -0
- package/src/plugins/cli.ts +119 -0
- package/src/plugins/discover.ts +108 -0
- package/src/plugins/index.ts +50 -0
- package/src/plugins/install.ts +184 -0
- package/src/plugins/load.ts +124 -0
- package/src/plugins/manifest.ts +92 -0
- package/src/plugins/state.ts +83 -0
- package/src/providers/auth.ts +408 -0
- package/src/providers/cache.ts +223 -0
- package/src/providers/catalog-local.ts +160 -0
- package/src/providers/catalog.ts +421 -0
- package/src/providers/middleware-context.ts +86 -0
- package/src/providers/middleware.ts +373 -0
- package/src/providers/model-list.ts +23 -0
- package/src/providers/models-index.json +1 -0
- package/src/providers/oauth/common.ts +105 -0
- package/src/providers/oauth/device-code.ts +107 -0
- package/src/providers/oauth/github-copilot.ts +146 -0
- package/src/providers/oauth/loopback.ts +158 -0
- package/src/providers/oauth/openai.ts +163 -0
- package/src/providers/oauth/openrouter.ts +89 -0
- package/src/providers/oauth/pkce.ts +45 -0
- package/src/providers/oauth/registry.ts +39 -0
- package/src/providers/oauth/seam.ts +89 -0
- package/src/providers/profile-glm53.ts +111 -0
- package/src/providers/profile-sonnet5-persona.ts +65 -0
- package/src/providers/profile-sonnet5-voice.ts +23 -0
- package/src/providers/profiles.ts +156 -0
- package/src/providers/provider-config.ts +311 -0
- package/src/providers/registry.ts +333 -0
- package/src/providers/responses.ts +209 -0
- package/src/providers/retry.ts +234 -0
- package/src/providers/router.ts +294 -0
- package/src/providers/sse.ts +26 -0
- package/src/providers/stream-errors.ts +117 -0
- package/src/providers/stream.ts +566 -0
- package/src/providers/thinking.ts +189 -0
- package/src/providers/wire-messages.ts +129 -0
- package/src/providers/wire-responses.ts +79 -0
- package/src/providers/wire-select.ts +53 -0
- package/src/server/http.ts +291 -0
- package/src/server/openapi.ts +246 -0
- package/src/sextant/card-hits.ts +102 -0
- package/src/sextant/card-keys.ts +55 -0
- package/src/sextant/context-source.ts +157 -0
- package/src/sextant/crew-cards.ts +350 -0
- package/src/sextant/draw-agents.ts +273 -0
- package/src/sextant/draw-code.ts +388 -0
- package/src/sextant/draw-context.ts +222 -0
- package/src/sextant/draw-frame.ts +164 -0
- package/src/sextant/draw-market.ts +573 -0
- package/src/sextant/draw-messages.ts +386 -0
- package/src/sextant/draw-pet.ts +230 -0
- package/src/sextant/draw-plan.ts +187 -0
- package/src/sextant/draw-tabs.ts +85 -0
- package/src/sextant/draw-util.ts +65 -0
- package/src/sextant/draw-wizard.ts +378 -0
- package/src/sextant/engine.ts +230 -0
- package/src/sextant/frame-hits.ts +25 -0
- package/src/sextant/frame.ts +101 -0
- package/src/sextant/git-status.ts +197 -0
- package/src/sextant/grid.ts +59 -0
- package/src/sextant/input.ts +119 -0
- package/src/sextant/keys.ts +521 -0
- package/src/sextant/layout.ts +86 -0
- package/src/sextant/local-commands.ts +169 -0
- package/src/sextant/market-source.ts +287 -0
- package/src/sextant/mentions.ts +200 -0
- package/src/sextant/message-hits.ts +26 -0
- package/src/sextant/model.ts +387 -0
- package/src/sextant/overlays.ts +456 -0
- package/src/sextant/panel-hits.ts +38 -0
- package/src/sextant/pet.ts +399 -0
- package/src/sextant/screen.ts +324 -0
- package/src/sextant/scroll-hits.ts +66 -0
- package/src/sextant/scrollbar.ts +82 -0
- package/src/sextant/sextant-bridge.ts +174 -0
- package/src/sextant/sextant-cards.ts +142 -0
- package/src/sextant/sextant-diff-base.ts +63 -0
- package/src/sextant/sextant-files.ts +154 -0
- package/src/sextant/sextant-frame-loop.ts +335 -0
- package/src/sextant/sextant-renderer.ts +574 -0
- package/src/sextant/sextant-repo.ts +140 -0
- package/src/sextant/theme.ts +66 -0
- package/src/sextant/tool-rows.ts +189 -0
- package/src/sextant/types.ts +493 -0
- package/src/skills/index.ts +387 -0
- package/src/skills/pack.ts +220 -0
- package/src/skills/spec.ts +162 -0
- package/src/skills/tools.ts +69 -0
- package/src/skills/versioned.ts +227 -0
- package/src/telemetry/otel-export.ts +122 -0
- package/src/telemetry/otel-lanes.ts +89 -0
- package/src/telemetry/otel-logs.ts +131 -0
- package/src/telemetry/otel-metrics.ts +136 -0
- package/src/telemetry/otel.ts +397 -0
- package/src/telemetry/otlp.ts +76 -0
- package/src/tools/ask-user.ts +156 -0
- package/src/tools/bash-bg.ts +94 -0
- package/src/tools/bash-jobs.ts +237 -0
- package/src/tools/design.ts +151 -0
- package/src/tools/evalcell.ts +338 -0
- package/src/tools/html-text.ts +139 -0
- package/src/tools/provider.ts +149 -0
- package/src/tools/task.ts +250 -0
- package/src/tools/todo.ts +320 -0
- package/src/tools/webfetch.ts +332 -0
- package/src/tools/websearch.ts +359 -0
- package/src/tui/agents-cmd.ts +41 -0
- package/src/tui/app.ts +749 -0
- package/src/tui/attach.ts +127 -0
- package/src/tui/boot-notes.ts +41 -0
- package/src/tui/builtin-prompts.ts +59 -0
- package/src/tui/checkpoints-cmd.ts +70 -0
- package/src/tui/clipboard-image.ts +81 -0
- package/src/tui/clipboard.ts +78 -0
- package/src/tui/commands.ts +283 -0
- package/src/tui/config-view.ts +53 -0
- package/src/tui/context-cmds.ts +282 -0
- package/src/tui/cost.ts +108 -0
- package/src/tui/crash-guard.ts +173 -0
- package/src/tui/focus-terminal.ts +34 -0
- package/src/tui/git-cmds.ts +273 -0
- package/src/tui/git-plain.ts +58 -0
- package/src/tui/info-cmd.ts +150 -0
- package/src/tui/input-plain.ts +76 -0
- package/src/tui/mcp-cmd.ts +128 -0
- package/src/tui/memory-note.ts +77 -0
- package/src/tui/modes-cmd.ts +45 -0
- package/src/tui/notify-seq.ts +100 -0
- package/src/tui/notify.ts +318 -0
- package/src/tui/overlays.ts +97 -0
- package/src/tui/pi-renderer.ts +428 -0
- package/src/tui/providers-cmd.ts +377 -0
- package/src/tui/reasoning-view.ts +56 -0
- package/src/tui/renderer.ts +128 -0
- package/src/tui/replay-marker.ts +29 -0
- package/src/tui/session-cmd.ts +148 -0
- package/src/tui/session-manage.ts +95 -0
- package/src/tui/sextant-attach.ts +102 -0
- package/src/tui/sextant-io.ts +202 -0
- package/src/tui/sextant-smoke.ts +110 -0
- package/src/tui/shell-cmd.ts +158 -0
- package/src/tui/smoke.ts +72 -0
- package/src/tui/staged-terminal.ts +50 -0
- package/src/tui/startup.ts +12 -0
- package/src/tui/theme.ts +59 -0
- package/src/tui/todo-label.ts +7 -0
- package/src/tui/trust-card.ts +107 -0
- package/src/tui/tui-commands.ts +87 -0
- package/tsconfig.json +30 -0
- package/vendor/pi-tui/LICENSE +21 -0
- package/vendor/pi-tui/PATCHES.md +12 -0
- package/vendor/pi-tui/PROVENANCE.md +12 -0
- package/vendor/pi-tui/README.upstream.md +854 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
- package/vendor/pi-tui/src/autocomplete.ts +827 -0
- package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
- package/vendor/pi-tui/src/components/box.ts +138 -0
- package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
- package/vendor/pi-tui/src/components/editor.ts +2364 -0
- package/vendor/pi-tui/src/components/h-stack.ts +45 -0
- package/vendor/pi-tui/src/components/image.ts +128 -0
- package/vendor/pi-tui/src/components/input.ts +448 -0
- package/vendor/pi-tui/src/components/loader.ts +93 -0
- package/vendor/pi-tui/src/components/markdown.ts +1016 -0
- package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
- package/vendor/pi-tui/src/components/select-list.ts +230 -0
- package/vendor/pi-tui/src/components/settings-list.ts +277 -0
- package/vendor/pi-tui/src/components/spacer.ts +29 -0
- package/vendor/pi-tui/src/components/stack.ts +155 -0
- package/vendor/pi-tui/src/components/text.ts +108 -0
- package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
- package/vendor/pi-tui/src/components/v-stack.ts +34 -0
- package/vendor/pi-tui/src/editor-component.ts +75 -0
- package/vendor/pi-tui/src/fuzzy.ts +138 -0
- package/vendor/pi-tui/src/index.ts +149 -0
- package/vendor/pi-tui/src/keybindings.ts +321 -0
- package/vendor/pi-tui/src/keys.ts +1402 -0
- package/vendor/pi-tui/src/kill-ring.ts +47 -0
- package/vendor/pi-tui/src/latex.ts +1381 -0
- package/vendor/pi-tui/src/layout-node.ts +52 -0
- package/vendor/pi-tui/src/layout.ts +411 -0
- package/vendor/pi-tui/src/native-modifiers.ts +60 -0
- package/vendor/pi-tui/src/native-module-path.ts +32 -0
- package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
- package/vendor/pi-tui/src/terminal-colors.ts +74 -0
- package/vendor/pi-tui/src/terminal-image.ts +701 -0
- package/vendor/pi-tui/src/terminal.ts +554 -0
- package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
- package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
- package/vendor/pi-tui/src/tui.ts +1264 -0
- package/vendor/pi-tui/src/undo-stack.ts +29 -0
- package/vendor/pi-tui/src/utils.ts +1327 -0
- package/vendor/pi-tui/src/word-navigation.ts +118 -0
- package/vendor/pi-tui/test/test-themes.ts +39 -0
- package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** A session id typed on the command line (aion hygiene h2, brought over 2026-09-07): the CLI's ONE gate for the entry
|
|
2
|
+
* points that used to take an id straight into a path join — `rovecode --resume <id|prefix>` (main.ts hand-parses the
|
|
3
|
+
* value) and `rovecode trace <id|prefix>`. Both resolve here through core/session-ops.ts resolveSession: validity
|
|
4
|
+
* first (core/session-id.ts), then an exact id or a UNIQUE prefix among the sessions under the root; a miss or an
|
|
5
|
+
* ambiguity is the usage error, never a fresh session dir under the typed name (which is what `--resume typo` used to
|
|
6
|
+
* create, silently). A bad id is exit 2 with ONE stderr line; `fail` is injectable for tests. */
|
|
7
|
+
|
|
8
|
+
import { describeBadSessionId } from "../core/session-id.ts";
|
|
9
|
+
import { resolveSession } from "../core/session-ops.ts";
|
|
10
|
+
|
|
11
|
+
const usageExit = (msg: string): never => {
|
|
12
|
+
process.stderr.write(`error: ${msg}\n`); // ONE stderr line
|
|
13
|
+
return process.exit(2);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** `<what> <id>`: the id, or the usage error — missing, empty, `.`/`..`, a separator (core/session-id.ts). */
|
|
17
|
+
export function sessionIdArg(what: string, id: string | undefined, fail: (msg: string) => never = usageExit): string {
|
|
18
|
+
if (id === undefined) return fail(`${what} needs a session id or prefix`);
|
|
19
|
+
const bad = describeBadSessionId(id);
|
|
20
|
+
return bad === undefined ? id : fail(`${what}: ${bad}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Validity (sessionIdArg) + resolution against the sessions under `root`: an exact id wins, else exactly ONE prefix
|
|
24
|
+
* match; several or none is the usage error naming them (never the first hit, never a new directory). */
|
|
25
|
+
export function resolveSessionArg(what: string, root: string, id: string | undefined, fail: (msg: string) => never = usageExit): string {
|
|
26
|
+
const wanted = sessionIdArg(what, id, fail);
|
|
27
|
+
const r = resolveSession(root, wanted);
|
|
28
|
+
if (r.ok) return r.id;
|
|
29
|
+
return fail(r.error.startsWith("no session matching") ? `${what}: ${r.error} in ${root} (rovecode sessions lists them)` : `${what}: ${r.error}`);
|
|
30
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** `rovecode sessions [--json]` (aion port #59, brought over 2026-09-07): the headless view of the TUI's /sessions picker
|
|
2
|
+
* over the ONE scanner (core/session.ts scanSessions — no second directory walk). Rows carry id, created, updated, cwd
|
|
3
|
+
* (the workspace whose sessions were listed — sessions are per project dir), the first prompt and the user-turn
|
|
4
|
+
* count; newest (updatedAt) first. `--json` prints the row array; the text table prints the cwd once in its header
|
|
5
|
+
* and, when there are any, ONE footer line counting the hollow directories (a meta.json, nothing ever written) from
|
|
6
|
+
* the stat alone — they are not rows, and whether to delete them is Berkay's open question; the count keeps it
|
|
7
|
+
* askable. Exit 0, also for an empty list.
|
|
8
|
+
* Port #84 — four verbs over core/session-ops.ts: `rename <id|prefix> <title…>`, `delete <id|prefix> [--yes]`,
|
|
9
|
+
* `fork <id|prefix> [--json]`, `search <terms…> [--json]`. Rows and the table show a set `title` (`--json` rows carry
|
|
10
|
+
* the key ONLY when set). Ids resolve by the /resume rule BEFORE any write; a usage error (unknown verb, missing id,
|
|
11
|
+
* bad / ambiguous / unknown id, empty title or query, an oversize fork) is exit 2 + ONE stderr line, nothing removed
|
|
12
|
+
* or created. Nothing boots a runtime or provider. `--yes` (codex's DeleteConfirmation Skip, pattern only) skips the
|
|
13
|
+
* y/N confirmation; a non-TTY stdin without it is refused (exit 2) — the TTY prompt itself (Bun's synchronous
|
|
14
|
+
* `confirm`, default N) is not hermetically tested. `--json` / `--yes` are booleans: no dispatch.ts VALUE_FLAGS entry.
|
|
15
|
+
* aion's `--cwd <dir>` is not brought: rovecode commands act on the cwd. */
|
|
16
|
+
|
|
17
|
+
import { scanSessions } from "../core/session.ts";
|
|
18
|
+
import { deleteSession, forkSession, oneLineTitle, renameSession, resolveSession, searchSessions, sessionsRoot, type SearchRow } from "../core/session-ops.ts";
|
|
19
|
+
|
|
20
|
+
export interface SessionRow {
|
|
21
|
+
id: string;
|
|
22
|
+
/** ISO 8601 */
|
|
23
|
+
created: string;
|
|
24
|
+
updated: string;
|
|
25
|
+
cwd: string;
|
|
26
|
+
/** first user-message text, single line, ≤80 chars ("" for an empty session) */
|
|
27
|
+
firstPrompt: string;
|
|
28
|
+
/** user messages recorded in the session file (every branch) */
|
|
29
|
+
turns: number;
|
|
30
|
+
/** all entries (messages + event markers) */
|
|
31
|
+
entries: number;
|
|
32
|
+
/** the user-given title — the key is present ONLY when one is set */
|
|
33
|
+
title?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SessionListing { rows: SessionRow[]; hollow: number; root: string }
|
|
37
|
+
|
|
38
|
+
/** the rows (sessions that hold something) plus the hollow count, from one scan */
|
|
39
|
+
export function sessionListing(cwd: string): SessionListing {
|
|
40
|
+
const root = sessionsRoot(cwd);
|
|
41
|
+
const scan = scanSessions(root);
|
|
42
|
+
const rows = scan.sessions.map((s) => ({
|
|
43
|
+
id: s.id, created: new Date(s.createdAt).toISOString(), updated: new Date(s.updatedAt).toISOString(),
|
|
44
|
+
cwd, firstPrompt: s.preview, turns: s.turns, entries: s.entryCount,
|
|
45
|
+
...(s.title !== undefined ? { title: s.title } : {}),
|
|
46
|
+
}));
|
|
47
|
+
return { rows, hollow: scan.hollow.length, root };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function sessionRows(cwd: string): SessionRow[] {
|
|
51
|
+
return sessionListing(cwd).rows;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** the one footer line for the hollow directories, or undefined when there are none */
|
|
55
|
+
export function hollowLine(count: number, root: string): string | undefined {
|
|
56
|
+
if (count === 0) return undefined;
|
|
57
|
+
return `${count} empty session director${count === 1 ? "y" : "ies"} in ${root} — nothing was ever written to them`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatSessions(listing: SessionListing, json: boolean): string {
|
|
61
|
+
const { rows, hollow, root } = listing;
|
|
62
|
+
if (json) return JSON.stringify(rows);
|
|
63
|
+
const footer = hollowLine(hollow, root);
|
|
64
|
+
if (rows.length === 0) return [`no sessions in ${root} — a \`rovecode run\` or a TUI turn starts one`, ...(footer ? [footer] : [])].join("\n");
|
|
65
|
+
const lines = [`sessions in ${root} (newest first; --json for the full rows):`, `${"id".padEnd(36)} ${"created".padEnd(24)} turns first prompt`];
|
|
66
|
+
for (const r of rows) lines.push(`${r.id.padEnd(36)} ${r.created.padEnd(24)} ${String(r.turns).padStart(5)} ${r.title ?? (r.firstPrompt || "(empty session)")}`); // a title stands in for the first prompt
|
|
67
|
+
if (footer) lines.push(footer);
|
|
68
|
+
return lines.join("\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** one search hit per line: session id8 · ISO time · entry id8 (or `title` for a title-tier hit) · [title] preview */
|
|
72
|
+
export function formatSearchHit(r: SearchRow): string {
|
|
73
|
+
const where = r.entryId === "" ? "title" : r.entryId.slice(0, 8);
|
|
74
|
+
return `${r.sessionId.slice(0, 8)} ${new Date(r.timestamp).toISOString()} ${where.padEnd(8)} ${r.title !== undefined ? `[${r.title}] ` : ""}${r.preview}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const SESSIONS_USAGE = "usage: rovecode sessions [--json] | rename <id|prefix> <title…> | delete <id|prefix> [--yes] | fork <id|prefix> [--json] | search <terms…> [--json]";
|
|
78
|
+
const VERBS: ReadonlySet<string> = new Set(["rename", "delete", "fork", "search"]);
|
|
79
|
+
|
|
80
|
+
/** a token that is a flag, not a value: `-x`, `--x` (a lone `-` would be stdin, a value) */
|
|
81
|
+
const isFlag = (a: string): boolean => a.length > 1 && a.startsWith("-");
|
|
82
|
+
|
|
83
|
+
/** the words after the `sessions` token minus every flag token */
|
|
84
|
+
export function sessionWords(argv: readonly string[]): string[] {
|
|
85
|
+
const at = argv.indexOf("sessions");
|
|
86
|
+
return at === -1 ? [] : argv.slice(at + 1).filter((a) => !isFlag(a));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface SessionsCmdIo {
|
|
90
|
+
out: (text: string) => void;
|
|
91
|
+
err: (text: string) => void;
|
|
92
|
+
/** stdin is a terminal (the y/N prompt is possible); default process.stdin.isTTY */
|
|
93
|
+
tty?: boolean;
|
|
94
|
+
/** the y/N prompt; default Bun's global confirm */
|
|
95
|
+
confirm?: (question: string) => boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** main.ts dispatch: the sessions of `cwd`. Returns the exit code. */
|
|
99
|
+
export function cmdSessions(argv: readonly string[], cwd: string, io: Partial<SessionsCmdIo> = {}): number {
|
|
100
|
+
const out = io.out ?? ((t: string) => console.log(t));
|
|
101
|
+
const err = io.err ?? ((t: string) => process.stderr.write(`${t}\n`));
|
|
102
|
+
const usageError = (msg: string): number => { err(`error: ${msg} — ${SESSIONS_USAGE}`); return 2; }; // ONE stderr line, exit 2
|
|
103
|
+
const json = argv.includes("--json");
|
|
104
|
+
const words = sessionWords(argv);
|
|
105
|
+
const verb = words[0];
|
|
106
|
+
if (verb === undefined) { out(formatSessions(sessionListing(cwd), json)); return 0; }
|
|
107
|
+
if (!VERBS.has(verb)) return usageError(`unknown sessions verb "${verb}"`);
|
|
108
|
+
const root = sessionsRoot(cwd);
|
|
109
|
+
if (verb === "search") {
|
|
110
|
+
const q = words.slice(1).join(" ").trim();
|
|
111
|
+
if (q === "") return usageError("search needs at least one term");
|
|
112
|
+
const rows = searchSessions(root, q, 10);
|
|
113
|
+
if (json) out(JSON.stringify(rows));
|
|
114
|
+
else out(rows.length === 0 ? `no matches for "${q}" in ${root}` : rows.map(formatSearchHit).join("\n"));
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
const ref = words[1];
|
|
118
|
+
if (ref === undefined) return usageError(`${verb} needs a session id or prefix`);
|
|
119
|
+
const found = resolveSession(root, ref); // validity + the /resume rule over readdir names — nothing is created or removed before this
|
|
120
|
+
if (!found.ok) return usageError(found.error);
|
|
121
|
+
if (verb === "rename") {
|
|
122
|
+
const title = oneLineTitle(words.slice(2).join(" "));
|
|
123
|
+
if (title === undefined) return usageError("rename needs a non-empty title");
|
|
124
|
+
renameSession(root, found.id, title);
|
|
125
|
+
out(`renamed ${found.id} → "${title}"`);
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
if (verb === "fork") {
|
|
129
|
+
let r;
|
|
130
|
+
try { r = forkSession(root, found.summary); } catch (e) { return usageError(e instanceof Error ? e.message : String(e)); }
|
|
131
|
+
out(json ? JSON.stringify(r) : `${r.id} fork of ${r.from.slice(0, 8)} · "${r.title}"`);
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
// delete — codex DeleteConfirmation { Prompt, Skip }: `--yes` = Skip; Prompt only on a TTY (default N); otherwise refused
|
|
135
|
+
if (!argv.includes("--yes")) {
|
|
136
|
+
const tty = io.tty ?? process.stdin.isTTY === true;
|
|
137
|
+
if (!tty) return usageError(`delete ${found.id.slice(0, 8)} needs --yes when stdin is not a terminal`);
|
|
138
|
+
const label = found.summary.title ?? (found.summary.preview || "(empty session)");
|
|
139
|
+
const ask = io.confirm ?? ((q: string) => confirm(q));
|
|
140
|
+
if (!ask(`delete session ${found.id} (${label}, ${found.summary.entryCount} entries) and its checkpoints?`)) { out("delete cancelled — nothing removed"); return 0; }
|
|
141
|
+
}
|
|
142
|
+
const { removed } = deleteSession(cwd, root, found.id);
|
|
143
|
+
out(removed.length === 0 ? `nothing to remove for ${found.id}` : removed.map((p) => `removed ${p}`).join("\n"));
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
package/src/cli/setup.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/** `rovecode setup` — connect a model in one sitting: pick a provider, name the model, paste the key
|
|
2
|
+
* (masked, via auth.ts readSecret), one tiny real call, and the pick becomes the default in
|
|
3
|
+
* ~/.rovecode/providers.json. Every step reads through an injectable seam (ask / secret / save /
|
|
4
|
+
* probe) so the whole flow is unit-testable with scripted answers and no network. Piped stdin
|
|
5
|
+
* (no TTY) prints the three-line recipe and exits 2 — a wizard must not consume a script's input.
|
|
6
|
+
* Wording: core/voice.ts rules — rovecode speaks, short lines, every problem ends with "→ next:". */
|
|
7
|
+
|
|
8
|
+
import { createInterface } from "node:readline";
|
|
9
|
+
import { readSecret, saveCredential } from "../providers/auth.ts";
|
|
10
|
+
import { PROVIDER_ID_RE, inferProtocol, isConfigured } from "../providers/provider-config.ts";
|
|
11
|
+
import type { ProviderRegistry } from "../providers/registry.ts";
|
|
12
|
+
import { next, noModelHint } from "../core/voice.ts";
|
|
13
|
+
|
|
14
|
+
export interface ProbeResult { ok: boolean; model: string; detail: string }
|
|
15
|
+
|
|
16
|
+
export interface SetupDeps {
|
|
17
|
+
registry: ProviderRegistry;
|
|
18
|
+
/** default console.log */
|
|
19
|
+
out?: (line: string) => void;
|
|
20
|
+
/** one plain-text answer (default: readline on stdin, echoed) */
|
|
21
|
+
ask?: (prompt: string) => Promise<string>;
|
|
22
|
+
/** one secret (default: readSecret — masked on a TTY) */
|
|
23
|
+
secret?: (prompt: string) => Promise<string>;
|
|
24
|
+
/** default auth.ts saveCredential */
|
|
25
|
+
save?: (id: string, secret: string, keyEnv: string) => void;
|
|
26
|
+
/** default registry.probe (one tiny real call) */
|
|
27
|
+
probe?: (id: string, model?: string) => Promise<ProbeResult>;
|
|
28
|
+
/** default process.stdin.isTTY === true */
|
|
29
|
+
tty?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Pick { key: string; label: string; id?: string; local?: boolean; url?: "openai" | "anthropic" }
|
|
33
|
+
|
|
34
|
+
/** the numbered menu: the common hosted providers, the two local servers, and two "your own URL" doors */
|
|
35
|
+
export const SETUP_PICKS: readonly Pick[] = [
|
|
36
|
+
{ key: "1", id: "anthropic", label: "anthropic — Claude models" },
|
|
37
|
+
{ key: "2", id: "openai", label: "openai" },
|
|
38
|
+
{ key: "3", id: "openrouter", label: "openrouter — many models behind one key" },
|
|
39
|
+
{ key: "4", id: "deepseek", label: "deepseek" },
|
|
40
|
+
{ key: "5", id: "groq", label: "groq" },
|
|
41
|
+
{ key: "6", id: "ollama", label: "ollama — local, no key needed", local: true },
|
|
42
|
+
{ key: "7", id: "lmstudio", label: "lmstudio — local, no key needed", local: true },
|
|
43
|
+
{ key: "8", label: "another OpenAI-compatible URL (a proxy, vLLM, …)", url: "openai" },
|
|
44
|
+
{ key: "9", label: "another Anthropic-protocol URL", url: "anthropic" },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export const SETUP_DONE = 'Done. Try: rovecode "explain this repo"';
|
|
48
|
+
|
|
49
|
+
/** one echoed line from the terminal — shared with main.ts's numbered model picker */
|
|
50
|
+
export function askLine(prompt: string): Promise<string> {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
53
|
+
let settled = false;
|
|
54
|
+
rl.question(prompt, (a) => { if (settled) return; settled = true; rl.close(); resolve(a.trim()); });
|
|
55
|
+
rl.once("close", () => { if (!settled) { settled = true; resolve(""); } });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** exit code: 0 connected (or added with a clear next step), 2 not a TTY / cancelled / invalid input */
|
|
60
|
+
export async function runSetup(deps: SetupDeps): Promise<number> {
|
|
61
|
+
const out = deps.out ?? console.log;
|
|
62
|
+
const ask = deps.ask ?? askLine;
|
|
63
|
+
const secret = deps.secret ?? ((p: string) => readSecret(p));
|
|
64
|
+
const save = deps.save ?? saveCredential;
|
|
65
|
+
const reg = deps.registry;
|
|
66
|
+
const probe = deps.probe ?? ((id: string, model?: string) => reg.probe(id, model));
|
|
67
|
+
const tty = deps.tty ?? process.stdin.isTTY === true;
|
|
68
|
+
|
|
69
|
+
if (!tty) { out(noModelHint("cli")); out(" (rovecode setup needs a terminal — this stdin is a pipe)"); return 2; }
|
|
70
|
+
|
|
71
|
+
out("◆ rovecode setup — let's connect a model. Two or three questions.");
|
|
72
|
+
out("");
|
|
73
|
+
for (const p of SETUP_PICKS) out(` ${p.key} ${p.label}`);
|
|
74
|
+
out("");
|
|
75
|
+
let pick: Pick | undefined;
|
|
76
|
+
for (let tries = 0; tries < 3 && pick === undefined; tries++) {
|
|
77
|
+
const a = (await ask("Which one? [1-9, empty = cancel]: ")).trim();
|
|
78
|
+
if (a.length === 0) { out(`cancelled — nothing changed. ${next("rovecode setup")} whenever you like`); return 2; }
|
|
79
|
+
pick = SETUP_PICKS.find((p) => p.key === a || p.id === a);
|
|
80
|
+
if (pick === undefined) out(` "${a}" is not on the list — a number from 1 to 9, please.`);
|
|
81
|
+
}
|
|
82
|
+
if (pick === undefined) return 2;
|
|
83
|
+
|
|
84
|
+
// ---- the provider: a built-in, a keyless local server, or a URL of your own ----
|
|
85
|
+
let id: string;
|
|
86
|
+
if (pick.url !== undefined) {
|
|
87
|
+
id = (await ask("Short name for it (letters, digits, - _ .): ")).trim().toLowerCase();
|
|
88
|
+
if (!PROVIDER_ID_RE.test(id)) { out(` "${id}" won't work as an id. ${next("rovecode setup, and pick a name like myproxy")}`); return 2; }
|
|
89
|
+
const baseUrl = (await ask("Base URL (e.g. https://host/v1): ")).trim();
|
|
90
|
+
if (!/^https?:\/\//.test(baseUrl)) { out(` "${baseUrl}" is not an http(s) URL. ${next("rovecode setup")}`); return 2; }
|
|
91
|
+
const protocol = pick.url === "anthropic" ? "anthropic" : baseUrl.includes("anthropic.com") ? inferProtocol(baseUrl) : "openai";
|
|
92
|
+
const noKey = (await ask("Does it need an API key? [Y/n]: ")).trim().toLowerCase().startsWith("n");
|
|
93
|
+
const r = reg.add({ id, baseUrl, protocol, ...(noKey ? { noKey: true } : {}) }, "user");
|
|
94
|
+
if ("error" in r) { out(` ${r.error}. ${next("rovecode setup")}`); return 2; }
|
|
95
|
+
out(`◆ ${id} registered (${protocol} protocol, ${baseUrl}).`);
|
|
96
|
+
} else {
|
|
97
|
+
id = pick.id!;
|
|
98
|
+
const known = reg.get(id);
|
|
99
|
+
if (known === undefined) { out(` ${id} is missing from the built-in table — that is a bug. ${next("rovecode provider add " + id + " <baseUrl>")}`); return 2; }
|
|
100
|
+
if (pick.local === true && known.noKey !== true) {
|
|
101
|
+
// the built-in row expects a key env; a local server needs none — the user entry says so
|
|
102
|
+
const r = reg.add({ id, baseUrl: known.baseUrl, protocol: known.protocol, noKey: true, ...(known.defaultModel !== undefined ? { defaultModel: known.defaultModel } : {}) }, "user");
|
|
103
|
+
if ("error" in r) { out(` ${r.error}. ${next("rovecode setup")}`); return 2; }
|
|
104
|
+
out(`◆ ${id} marked as a local server (${known.baseUrl}, no key).`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---- the model ----
|
|
109
|
+
const spec = reg.get(id)!;
|
|
110
|
+
const suggested = spec.defaultModel;
|
|
111
|
+
const modelAnswer = (await ask(suggested !== undefined ? `Model id [${suggested}]: ` : "Model id (empty = choose later with rovecode model list): ")).trim();
|
|
112
|
+
const model = modelAnswer.length > 0 ? modelAnswer : suggested;
|
|
113
|
+
|
|
114
|
+
// ---- the key ----
|
|
115
|
+
let hasKey = isConfigured(spec);
|
|
116
|
+
if (!hasKey) {
|
|
117
|
+
const s = await secret(`${spec.keyEnv} (hidden): `);
|
|
118
|
+
if (s.trim().length === 0) {
|
|
119
|
+
out(`◆ no key stored — I can't call ${id} without one.`);
|
|
120
|
+
out(` ${next(`rovecode auth set ${id}`)} (or set ${spec.keyEnv} in your environment)`);
|
|
121
|
+
} else {
|
|
122
|
+
save(id, s.trim(), spec.keyEnv);
|
|
123
|
+
reg.refresh();
|
|
124
|
+
hasKey = true;
|
|
125
|
+
out(`◆ key stored for ${id} (hidden; ~/.rovecode/credentials.json).`);
|
|
126
|
+
}
|
|
127
|
+
} else out(`◆ ${id} already has a key (${spec.keySource === "stored" ? "stored" : `env ${spec.keyEnv}`}).`);
|
|
128
|
+
|
|
129
|
+
// ---- one tiny real call ----
|
|
130
|
+
if (hasKey && model !== undefined) {
|
|
131
|
+
out(`◆ testing ${id}/${model} …`);
|
|
132
|
+
const r = await probe(id, model);
|
|
133
|
+
if (r.ok) out(` ${r.detail}`);
|
|
134
|
+
else {
|
|
135
|
+
out(` that didn't work: ${r.detail}`);
|
|
136
|
+
out(` ${next(`check the key and the URL, then rovecode provider test ${id} ${model}`)}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---- make it the default ----
|
|
141
|
+
if (model !== undefined) {
|
|
142
|
+
const d = reg.setDefault(`${id}/${model}`, "user");
|
|
143
|
+
if ("error" in d) { out(` ${d.error}. ${next(`rovecode model use ${id}/<model>`)}`); return 2; }
|
|
144
|
+
out(`◆ default → ${d.provider}/${d.model} (~/.rovecode/providers.json; running TUIs switch live).`);
|
|
145
|
+
} else {
|
|
146
|
+
out(`◆ ${id} is set up but has no model yet.`);
|
|
147
|
+
out(` ${next(`rovecode model list ${id}, then rovecode model use ${id}/<model>`)}`);
|
|
148
|
+
}
|
|
149
|
+
out("");
|
|
150
|
+
out(SETUP_DONE);
|
|
151
|
+
out(" or open the cockpit: rovecode");
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/** `rovecode skills list|validate|pack|install` (port #72): the agentskills.io-shaped skill catalogue on the command
|
|
2
|
+
* line. Mirrors cli/mcp-market-cmd.ts — hand-parsed argv, an io seam, ONE dispatch line in main.ts — plus an
|
|
3
|
+
* injectable fetch and DNS resolver for the URL install. Exit codes: 0 ok · 1 usage / strict finding / refused
|
|
4
|
+
* overwrite / non-http(s) URL / symlink / a refused address · 2 io (unreadable, not an archive, unsafe entries, an
|
|
5
|
+
* invalid staged skill, HTTP or network failure). `list` is the LENIENT loader the runtime uses (every loaded skill,
|
|
6
|
+
* every loader warning, the files it refused); `validate` and `pack` are STRICT (spec violations are errors, unknown
|
|
7
|
+
* keys notes, nothing is written after a finding). Scopes are skills/index.ts skillsDir(cwd, scope):
|
|
8
|
+
* `<cwd>/.rovecode/skills`, or `<home>/skills` with --user (ROVECODE_HOME honoured). allowed-tools is never enforced.
|
|
9
|
+
*
|
|
10
|
+
* `install <url>` goes through the SAME SSRF guard as web_fetch and web_search (tools/webfetch.ts ssrfDenyReason:
|
|
11
|
+
* loopback, private, link-local and reserved ranges refused, names resolved through the injectable resolver) — on the
|
|
12
|
+
* URL and on every redirect hop, http(s) only, ≤ 5 hops. The reason is that a human typing a URL is not the only
|
|
13
|
+
* caller: a model with bash can run `rovecode skills install http://169.254.169.254/…`, and cloud metadata is exactly
|
|
14
|
+
* what that guard is for. A CLI that skips the check its own tools make is the soft spot in the boundary, so do not
|
|
15
|
+
* remove this. There is deliberately NO allow-private knob: an intranet-hosted archive is refused, and `curl` +
|
|
16
|
+
* `skills install <file.tar.gz>` is the local path — one env knob is a decision for Berkay, not a default.
|
|
17
|
+
* Body bounded at 64 MiB (webfetch readBounded) under a ref'd timeout. */
|
|
18
|
+
|
|
19
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { basename, join, resolve } from "node:path";
|
|
21
|
+
import { SKILL_FILE, SkillStore, skillsDir } from "../skills/index.ts";
|
|
22
|
+
import { parseSkillFrontmatter, validateSkillMeta, type Validation } from "../skills/spec.ts";
|
|
23
|
+
import { installFromBytes, installFromDir, MAX_ARCHIVE_BYTES, packSkill, SkillCmdError, type InstallResult } from "../skills/pack.ts";
|
|
24
|
+
import { abortable, dnsResolver, readBounded, ssrfDenyReason, type FetchLike, type Resolver } from "../tools/webfetch.ts";
|
|
25
|
+
|
|
26
|
+
export interface SkillsCmdIo { out(line: string): void; err(line: string): void }
|
|
27
|
+
export interface SkillsCmdDeps {
|
|
28
|
+
io?: SkillsCmdIo;
|
|
29
|
+
/** the URL install's fetch (tests inject a loopback / scripted one) */
|
|
30
|
+
fetch?: FetchLike;
|
|
31
|
+
/** the SSRF guard's name resolver (tests inject one that answers for their loopback fixture) */
|
|
32
|
+
resolve?: Resolver;
|
|
33
|
+
/** the user-scope skills dir (default `<user state dir>/skills`) */
|
|
34
|
+
globalDir?: string;
|
|
35
|
+
}
|
|
36
|
+
const stdio: SkillsCmdIo = { out: (l) => console.log(l), err: (l) => console.error(l) };
|
|
37
|
+
|
|
38
|
+
export const SKILLS_USAGE =
|
|
39
|
+
"usage: rovecode skills list [--json] | rovecode skills validate <dir> | rovecode skills pack <dir> [--out <file>] [--force] | " +
|
|
40
|
+
"rovecode skills install <dir|file.tar.gz|http(s)-url> [--user] [--force]";
|
|
41
|
+
export const INSTALL_TIMEOUT_MS = 60_000;
|
|
42
|
+
export const MAX_REDIRECTS = 5;
|
|
43
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
44
|
+
const URL_SCHEME = /^([a-z][a-z0-9+.-]*):\/\//i;
|
|
45
|
+
|
|
46
|
+
/** Entry point: `args` are the argv words AFTER `skills`. Returns the exit code. */
|
|
47
|
+
export async function cmdSkills(args: string[], cwd: string, deps: SkillsCmdDeps = {}): Promise<number> {
|
|
48
|
+
const io = deps.io ?? stdio;
|
|
49
|
+
const action = args[0] ?? "";
|
|
50
|
+
const rest = args.slice(1);
|
|
51
|
+
const flags = new Set(rest.filter((a) => a.startsWith("--")));
|
|
52
|
+
const outIx = rest.indexOf("--out");
|
|
53
|
+
const outFile = outIx !== -1 ? rest[outIx + 1] : undefined;
|
|
54
|
+
const positional = rest.filter((a, i) => !a.startsWith("--") && rest[i - 1] !== "--out");
|
|
55
|
+
const globalDir = deps.globalDir ?? skillsDir(cwd, "global");
|
|
56
|
+
try {
|
|
57
|
+
if (action === "list") return list(cwd, globalDir, flags.has("--json"), io);
|
|
58
|
+
if (action === "validate" && positional[0] !== undefined) return validate(resolve(cwd, positional[0]), io);
|
|
59
|
+
if (action === "pack" && positional[0] !== undefined) {
|
|
60
|
+
return await pack(resolve(cwd, positional[0]), outFile !== undefined ? resolve(cwd, outFile) : undefined, flags.has("--force"), cwd, io);
|
|
61
|
+
}
|
|
62
|
+
if (action === "install" && positional[0] !== undefined) {
|
|
63
|
+
const target = flags.has("--user") ? globalDir : skillsDir(cwd, "project");
|
|
64
|
+
return await install(positional[0], cwd, target, flags.has("--force"), deps.fetch ?? ((u, i) => fetch(u, i)), deps.resolve ?? dnsResolver, io);
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
io.err(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
68
|
+
return err instanceof SkillCmdError ? err.code : 2;
|
|
69
|
+
}
|
|
70
|
+
io.err(SKILLS_USAGE);
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function list(cwd: string, globalDir: string, json: boolean, io: SkillsCmdIo): number {
|
|
75
|
+
const r = new SkillStore(cwd, { globalDir }).scan();
|
|
76
|
+
const byPath = new Map<string, string[]>();
|
|
77
|
+
for (const w of r.warnings) byPath.set(w.path, [...(byPath.get(w.path) ?? []), w.reason]);
|
|
78
|
+
const rows = r.skills.map((s) => ({
|
|
79
|
+
name: s.name, version: s.version, scope: s.scope, path: s.path, dir: s.dir, description: s.fullDescription,
|
|
80
|
+
license: s.license, compatibility: s.compatibility, metadata: s.metadata, allowedTools: s.allowedTools,
|
|
81
|
+
warnings: byPath.get(s.path) ?? [],
|
|
82
|
+
}));
|
|
83
|
+
if (json) {
|
|
84
|
+
io.out(JSON.stringify({ skills: rows, invalid: r.invalid }, null, 2));
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
if (rows.length === 0 && r.invalid.length === 0) {
|
|
88
|
+
io.out(`no skills installed (${skillsDir(cwd, "project")} or ${globalDir}) — rovecode skills install <dir|file.tar.gz|url>`);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
for (const row of rows) io.out(`${row.name.padEnd(24)} ${(row.version || "-").padEnd(10)} ${row.scope.padEnd(8)} ${row.path}`);
|
|
92
|
+
for (const row of rows) for (const w of row.warnings) io.err(`warning: ${row.path}: ${w}`);
|
|
93
|
+
for (const i of r.invalid) io.err(`warning: ${i.path}: not loaded — ${i.reason}`);
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The strict spec check of `<dir>/SKILL.md`; unreadable → 2, no frontmatter → a finding (1). */
|
|
98
|
+
function strictCheck(dir: string): Validation {
|
|
99
|
+
const file = join(dir, SKILL_FILE);
|
|
100
|
+
let text: string;
|
|
101
|
+
try { text = readFileSync(file, "utf8"); }
|
|
102
|
+
catch (e) { throw new SkillCmdError(`cannot read ${file}: ${e instanceof Error ? e.message : String(e)}`, 2); }
|
|
103
|
+
const fm = parseSkillFrontmatter(text);
|
|
104
|
+
if (fm === null) throw new SkillCmdError(`${file}: missing or unterminated frontmatter block`, 1);
|
|
105
|
+
return validateSkillMeta(fm, basename(dir), "strict");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** one stderr line per finding, notes on stdout; true when there are no errors */
|
|
109
|
+
function report(v: Validation, dir: string, io: SkillsCmdIo): boolean {
|
|
110
|
+
for (const e of v.errors) io.err(`${join(dir, SKILL_FILE)}: ${e}`);
|
|
111
|
+
for (const n of v.notes) io.out(`note: ${n}`);
|
|
112
|
+
return v.errors.length === 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function validate(dir: string, io: SkillsCmdIo): number {
|
|
116
|
+
const v = strictCheck(dir);
|
|
117
|
+
if (!report(v, dir, io)) return 1;
|
|
118
|
+
io.out(`ok: ${v.meta.name}${v.meta.version ? ` (v${v.meta.version})` : ""} — ${join(dir, SKILL_FILE)}`);
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function pack(dir: string, outFile: string | undefined, force: boolean, cwd: string, io: SkillsCmdIo): Promise<number> {
|
|
123
|
+
const v = strictCheck(dir);
|
|
124
|
+
if (!report(v, dir, io)) return 1; // any finding → nothing written
|
|
125
|
+
const out = outFile ?? join(cwd, `${v.meta.name}.tar.gz`);
|
|
126
|
+
if (existsSync(out) && !force) {
|
|
127
|
+
io.err(`error: ${out} already exists (use --force to overwrite)`);
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
const bytes = await packSkill(dir, v.meta.name); // a symlink inside → SkillCmdError(1) before any write
|
|
131
|
+
writeFileSync(out, bytes);
|
|
132
|
+
io.out(`packed ${v.meta.name} → ${out} (${bytes.length} bytes)`);
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function install(src: string, cwd: string, dir: string, force: boolean, fetchImpl: FetchLike, resolve_: Resolver, io: SkillsCmdIo): Promise<number> {
|
|
137
|
+
const scheme = URL_SCHEME.exec(src)?.[1]?.toLowerCase();
|
|
138
|
+
let result: InstallResult;
|
|
139
|
+
if (scheme !== undefined) {
|
|
140
|
+
if (scheme !== "http" && scheme !== "https") throw new SkillCmdError(`only http(s) URLs can be installed (got ${scheme}:)`, 1);
|
|
141
|
+
result = await installFromBytes(await download(src, fetchImpl, resolve_), dir, { force });
|
|
142
|
+
} else {
|
|
143
|
+
const path = resolve(cwd, src);
|
|
144
|
+
let isDir: boolean;
|
|
145
|
+
try { isDir = statSync(path).isDirectory(); }
|
|
146
|
+
catch { throw new SkillCmdError(`${path}: no such file or directory`, 2); }
|
|
147
|
+
result = isDir ? installFromDir(path, dir, { force }) : await installFromBytes(readFileSync(path), dir, { force });
|
|
148
|
+
}
|
|
149
|
+
io.out(`installed ${result.name}${result.version ? ` (v${result.version})` : ""} → ${result.path}`);
|
|
150
|
+
for (const w of result.warnings) io.err(`warning: ${w}`);
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The archive body under the SSRF guard (see the header): the address is checked BEFORE the request and again on
|
|
155
|
+
* every redirect hop — `redirect: "manual"`, so a public URL cannot redirect us into the metadata service — http(s)
|
|
156
|
+
* only, ≤ MAX_REDIRECTS. A REF'D timeout drives the abort (Bun's AbortSignal.timeout timer is unref'd — webfetch.ts
|
|
157
|
+
* pattern). A refused address is 1 (nothing was fetched); non-2xx / network failure / over the cap are 2. */
|
|
158
|
+
async function download(url: string, fetchImpl: FetchLike, resolve_: Resolver): Promise<Uint8Array> {
|
|
159
|
+
const ac = new AbortController();
|
|
160
|
+
const timer = setTimeout(() => ac.abort(), INSTALL_TIMEOUT_MS);
|
|
161
|
+
(timer as unknown as { ref?: () => void }).ref?.();
|
|
162
|
+
const failure = (e: unknown): SkillCmdError => new SkillCmdError(
|
|
163
|
+
`download failed: ${ac.signal.aborted ? `timed out after ${INSTALL_TIMEOUT_MS}ms` : e instanceof Error ? e.message : String(e)}`, 2);
|
|
164
|
+
try {
|
|
165
|
+
let current = new URL(url);
|
|
166
|
+
let hops = 0;
|
|
167
|
+
for (;;) {
|
|
168
|
+
if (current.protocol !== "http:" && current.protocol !== "https:") {
|
|
169
|
+
throw new SkillCmdError(`only http(s) URLs can be installed (got ${current.protocol})`, 1);
|
|
170
|
+
}
|
|
171
|
+
let reason: string | null;
|
|
172
|
+
try { reason = await abortable(ssrfDenyReason(current.hostname, resolve_), ac.signal); }
|
|
173
|
+
catch (e) { throw failure(e); }
|
|
174
|
+
if (reason !== null) throw new SkillCmdError(`refused ${current.href}: ${reason}`, 1);
|
|
175
|
+
let res: Response;
|
|
176
|
+
try { res = await abortable(fetchImpl(current.href, { signal: ac.signal, redirect: "manual" }), ac.signal); }
|
|
177
|
+
catch (e) { throw failure(e); }
|
|
178
|
+
if (REDIRECT_STATUSES.has(res.status)) {
|
|
179
|
+
const location = res.headers.get("location");
|
|
180
|
+
await res.body?.cancel().catch(() => {});
|
|
181
|
+
if (!location) throw new SkillCmdError(`download failed: HTTP ${res.status} from ${current.href} without a Location header`, 2);
|
|
182
|
+
if (++hops > MAX_REDIRECTS) throw new SkillCmdError(`download failed: too many redirects (more than ${MAX_REDIRECTS}) starting from ${url}`, 2);
|
|
183
|
+
try { current = new URL(location, current); } catch { throw new SkillCmdError(`download failed: invalid redirect target ${location}`, 2); }
|
|
184
|
+
continue; // the guard runs again on the new host — that is the point of the manual loop
|
|
185
|
+
}
|
|
186
|
+
if (!res.ok) throw new SkillCmdError(`download failed: HTTP ${res.status} for ${current.href}`, 2);
|
|
187
|
+
const { bytes, truncated } = await readBounded(res, MAX_ARCHIVE_BYTES);
|
|
188
|
+
if (truncated) throw new SkillCmdError(`download exceeds ${MAX_ARCHIVE_BYTES} bytes`, 2);
|
|
189
|
+
return bytes;
|
|
190
|
+
}
|
|
191
|
+
} finally {
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Interactive startup only. The loading screen owns stdout through module loading, resume resolution,
|
|
2
|
+
* runtime construction and the sandbox probe. runTui hands it over immediately before its first complete
|
|
3
|
+
* frame. Plain chat never loads the renderers; optional network/cache work is not a readiness barrier. */
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { startIntro } from "../core/intro.ts";
|
|
6
|
+
import type { CliInvocation } from "./dispatch.ts";
|
|
7
|
+
import pkg from "../../package.json";
|
|
8
|
+
|
|
9
|
+
export async function startChat(cli: CliInvocation, argv: string[] = process.argv): Promise<void> {
|
|
10
|
+
if (cli.plain) {
|
|
11
|
+
const [{ runRepl }, { parseAddDirs }, { resolveBoot }] = await Promise.all([
|
|
12
|
+
import("./repl.ts"), import("./run-flags.ts"), import("./resume.ts"),
|
|
13
|
+
]);
|
|
14
|
+
resolveBoot(argv, join(process.cwd(), ".rovecode", "sessions")); // keep the existing invalid-resume diagnostic
|
|
15
|
+
await runRepl({ yolo: cli.yolo, addDirs: parseAddDirs(argv) });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const intro = startIntro({
|
|
19
|
+
write: (text) => { process.stdout.write(text); },
|
|
20
|
+
tty: process.stdout.isTTY === true && !argv.includes("--no-intro") && process.env.ROVECODE_INTRO !== "0",
|
|
21
|
+
version: pkg.version, holdUntilReady: true,
|
|
22
|
+
size: () => ({ columns: process.stdout.columns || 80, rows: process.stdout.rows || 24 }),
|
|
23
|
+
onResize: (paint) => { process.stdout.on("resize", paint); return () => { process.stdout.off("resize", paint); }; },
|
|
24
|
+
});
|
|
25
|
+
const ac = new AbortController();
|
|
26
|
+
let signalExit = 130;
|
|
27
|
+
const interrupt = () => { signalExit = 130; ac.abort(new Error("startup interrupted")); };
|
|
28
|
+
const terminate = () => { signalExit = 143; ac.abort(new Error("startup terminated")); };
|
|
29
|
+
const finish = () => {
|
|
30
|
+
intro.finish();
|
|
31
|
+
process.off("SIGINT", interrupt); process.off("SIGTERM", terminate);
|
|
32
|
+
};
|
|
33
|
+
// Also covers parsers that exit(2) instead of throwing. No raw input or keystroke consumption here.
|
|
34
|
+
process.once("exit", finish);
|
|
35
|
+
process.once("SIGINT", interrupt); process.once("SIGTERM", terminate);
|
|
36
|
+
const t0 = Number(process.env._ROVECODE_BOOT_T0 ?? -1);
|
|
37
|
+
const trace = (label: string) => { if (t0 >= 0) process.stderr.write(`[boot] +${Date.now() - t0}ms ${label}\n`); };
|
|
38
|
+
try {
|
|
39
|
+
trace("intro started");
|
|
40
|
+
intro.status("loading modules");
|
|
41
|
+
const [{ runTui }, { interactiveRenderer }, { parseAddDirs }, { resolveBoot }] = await Promise.all([
|
|
42
|
+
import("../tui/app.ts"), import("../tui/notify.ts"), import("./run-flags.ts"), import("./resume.ts"),
|
|
43
|
+
]);
|
|
44
|
+
ac.signal.throwIfAborted();
|
|
45
|
+
trace("surface modules loaded");
|
|
46
|
+
intro.status("opening session");
|
|
47
|
+
const cwd = process.cwd();
|
|
48
|
+
const boot = resolveBoot(argv, join(cwd, ".rovecode", "sessions"));
|
|
49
|
+
const addDirs = parseAddDirs(argv);
|
|
50
|
+
await runTui({
|
|
51
|
+
yolo: cli.yolo, acceptEdits: cli.acceptEdits,
|
|
52
|
+
...(cli.effort !== undefined ? { effort: cli.effort } : {}),
|
|
53
|
+
sessionId: boot.id, ...(boot.note !== undefined ? { bootNote: boot.note } : {}),
|
|
54
|
+
renderer: interactiveRenderer(cli, process.env, process.stdout, process.stdin, cwd),
|
|
55
|
+
...(cli.pet !== undefined ? { pet: cli.pet } : {}), ...(addDirs.length > 0 ? { addDirs } : {}),
|
|
56
|
+
startup: { status: intro.status, finish, animationDone: intro.animationDone, signal: ac.signal },
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (ac.signal.aborted) { process.exitCode = signalExit; return; }
|
|
60
|
+
throw error;
|
|
61
|
+
} finally {
|
|
62
|
+
finish();
|
|
63
|
+
process.off("exit", finish);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** `rovecode trust` — the approval verb for every project file the trust gate covers (core/trust.ts): settings.json's
|
|
2
|
+
* command-bearing keys, hooks.ts, sandbox.json, the MCP files. `trust show` says what each file WOULD do; `trust [--yes]`
|
|
3
|
+
* approves them as they are now (a TTY confirms after the listing; off a TTY --yes is required); `trust untrust` withdraws.
|
|
4
|
+
* `rovecode mcp trust` keeps working and acts on the MCP files only — the same store, so nothing already approved is lost. */
|
|
5
|
+
|
|
6
|
+
import { rovecodeHome } from "../providers/auth.ts";
|
|
7
|
+
import { projectTrustRows, trustRows, trustShowLines, untrustRows } from "../core/project-trust.ts";
|
|
8
|
+
|
|
9
|
+
export const TRUST_USAGE = [
|
|
10
|
+
"usage: rovecode trust list this repo's gated files with what they would do, then approve them (asks on a TTY)",
|
|
11
|
+
" rovecode trust --yes approve without asking (after reading `trust show`)",
|
|
12
|
+
" rovecode trust show list only — nothing changes",
|
|
13
|
+
" rovecode trust untrust withdraw the approval; the files contribute nothing again until trusted",
|
|
14
|
+
" gated: .rovecode/settings.json (verify, lsp, notify_command) · .rovecode/hooks.ts|js · .rovecode/sandbox.json · .rovecode/mcp.json · .mcp.json",
|
|
15
|
+
" the store is ~/.rovecode/plugins.json (path → sha256): any edit to a file asks again",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export interface TrustCmdIo {
|
|
19
|
+
out?: (line: string) => void;
|
|
20
|
+
err?: (line: string) => void;
|
|
21
|
+
/** default rovecodeHome() */
|
|
22
|
+
home?: string;
|
|
23
|
+
/** default process.stdin.isTTY */
|
|
24
|
+
tty?: boolean;
|
|
25
|
+
/** the confirmation prompt (default Bun's confirm()) */
|
|
26
|
+
confirm?: (question: string) => Promise<boolean> | boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function cmdTrust(argv: string[], cwd: string = process.cwd(), io: TrustCmdIo = {}): Promise<number> {
|
|
30
|
+
const out = io.out ?? ((l: string) => console.log(l));
|
|
31
|
+
const err = io.err ?? ((l: string) => console.error(l));
|
|
32
|
+
const home = io.home ?? rovecodeHome();
|
|
33
|
+
const words = argv.filter((a) => !a.startsWith("--"));
|
|
34
|
+
const yes = argv.includes("--yes");
|
|
35
|
+
const verb = words[0] ?? "";
|
|
36
|
+
if (verb === "-h" || verb === "help" || argv.includes("--help")) { for (const l of TRUST_USAGE) out(l); return 0; }
|
|
37
|
+
if (verb !== "" && verb !== "show" && verb !== "untrust") { err(`unknown trust command "${verb}"`); for (const l of TRUST_USAGE) err(l); return 2; }
|
|
38
|
+
const rows = projectTrustRows(cwd, home);
|
|
39
|
+
if (verb === "show") { for (const l of trustShowLines(rows)) out(l); return 0; }
|
|
40
|
+
if (verb === "untrust") { for (const l of untrustRows(home, rows)) out(l); return 0; }
|
|
41
|
+
if (rows.length === 0) { for (const l of trustShowLines(rows)) out(l); return 1; }
|
|
42
|
+
for (const l of trustShowLines(rows)) out(l);
|
|
43
|
+
if (!yes) {
|
|
44
|
+
const tty = io.tty ?? process.stdin.isTTY === true;
|
|
45
|
+
if (!tty) { err("nothing trusted: no terminal to confirm on — re-run with --yes after reading the lines above"); return 1; }
|
|
46
|
+
const ask = io.confirm ?? ((q: string) => confirm(q));
|
|
47
|
+
if (!(await ask("trust these files as they are now? [y/N]"))) { out("nothing trusted"); return 1; }
|
|
48
|
+
}
|
|
49
|
+
for (const l of trustRows(home, rows)) out(l);
|
|
50
|
+
out("restart rovecode to apply — an edit to any of these files asks again");
|
|
51
|
+
return 0;
|
|
52
|
+
}
|