@causyne/agent 0.0.0-stage → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +102 -0
- package/README.md +31 -2
- package/SECURITY.md +24 -0
- package/bin/causyne.js +13 -0
- package/docs/ARCHITECTURE.md +63 -0
- package/docs/PROVENANCE.md +29 -0
- package/docs/SECURITY_REVIEW.md +89 -0
- package/docs/THIRD_PARTY_NOTICES.md +49 -0
- package/native-skills/ai-engineering/context-window-management/SKILL.md +43 -0
- package/native-skills/ai-engineering/llm-evaluation/SKILL.md +46 -0
- package/native-skills/ai-engineering/model-cost-and-latency/SKILL.md +46 -0
- package/native-skills/ai-engineering/prompt-construction/SKILL.md +42 -0
- package/native-skills/ai-engineering/retrieval-augmented-generation/SKILL.md +42 -0
- package/native-skills/ai-engineering/structured-output-and-schemas/SKILL.md +45 -0
- package/native-skills/ai-engineering/tool-calling-agents/SKILL.md +43 -0
- package/native-skills/android/app-performance-profiling/SKILL.md +42 -0
- package/native-skills/android/background-work-scheduling/SKILL.md +47 -0
- package/native-skills/android/compose-state-and-recomposition/SKILL.md +43 -0
- package/native-skills/android/gradle-build-configuration/SKILL.md +41 -0
- package/native-skills/android/lifecycle-and-leaks/SKILL.md +46 -0
- package/native-skills/android/play-store-release/SKILL.md +43 -0
- package/native-skills/android/room-and-persistence/SKILL.md +40 -0
- package/native-skills/backend/authentication-and-sessions/SKILL.md +45 -0
- package/native-skills/backend/background-jobs-and-queues/SKILL.md +42 -0
- package/native-skills/backend/caching-strategy/SKILL.md +44 -0
- package/native-skills/backend/graceful-shutdown/SKILL.md +42 -0
- package/native-skills/backend/rate-limiting-and-quotas/SKILL.md +40 -0
- package/native-skills/backend/rest-api-design/SKILL.md +40 -0
- package/native-skills/backend/webhook-delivery-and-receipt/SKILL.md +43 -0
- package/native-skills/cloud/cost-optimisation/SKILL.md +46 -0
- package/native-skills/cloud/iam-least-privilege/SKILL.md +43 -0
- package/native-skills/cloud/infrastructure-as-code/SKILL.md +43 -0
- package/native-skills/cloud/multi-region-and-failover/SKILL.md +43 -0
- package/native-skills/cloud/serverless-patterns/SKILL.md +47 -0
- package/native-skills/data/data-quality-monitoring/SKILL.md +51 -0
- package/native-skills/data/etl-pipeline-reliability/SKILL.md +43 -0
- package/native-skills/data/exploratory-analysis/SKILL.md +43 -0
- package/native-skills/data/metric-definition/SKILL.md +42 -0
- package/native-skills/data/notebook-to-production/SKILL.md +43 -0
- package/native-skills/database/backup-and-recovery/SKILL.md +43 -0
- package/native-skills/database/connection-pooling/SKILL.md +42 -0
- package/native-skills/database/data-integrity-and-constraints/SKILL.md +42 -0
- package/native-skills/database/query-optimisation/SKILL.md +38 -0
- package/native-skills/database/safe-migrations/SKILL.md +47 -0
- package/native-skills/database/schema-design/SKILL.md +38 -0
- package/native-skills/database/transactions-and-isolation/SKILL.md +41 -0
- package/native-skills/design/colour-system-construction/SKILL.md +38 -0
- package/native-skills/design/design-direction-brief/SKILL.md +41 -0
- package/native-skills/design/motion-and-transitions/SKILL.md +38 -0
- package/native-skills/design/responsive-strategy/SKILL.md +39 -0
- package/native-skills/design/spacing-and-rhythm/SKILL.md +38 -0
- package/native-skills/design/type-scale-and-hierarchy/SKILL.md +39 -0
- package/native-skills/design/visual-qa-loop/SKILL.md +42 -0
- package/native-skills/devops/docker-multi-stage-builds/SKILL.md +60 -0
- package/native-skills/devops/github-actions-ci/SKILL.md +60 -0
- package/native-skills/devops/incident-triage/SKILL.md +38 -0
- package/native-skills/devops/kubernetes-deployment-hardening/SKILL.md +64 -0
- package/native-skills/devops/observability-baseline/SKILL.md +38 -0
- package/native-skills/devops/secret-management/SKILL.md +40 -0
- package/native-skills/devops/terraform-state-and-modules/SKILL.md +36 -0
- package/native-skills/devops/zero-downtime-deploys/SKILL.md +40 -0
- package/native-skills/git/bisect-regression-hunt/SKILL.md +43 -0
- package/native-skills/git/branch-strategy/SKILL.md +39 -0
- package/native-skills/git/commit-hygiene/SKILL.md +41 -0
- package/native-skills/git/history-recovery/SKILL.md +44 -0
- package/native-skills/git/large-repository-surgery/SKILL.md +46 -0
- package/native-skills/git/merge-conflict-resolution/SKILL.md +42 -0
- package/native-skills/media/cinematic-3d-shot/SKILL.md +104 -0
- package/native-skills/mobile/app-startup-optimisation/SKILL.md +41 -0
- package/native-skills/mobile/cross-platform-tradeoffs/SKILL.md +42 -0
- package/native-skills/mobile/deep-linking/SKILL.md +39 -0
- package/native-skills/mobile/mobile-storage-security/SKILL.md +45 -0
- package/native-skills/mobile/offline-first-sync/SKILL.md +42 -0
- package/native-skills/mobile/push-notifications/SKILL.md +38 -0
- package/native-skills/performance/algorithmic-complexity/SKILL.md +43 -0
- package/native-skills/performance/concurrency-and-parallelism/SKILL.md +42 -0
- package/native-skills/performance/load-testing/SKILL.md +42 -0
- package/native-skills/performance/memory-and-leak-hunting/SKILL.md +41 -0
- package/native-skills/performance/network-and-payload-efficiency/SKILL.md +42 -0
- package/native-skills/performance/profiling-methodology/SKILL.md +46 -0
- package/native-skills/python/async-correctness/SKILL.md +40 -0
- package/native-skills/python/cli-tool-construction/SKILL.md +44 -0
- package/native-skills/python/data-processing-pipelines/SKILL.md +38 -0
- package/native-skills/python/environment-isolation/SKILL.md +41 -0
- package/native-skills/python/packaging-and-distribution/SKILL.md +56 -0
- package/native-skills/python/pytest-suite-design/SKILL.md +39 -0
- package/native-skills/python/type-hints-and-static-checking/SKILL.md +42 -0
- package/native-skills/security/authorization-model/SKILL.md +42 -0
- package/native-skills/security/crypto-usage/SKILL.md +48 -0
- package/native-skills/security/dependency-supply-chain/SKILL.md +42 -0
- package/native-skills/security/input-validation-and-injection/SKILL.md +44 -0
- package/native-skills/security/secure-file-uploads/SKILL.md +46 -0
- package/native-skills/security/security-headers-and-csp/SKILL.md +44 -0
- package/native-skills/security/threat-modelling/SKILL.md +46 -0
- package/native-skills/testing/end-to-end-test-stability/SKILL.md +42 -0
- package/native-skills/testing/property-based-testing/SKILL.md +46 -0
- package/native-skills/testing/regression-test-from-a-bug/SKILL.md +41 -0
- package/native-skills/testing/snapshot-and-visual-regression/SKILL.md +42 -0
- package/native-skills/testing/test-doubles-and-mocking/SKILL.md +43 -0
- package/native-skills/testing/test-strategy-and-pyramid/SKILL.md +39 -0
- package/native-skills/testing/writing-good-assertions/SKILL.md +42 -0
- package/native-skills/web/accessible-components/SKILL.md +45 -0
- package/native-skills/web/api-client-resilience/SKILL.md +39 -0
- package/native-skills/web/bundle-size-reduction/SKILL.md +38 -0
- package/native-skills/web/core-web-vitals/SKILL.md +40 -0
- package/native-skills/web/css-layout-systems/SKILL.md +41 -0
- package/native-skills/web/form-validation-ux/SKILL.md +39 -0
- package/native-skills/web/react-state-architecture/SKILL.md +41 -0
- package/native-skills/web/ssr-hydration-correctness/SKILL.md +45 -0
- package/package.json +68 -4
- package/src/agentLoop.js +2458 -0
- package/src/appChannel.js +109 -0
- package/src/approvalGrants.js +66 -0
- package/src/auditLog.js +52 -0
- package/src/autoUpdate.js +12 -0
- package/src/capabilities.js +36 -0
- package/src/checkpoints.js +1199 -0
- package/src/cliMode.js +638 -0
- package/src/commands.js +805 -0
- package/src/compiledSkills.js +540 -0
- package/src/config.js +130 -0
- package/src/contextManager.js +498 -0
- package/src/cortex/autoCondenser.js +192 -0
- package/src/cortex/provisioner.js +27 -0
- package/src/cortex/vectorDb.js +447 -0
- package/src/designPalettes.js +99 -0
- package/src/devMode.js +6 -0
- package/src/embodimentSession.js +180 -0
- package/src/entrypoint.js +194 -0
- package/src/execSafe.js +21 -0
- package/src/gitSnapshot.js +378 -0
- package/src/mcp/jsonRpc.js +165 -0
- package/src/mcp/mcpClient.js +136 -0
- package/src/mcp/mcpRegistry.js +207 -0
- package/src/memory.js +62 -0
- package/src/memoryManager.js +124 -0
- package/src/modelCatalogue.js +132 -0
- package/src/modelIds.js +42 -0
- package/src/modelRouter.js +145 -0
- package/src/outputStore.js +246 -0
- package/src/perception.js +124 -0
- package/src/planCache.js +437 -0
- package/src/procSignal.js +32 -0
- package/src/providers/anthropic.js +408 -0
- package/src/providers/base.js +328 -0
- package/src/providers/dashscope-byok.js +86 -0
- package/src/providers/deepseek.js +80 -0
- package/src/providers/factory.js +37 -0
- package/src/providers/gemini.js +346 -0
- package/src/providers/moonshot.js +73 -0
- package/src/providers/muse.js +169 -0
- package/src/providers/ollama.js +104 -0
- package/src/providers/openai-compatible.js +84 -0
- package/src/providers/openai.js +20 -0
- package/src/providers/qwen-byok.js +76 -0
- package/src/providers/xai.js +73 -0
- package/src/provisioning/capabilityConsent.js +24 -0
- package/src/provisioning/capabilityProvisioner.js +271 -0
- package/src/provisioning/capabilityTools.js +29 -0
- package/src/reporting.js +373 -0
- package/src/sandbox.js +529 -0
- package/src/sessionStore.js +279 -0
- package/src/skillsManager.js +288 -0
- package/src/slopDetector.js +307 -0
- package/src/taskBuffer.js +45 -0
- package/src/taskShape.js +241 -0
- package/src/textDiff.js +150 -0
- package/src/tokenTracker.js +560 -0
- package/src/tool-scripts/ax_snapshot.applescript +39 -0
- package/src/tool-scripts/ax_snapshot.ps1 +10 -0
- package/src/tool-scripts/desktop_act.applescript +58 -0
- package/src/tool-scripts/resolve_automator.py +119 -0
- package/src/toolPacks.js +524 -0
- package/src/tools/archive.js +63 -0
- package/src/tools/astryxOracle.js +78 -0
- package/src/tools/axSnapshot.js +165 -0
- package/src/tools/blenderAutomator.js +349 -0
- package/src/tools/browserAutomator.js +252 -0
- package/src/tools/checkSystemSpecs.js +64 -0
- package/src/tools/clipboard.js +36 -0
- package/src/tools/codeSearchAst.js +101 -0
- package/src/tools/codeSearcher.js +230 -0
- package/src/tools/codeTools.js +262 -0
- package/src/tools/codebaseSearch.js +75 -0
- package/src/tools/createSkill.js +129 -0
- package/src/tools/deferredMcpSearch.js +246 -0
- package/src/tools/desktopAct.js +208 -0
- package/src/tools/desktopOpen.js +34 -0
- package/src/tools/dynamicLoader.js +44 -0
- package/src/tools/executeProceduralMedia.js +93 -0
- package/src/tools/fsDelete.js +51 -0
- package/src/tools/fsEdit.js +105 -0
- package/src/tools/fsFind.js +128 -0
- package/src/tools/fsList.js +77 -0
- package/src/tools/fsMove.js +57 -0
- package/src/tools/fsRead.js +87 -0
- package/src/tools/fsSearch.js +198 -0
- package/src/tools/fsWrite.js +41 -0
- package/src/tools/gameAiBehaviorTree.js +208 -0
- package/src/tools/gameAnimationRig.js +194 -0
- package/src/tools/gameAssetFetcher.js +372 -0
- package/src/tools/gameAssetHarvester.js +210 -0
- package/src/tools/gameAudioSynth.js +267 -0
- package/src/tools/gameBuilder.js +886 -0
- package/src/tools/gameCanvasAudit.js +101 -0
- package/src/tools/gameCinematicSequencer.js +151 -0
- package/src/tools/gameDataForge.js +124 -0
- package/src/tools/gameDialogueTreeGen.js +166 -0
- package/src/tools/gameEconomyBalancer.js +161 -0
- package/src/tools/gameEntitySpawner.js +207 -0
- package/src/tools/gameLoreForge.js +153 -0
- package/src/tools/gameQuestArcBuilder.js +152 -0
- package/src/tools/gameSceneBuilder.js +158 -0
- package/src/tools/gameShaderForge.js +181 -0
- package/src/tools/gameTilemapGenerator.js +164 -0
- package/src/tools/gameUiHudBuilder.js +292 -0
- package/src/tools/gameVfxEmitterGen.js +108 -0
- package/src/tools/gameWebRuntimeInspector.js +91 -0
- package/src/tools/generateMediaViaApi.js +358 -0
- package/src/tools/gitExec.js +54 -0
- package/src/tools/godotAutomator.js +437 -0
- package/src/tools/manageLocalAiDependencies.js +220 -0
- package/src/tools/memoryWrite.js +99 -0
- package/src/tools/multiEdit.js +107 -0
- package/src/tools/palmierAutomator.js +104 -0
- package/src/tools/pdfReader.js +112 -0
- package/src/tools/persistentShell.js +148 -0
- package/src/tools/printer.js +316 -0
- package/src/tools/procTerm.js +91 -0
- package/src/tools/processList.js +52 -0
- package/src/tools/readNativeSkill.js +39 -0
- package/src/tools/registry.js +277 -0
- package/src/tools/resolveAutomator.js +102 -0
- package/src/tools/screenshot.js +75 -0
- package/src/tools/subagentOrchestrator.js +305 -0
- package/src/tools/sysRun.js +125 -0
- package/src/tools/todoWrite.js +75 -0
- package/src/tools/toolDispatch.js +127 -0
- package/src/tools/toolForge.js +124 -0
- package/src/tools/updatePlan.js +88 -0
- package/src/tools/viewImage.js +82 -0
- package/src/tools/visionInspector.js +154 -0
- package/src/tools/webFetch.js +74 -0
- package/src/tools/webSearch.js +82 -0
- package/src/tui/ask.js +164 -0
- package/src/tui/banner.js +158 -0
- package/src/tui/block.js +152 -0
- package/src/tui/chatRenderer.js +953 -0
- package/src/tui/completion.js +424 -0
- package/src/tui/dialogs.js +606 -0
- package/src/tui/diff.js +296 -0
- package/src/tui/history.js +135 -0
- package/src/tui/home.js +217 -0
- package/src/tui/invocation.js +105 -0
- package/src/tui/keys.js +443 -0
- package/src/tui/lineEditor.js +256 -0
- package/src/tui/overlay.js +256 -0
- package/src/tui/prompt.js +1258 -0
- package/src/tui/selectList.js +335 -0
- package/src/tui/stage.js +330 -0
- package/src/tui/stageSurface.js +609 -0
- package/src/tui/statusLine.js +254 -0
- package/src/tui/text.js +345 -0
- package/src/tui/theme.js +393 -0
- package/src/tui/toolDisplay.js +176 -0
- package/src/tui/transcript.js +235 -0
- package/src/tui/viewport.js +337 -0
- package/src/uncertainty.js +271 -0
- package/src/userSkills.js +265 -0
- package/src/utils/cli_ui.js +0 -0
- package/src/utils/inquirer.js +12 -0
- package/src/utils/paths.js +37 -0
- package/src/version.js +38 -0
- package/src/vfeTracker.js +220 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
// Completion for the chat prompt: `/` commands and `@` file paths.
|
|
2
|
+
//
|
|
3
|
+
// Two providers, one picker. The command provider reads the SAME registry that
|
|
4
|
+
// executes commands (src/commands.js), so a command can never appear in the
|
|
5
|
+
// picker and then not exist — there is no second list to drift.
|
|
6
|
+
//
|
|
7
|
+
// SECURITY: a filename is untrusted input. `touch $'evil\e[31m.txt'` is legal on
|
|
8
|
+
// every POSIX filesystem, and offering that name would inject an escape both
|
|
9
|
+
// into the picker and into the user's edit buffer. Anything that does not
|
|
10
|
+
// survive sanitize() unchanged is dropped, because a path we cannot print
|
|
11
|
+
// safely is also a path we cannot safely type.
|
|
12
|
+
//
|
|
13
|
+
// Pure Node. No dependency, no shell.
|
|
14
|
+
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import fsp from 'fs/promises';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import { listCommands } from '../commands.js';
|
|
19
|
+
import { listCompiledSkills as listSkillsForCompletion } from '../skillsManager.js';
|
|
20
|
+
import { sanitize, shortenHome } from './text.js';
|
|
21
|
+
|
|
22
|
+
export const MAX_VISIBLE = 8;
|
|
23
|
+
|
|
24
|
+
// Directories that are never worth indexing. Walking node_modules turns a
|
|
25
|
+
// 40 ms index into a 40 s one and buries real files under vendored ones.
|
|
26
|
+
const SKIP_DIRECTORIES = new Set([
|
|
27
|
+
'node_modules', '.git', 'dist', 'build', 'out', 'target',
|
|
28
|
+
'.next', '.nuxt', '.venv', 'venv', '__pycache__', '.gradle',
|
|
29
|
+
'.idea', '.cache', 'coverage', 'vendor', 'Pods', '.causyne'
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const MAX_INDEX_ENTRIES = 20000;
|
|
33
|
+
const MAX_INDEX_DEPTH = 12;
|
|
34
|
+
|
|
35
|
+
/** A value is offerable only if printing it changes nothing. */
|
|
36
|
+
export function isTerminalSafe(value) {
|
|
37
|
+
const text = String(value == null ? '' : value);
|
|
38
|
+
return text.length > 0 && sanitize(text) === text;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Matching
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Score `candidate` against `query`, or -1 for no match.
|
|
47
|
+
*
|
|
48
|
+
* Prefix beats subsequence, and a match on the basename beats one buried in a
|
|
49
|
+
* parent directory. Substring containment is deliberately NOT the test on its
|
|
50
|
+
* own — rules.json ZM-09: a naive `includes()` both over-matches unrelated
|
|
51
|
+
* paths and under-ranks the file the user meant.
|
|
52
|
+
*/
|
|
53
|
+
export function score(candidate, query) {
|
|
54
|
+
if (!query) return 0;
|
|
55
|
+
const haystack = candidate.toLowerCase();
|
|
56
|
+
const needle = query.toLowerCase();
|
|
57
|
+
|
|
58
|
+
if (haystack.startsWith(needle)) return 1000 - candidate.length;
|
|
59
|
+
|
|
60
|
+
const base = haystack.slice(haystack.lastIndexOf('/') + 1);
|
|
61
|
+
if (base.startsWith(needle)) return 800 - candidate.length;
|
|
62
|
+
if (base.includes(needle)) return 600 - candidate.length;
|
|
63
|
+
|
|
64
|
+
// Subsequence: every character of the query in order, gaps allowed. This is
|
|
65
|
+
// what lets "tuich" find "tui/chatRenderer.js".
|
|
66
|
+
let index = 0;
|
|
67
|
+
let gaps = 0;
|
|
68
|
+
for (const character of haystack) {
|
|
69
|
+
if (character === needle[index]) {
|
|
70
|
+
index += 1;
|
|
71
|
+
if (index === needle.length) return 400 - gaps - candidate.length;
|
|
72
|
+
} else if (index > 0) {
|
|
73
|
+
gaps += 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return -1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Score, sort and cap — and report how many matched BEFORE the cap.
|
|
81
|
+
*
|
|
82
|
+
* The cap exists for the file index, which can hold tens of thousands of paths.
|
|
83
|
+
* But the picker's "… N more" line was computed from the capped array, so with
|
|
84
|
+
* 115 skill entries it announced "42 more" when 107 were reachable by scrolling.
|
|
85
|
+
* A count that is wrong is worse than no count: it tells the user to stop
|
|
86
|
+
* looking. The total travels with the items so the caller can say the truth.
|
|
87
|
+
*/
|
|
88
|
+
function rank(items, query, limit = 50) {
|
|
89
|
+
const scored = [];
|
|
90
|
+
for (const item of items) {
|
|
91
|
+
const value = score(item.match !== undefined ? item.match : item.value, query);
|
|
92
|
+
if (value >= 0) scored.push({ item, value });
|
|
93
|
+
}
|
|
94
|
+
scored.sort((a, b) => b.value - a.value);
|
|
95
|
+
const ranked = scored.slice(0, limit).map((entry) => entry.item);
|
|
96
|
+
ranked.total = scored.length;
|
|
97
|
+
return ranked;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// A command's own vocabulary is bounded and small — a few hundred at most — and
|
|
101
|
+
// every entry is reachable by scrolling the picker. Capping it at the file-index
|
|
102
|
+
// limit hid two thirds of the skill library behind a number that denied it was
|
|
103
|
+
// there.
|
|
104
|
+
const ARGUMENT_LIMIT = 500;
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// `/` commands
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
function commandItems() {
|
|
111
|
+
const commands = listCommands('terminal')
|
|
112
|
+
.filter((command) => isTerminalSafe(command.name))
|
|
113
|
+
.map((command) => ({
|
|
114
|
+
value: '/' + command.name,
|
|
115
|
+
match: command.name,
|
|
116
|
+
label: '/' + command.name,
|
|
117
|
+
description: sanitize(command.description || '')
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
// Skills answer to the slash menu directly, the way claude-code surfaces a
|
|
121
|
+
// skill as a command you simply type. `/docker` finds the Docker skill
|
|
122
|
+
// without the user knowing it lives under `devops`, or that `/skills` exists
|
|
123
|
+
// at all — which is the whole difficulty with a hundred-entry library.
|
|
124
|
+
//
|
|
125
|
+
// Ranked against the skill NAME first so the name is what matches, and
|
|
126
|
+
// resolved to the canonical `/skills <domain>/<id>` on accept, so there is
|
|
127
|
+
// still exactly one execution path and no second command namespace to collide
|
|
128
|
+
// with the real commands.
|
|
129
|
+
const skills = listSkillsForCompletion().map((skill) => ({
|
|
130
|
+
value: `/skills ${skill.domain}/${skill.id}`,
|
|
131
|
+
match: `${skill.id} ${skill.domain}`,
|
|
132
|
+
label: `/${skill.id}`,
|
|
133
|
+
description: sanitize(skill.description || '')
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
// Commands first: on a bare `/` every entry scores 0, and a stable sort then
|
|
137
|
+
// leaves the real commands above the library.
|
|
138
|
+
return commands.concat(skills);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* A command's own argument suggestions.
|
|
143
|
+
*
|
|
144
|
+
* The command owns them: it knows what its argument means and this module does
|
|
145
|
+
* not. Anything a command returns is treated as untrusted display text and
|
|
146
|
+
* sanitised, the same as a filename.
|
|
147
|
+
*/
|
|
148
|
+
function argumentItems(name) {
|
|
149
|
+
const command = listCommands('terminal')
|
|
150
|
+
.find((candidate) => candidate.name === name && isTerminalSafe(candidate.name));
|
|
151
|
+
if (!command || typeof command.complete !== 'function') return [];
|
|
152
|
+
|
|
153
|
+
let produced;
|
|
154
|
+
try {
|
|
155
|
+
produced = command.complete();
|
|
156
|
+
} catch (e) {
|
|
157
|
+
return []; // a broken completer must never take the prompt down
|
|
158
|
+
}
|
|
159
|
+
if (!Array.isArray(produced)) return [];
|
|
160
|
+
|
|
161
|
+
return produced
|
|
162
|
+
.filter((entry) => entry && typeof entry.value === 'string' && isTerminalSafe(entry.value))
|
|
163
|
+
.map((entry) => ({
|
|
164
|
+
value: entry.value,
|
|
165
|
+
match: entry.match === undefined ? entry.value : entry.match,
|
|
166
|
+
label: sanitize(entry.label === undefined ? entry.value : entry.label),
|
|
167
|
+
description: sanitize(entry.description || '')
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// `@` files
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* A bounded, lazily built list of files under the workspace.
|
|
177
|
+
*
|
|
178
|
+
* openclaw shells out to `fd` and opencode uses `fff`; both are dependencies,
|
|
179
|
+
* and ZM-06 rules those out. ripgrep is not a safe substitute either — Causyne
|
|
180
|
+
* treats it as optional (src/tools/codeSearcher.js probes for a system binary
|
|
181
|
+
* and falls back to Node), so requiring it would make completion work on some
|
|
182
|
+
* machines and not others.
|
|
183
|
+
*
|
|
184
|
+
* So: the directory listing answers instantly and always, and this index
|
|
185
|
+
* upgrades the experience once it is ready. If it is never ready, nothing
|
|
186
|
+
* breaks — the caller must never await it.
|
|
187
|
+
*/
|
|
188
|
+
export class FileIndex {
|
|
189
|
+
constructor(root) {
|
|
190
|
+
this.root = root;
|
|
191
|
+
this.entries = null;
|
|
192
|
+
this.building = null;
|
|
193
|
+
this.truncated = false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
get isReady() {
|
|
197
|
+
return Array.isArray(this.entries);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Kick off the walk. Safe to call repeatedly; only the first one runs. */
|
|
201
|
+
ensure() {
|
|
202
|
+
if (this.entries || this.building) return this.building;
|
|
203
|
+
this.building = this.walk()
|
|
204
|
+
.then((entries) => { this.entries = entries; return entries; })
|
|
205
|
+
.catch(() => { this.entries = []; return []; })
|
|
206
|
+
.finally(() => { this.building = null; });
|
|
207
|
+
return this.building;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async walk() {
|
|
211
|
+
const found = [];
|
|
212
|
+
const queue = [{ dir: this.root, depth: 0 }];
|
|
213
|
+
|
|
214
|
+
while (queue.length > 0 && found.length < MAX_INDEX_ENTRIES) {
|
|
215
|
+
const { dir, depth } = queue.shift();
|
|
216
|
+
let listing;
|
|
217
|
+
try {
|
|
218
|
+
listing = await fsp.readdir(dir, { withFileTypes: true });
|
|
219
|
+
} catch (e) {
|
|
220
|
+
continue; // unreadable directory is not an error worth surfacing here
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const entry of listing) {
|
|
224
|
+
if (entry.name.startsWith('.') && entry.name !== '.env.example') continue;
|
|
225
|
+
const full = path.join(dir, entry.name);
|
|
226
|
+
|
|
227
|
+
if (entry.isDirectory()) {
|
|
228
|
+
if (SKIP_DIRECTORIES.has(entry.name) || depth >= MAX_INDEX_DEPTH) continue;
|
|
229
|
+
queue.push({ dir: full, depth: depth + 1 });
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (!entry.isFile()) continue;
|
|
233
|
+
|
|
234
|
+
const relative = path.relative(this.root, full);
|
|
235
|
+
if (!isTerminalSafe(relative)) continue;
|
|
236
|
+
found.push(relative);
|
|
237
|
+
if (found.length >= MAX_INDEX_ENTRIES) {
|
|
238
|
+
this.truncated = true;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return found;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Instant, shell-style completion of one directory level. */
|
|
248
|
+
function directoryItems(root, partial) {
|
|
249
|
+
const slash = partial.lastIndexOf('/');
|
|
250
|
+
const dirPart = slash === -1 ? '' : partial.slice(0, slash + 1);
|
|
251
|
+
const target = path.resolve(root, dirPart);
|
|
252
|
+
|
|
253
|
+
// Never offer paths outside the workspace root, whatever the user typed.
|
|
254
|
+
const relativeToRoot = path.relative(root, target);
|
|
255
|
+
if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) return [];
|
|
256
|
+
|
|
257
|
+
let listing;
|
|
258
|
+
try {
|
|
259
|
+
listing = fs.readdirSync(target, { withFileTypes: true });
|
|
260
|
+
} catch (e) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const items = [];
|
|
265
|
+
for (const entry of listing) {
|
|
266
|
+
if (entry.name.startsWith('.')) continue;
|
|
267
|
+
if (entry.isDirectory() && SKIP_DIRECTORIES.has(entry.name)) continue;
|
|
268
|
+
const value = dirPart + entry.name + (entry.isDirectory() ? '/' : '');
|
|
269
|
+
if (!isTerminalSafe(value)) continue;
|
|
270
|
+
items.push({
|
|
271
|
+
value,
|
|
272
|
+
match: value,
|
|
273
|
+
label: value,
|
|
274
|
+
description: entry.isDirectory() ? 'directory' : ''
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return items;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// Trigger detection
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* What, if anything, should be completed at the cursor.
|
|
286
|
+
*
|
|
287
|
+
* `/` only counts at the very start of the input — that is what keeps a task
|
|
288
|
+
* that merely mentions `/usr/local/bin` from opening a command picker, the same
|
|
289
|
+
* rule parseCommand() enforces when the line is finally submitted.
|
|
290
|
+
*/
|
|
291
|
+
export function detectTrigger(text, cursor) {
|
|
292
|
+
const before = text.slice(0, cursor);
|
|
293
|
+
|
|
294
|
+
if (/^\/[a-z0-9_?-]*$/i.test(before)) {
|
|
295
|
+
return { kind: 'command', start: 0, query: before.slice(1) };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// `/command <argument>` — the command's OWN suggestions.
|
|
299
|
+
//
|
|
300
|
+
// Completion used to stop dead at the space: `/skills d` offered nothing, so
|
|
301
|
+
// the only way to reach a skill was to submit `/skills` bare and browse. With
|
|
302
|
+
// a hundred of them that is a poor trade for someone who already knows the
|
|
303
|
+
// name they want.
|
|
304
|
+
//
|
|
305
|
+
// openclaw's contract (src/tui/commands.ts): a command declares its argument
|
|
306
|
+
// completions and the prompt asks it for the ones matching what has been typed
|
|
307
|
+
// — `getArgumentCompletions(prefix)`. Same shape here, generic over every
|
|
308
|
+
// command, so `/mcp`, `/model` and the rest can adopt it by adding one
|
|
309
|
+
// function and nothing in this file changes.
|
|
310
|
+
//
|
|
311
|
+
// Anchored, and a single argument token only: a second word ends the
|
|
312
|
+
// completion rather than trying to complete inside a sentence. opencode hides
|
|
313
|
+
// its picker on exactly the same condition.
|
|
314
|
+
const argument = before.match(/^\/([a-z0-9_?-]+)[ \t]+(\S*)$/i);
|
|
315
|
+
if (argument) {
|
|
316
|
+
return {
|
|
317
|
+
kind: 'command-argument',
|
|
318
|
+
command: argument[1],
|
|
319
|
+
start: cursor - argument[2].length,
|
|
320
|
+
query: argument[2]
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const mention = before.match(/(?:^|\s)@([^\s@]*)$/);
|
|
325
|
+
if (mention) {
|
|
326
|
+
return {
|
|
327
|
+
kind: 'file',
|
|
328
|
+
start: cursor - mention[1].length - 1,
|
|
329
|
+
query: mention[1]
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Suggestions for the cursor position, or null when nothing should open.
|
|
337
|
+
*
|
|
338
|
+
* Synchronous by design: a picker that appears a beat after you type feels
|
|
339
|
+
* broken. The file index is consulted only if it has already finished.
|
|
340
|
+
*/
|
|
341
|
+
export function suggest(text, cursor, options = {}) {
|
|
342
|
+
const trigger = detectTrigger(text, cursor);
|
|
343
|
+
if (!trigger) return null;
|
|
344
|
+
|
|
345
|
+
if (trigger.kind === 'command') {
|
|
346
|
+
const items = rank(commandItems(), trigger.query);
|
|
347
|
+
if (items.length === 0) return null;
|
|
348
|
+
return { ...trigger, items, total: items.total };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (trigger.kind === 'command-argument') {
|
|
352
|
+
const items = rank(argumentItems(trigger.command), trigger.query, ARGUMENT_LIMIT);
|
|
353
|
+
if (items.length === 0) return null;
|
|
354
|
+
return { ...trigger, items, total: items.total };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const root = options.root || process.cwd();
|
|
358
|
+
const index = options.index;
|
|
359
|
+
if (index && !index.isReady) index.ensure();
|
|
360
|
+
|
|
361
|
+
const pool = index && index.isReady && trigger.query.includes('/') === false && trigger.query
|
|
362
|
+
? index.entries.map((relative) => ({
|
|
363
|
+
value: relative, match: relative, label: relative, description: ''
|
|
364
|
+
}))
|
|
365
|
+
: directoryItems(root, trigger.query);
|
|
366
|
+
|
|
367
|
+
const items = rank(pool, trigger.query.slice(trigger.query.lastIndexOf('/') + 1));
|
|
368
|
+
if (items.length === 0) return null;
|
|
369
|
+
return { ...trigger, items, total: items.total };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Replace the trigger span with the chosen value. */
|
|
373
|
+
export function applyCompletion(text, cursor, trigger, item) {
|
|
374
|
+
if (trigger.kind === 'command-argument') {
|
|
375
|
+
const head = text.slice(0, trigger.start);
|
|
376
|
+
const tail = text.slice(cursor);
|
|
377
|
+
|
|
378
|
+
// A COMPLETE argument gets a trailing space; a partial one does not.
|
|
379
|
+
//
|
|
380
|
+
// This was written the other way round, on the theory that a space would
|
|
381
|
+
// reopen the picker on a value still being edited. It does not — a
|
|
382
|
+
// completed argument followed by a space matches no trigger at all. What it
|
|
383
|
+
// actually did was run the user's next words straight into the argument:
|
|
384
|
+
// accepting `python/data-processing-pipelines` and typing a question gave
|
|
385
|
+
// `/skills python/data-processing-pipelineswhy my data is like this ?` and
|
|
386
|
+
// a "skill not found" error.
|
|
387
|
+
//
|
|
388
|
+
// The distinction is the same one file completion already makes between a
|
|
389
|
+
// directory and a file: a domain (`devops`) is a place you may want to
|
|
390
|
+
// descend into with `/`, so it gets no space; a full `domain/skill` id is
|
|
391
|
+
// finished, so it gets one and the next thing typed is the instruction.
|
|
392
|
+
const complete = item.value.includes('/');
|
|
393
|
+
const replaced = head + item.value + (complete ? ' ' : '');
|
|
394
|
+
return { text: replaced + tail, cursor: replaced.length };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const value = trigger.kind === 'command' ? item.value : '@' + item.value;
|
|
398
|
+
const head = text.slice(0, trigger.start);
|
|
399
|
+
const tail = text.slice(cursor);
|
|
400
|
+
// A directory keeps the picker useful: no trailing space, so the next
|
|
401
|
+
// keystroke drills in rather than starting a new word.
|
|
402
|
+
//
|
|
403
|
+
// Everything else — including a skill picked from the slash menu, whose value
|
|
404
|
+
// is a whole `/skills domain/id` command — gets one, so the next thing typed
|
|
405
|
+
// is a new word rather than more of the last one. The exclusion that used to
|
|
406
|
+
// sit here for values containing a space was the same mistake as above: it
|
|
407
|
+
// made `/docker` complete to `/skills devops/docker-multi-stage-builds` with
|
|
408
|
+
// nothing after it, and the user's instruction fused onto the id.
|
|
409
|
+
const suffix = (trigger.kind === 'file' && item.value.endsWith('/')) ? '' : ' ';
|
|
410
|
+
const replaced = head + value + suffix;
|
|
411
|
+
return { text: replaced + tail, cursor: replaced.length };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Rows for the picker, already bounded and safe to print. */
|
|
415
|
+
export function renderPicker(items, selected, width) {
|
|
416
|
+
const start = Math.max(0, Math.min(selected - MAX_VISIBLE + 1, items.length - MAX_VISIBLE));
|
|
417
|
+
const window = items.slice(Math.max(0, start), Math.max(0, start) + MAX_VISIBLE);
|
|
418
|
+
return window.map((item, offset) => ({
|
|
419
|
+
item,
|
|
420
|
+
selected: Math.max(0, start) + offset === selected,
|
|
421
|
+
label: shortenHome(item.label),
|
|
422
|
+
description: item.description || ''
|
|
423
|
+
}));
|
|
424
|
+
}
|