titan-agent 6.1.0-beta.2 → 6.1.0-beta.21
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 +1 -29
- package/README.md +1 -1
- package/dist/agent/agentLessons.js +2 -2
- package/dist/agent/agentLessons.js.map +1 -1
- package/dist/agent/autoModeClassifier.js +95 -0
- package/dist/agent/autoModeClassifier.js.map +1 -0
- package/dist/agent/durableJournal.js +2 -2
- package/dist/agent/durableJournal.js.map +1 -1
- package/dist/agent/goalDriver.js +67 -0
- package/dist/agent/goalDriver.js.map +1 -1
- package/dist/agent/goalPlanFile.js +3 -3
- package/dist/agent/goalPlanFile.js.map +1 -1
- package/dist/agent/missionLifecycle.js +2 -2
- package/dist/agent/missionLifecycle.js.map +1 -1
- package/dist/agent/subAgent.js +41 -0
- package/dist/agent/subAgent.js.map +1 -1
- package/dist/agent/toolContract.js +49 -0
- package/dist/agent/toolContract.js.map +1 -0
- package/dist/agent/toolRunner.js +72 -11
- package/dist/agent/toolRunner.js.map +1 -1
- package/dist/agent/trajectoryRecorder.js +185 -0
- package/dist/agent/trajectoryRecorder.js.map +1 -0
- package/dist/agent/verification.js +164 -0
- package/dist/agent/verification.js.map +1 -0
- package/dist/agent/worktreeExecutor.js +207 -0
- package/dist/agent/worktreeExecutor.js.map +1 -0
- package/dist/config/schema.js +28 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/gateway/server.js +54 -0
- package/dist/gateway/server.js.map +1 -1
- package/dist/memory/facade.js +137 -0
- package/dist/memory/facade.js.map +1 -0
- package/dist/migrations/003-soma-profile-default.js +2 -2
- package/dist/migrations/003-soma-profile-default.js.map +1 -1
- package/dist/migrations/005-route-redirects.js +3 -1
- package/dist/migrations/005-route-redirects.js.map +1 -1
- package/dist/migrations/runner.js +2 -2
- package/dist/migrations/runner.js.map +1 -1
- package/dist/providers/defaultModel.js +14 -0
- package/dist/providers/defaultModel.js.map +1 -1
- package/dist/providers/router.js +4 -0
- package/dist/providers/router.js.map +1 -1
- package/dist/providers/stub.js +185 -0
- package/dist/providers/stub.js.map +1 -0
- package/dist/skills/builtin/backup.js +1 -2
- package/dist/skills/builtin/backup.js.map +1 -1
- package/dist/skills/builtin/contracts.js +254 -0
- package/dist/skills/builtin/contracts.js.map +1 -0
- package/dist/skills/builtin/download_image.js +9 -0
- package/dist/skills/builtin/download_image.js.map +1 -1
- package/dist/skills/builtin/filesystem.js +70 -20
- package/dist/skills/builtin/filesystem.js.map +1 -1
- package/dist/skills/registry.js +7 -0
- package/dist/skills/registry.js.map +1 -1
- package/dist/storage/patterns.js +2 -2
- package/dist/storage/patterns.js.map +1 -1
- package/dist/storage/personalGallery.js +2 -2
- package/dist/storage/personalGallery.js.map +1 -1
- package/dist/storage/somaProfile.js +2 -2
- package/dist/storage/somaProfile.js.map +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/constants.js.map +1 -1
- package/package.json +2 -2
- package/ui/dist/assets/{AuditPanel-DrDvXZ9u.js → AuditPanel-RNrvKmfX.js} +1 -1
- package/ui/dist/assets/{AutonomyPanel-BfhW-PRX.js → AutonomyPanel-D0nmt79R.js} +2 -2
- package/ui/dist/assets/{AutopilotPanel-C3yQBxAp.js → AutopilotPanel-DlcdfRRy.js} +1 -1
- package/ui/dist/assets/{AutoresearchPanel-BgpR3EcK.js → AutoresearchPanel-CXeO6kGK.js} +2 -2
- package/ui/dist/assets/BackupPanel-CKb-cDsk.js +1 -0
- package/ui/dist/assets/{BrowserPanel-BNaKZOnn.js → BrowserPanel-CDaoF6n5.js} +1 -1
- package/ui/dist/assets/CPActivity-BZbeztg1.js +1 -0
- package/ui/dist/assets/CPAgentDetail-C9fvLxKi.js +1 -0
- package/ui/dist/assets/CPAgents-BsBA9kb2.js +1 -0
- package/ui/dist/assets/CPApprovals-Di5itVIK.js +1 -0
- package/ui/dist/assets/CPCosts-DmgckHli.js +1 -0
- package/ui/dist/assets/CPDashboard-CmSb7woK.js +1 -0
- package/ui/dist/assets/{CPFiles-CUpP5I82.js → CPFiles-Dd0NGcpX.js} +1 -1
- package/ui/dist/assets/CPGoals-DeILFq0A.js +1 -0
- package/ui/dist/assets/{CPInbox-DUW_hntB.js → CPInbox-DAtidZez.js} +2 -2
- package/ui/dist/assets/CPIssueDetail-BvwoGI57.js +1 -0
- package/ui/dist/assets/CPIssues-CZ1hIKDP.js +1 -0
- package/ui/dist/assets/{CPLayout-J0fa-it4.js → CPLayout-luPwhNpA.js} +3 -3
- package/ui/dist/assets/CPOrg-BlYp_KP6.js +8 -0
- package/ui/dist/assets/CPRuns-D0HMCjIo.js +1 -0
- package/ui/dist/assets/{CPSocial-By_FStZ4.js → CPSocial-CZaGi14s.js} +2 -2
- package/ui/dist/assets/ChannelsPanel-khiBHwx3.js +1 -0
- package/ui/dist/assets/{CheckpointsPanel-DvKUd2VF.js → CheckpointsPanel-n2y3jzzT.js} +1 -1
- package/ui/dist/assets/{CommandPostHub-CQqRYBjf.js → CommandPostHub-iWf-zt5p.js} +4 -4
- package/ui/dist/assets/{CronPanel-Brmnnu8_.js → CronPanel-BkRJl8u6.js} +1 -1
- package/ui/dist/assets/{DataTable-seaktPzB.js → DataTable-DZ2cG3Cv.js} +1 -1
- package/ui/dist/assets/{DreamPanel-CO7iSdjn.js → DreamPanel-DF6l0WB-.js} +1 -1
- package/ui/dist/assets/{EmptyState-CmK9fywh.js → EmptyState-D7xwm8hA.js} +1 -1
- package/ui/dist/assets/{EvalHarnessPanel-2RVBKbym.js → EvalHarnessPanel-BHabu49z.js} +2 -2
- package/ui/dist/assets/{EvalPanel-CqAwhFQ5.js → EvalPanel-yAesMLSP.js} +1 -1
- package/ui/dist/assets/{FileViewer-D98m-vF5.js → FileViewer-DOWUQccc.js} +52 -14
- package/ui/dist/assets/{FilesPanel-dRQM0apb.js → FilesPanel-BjuDhqaX.js} +2 -2
- package/ui/dist/assets/FleetPanel-B0ZhS2XX.js +1 -0
- package/ui/dist/assets/{HomelabPanel-DjMUrO2B.js → HomelabPanel-D373ddvj.js} +1 -1
- package/ui/dist/assets/InfraView-CI99NZbI.js +2 -0
- package/ui/dist/assets/InlineEditableField-fDk3eGVo.js +1 -0
- package/ui/dist/assets/Input-j10hONMY.js +1 -0
- package/ui/dist/assets/IntegrationsPanel-kPa5Qq-f.js +1 -0
- package/ui/dist/assets/IntelligenceView-DeKBaAlY.js +2 -0
- package/ui/dist/assets/LearningPanel-B8tqHeEZ.js +1 -0
- package/ui/dist/assets/{LogsPanel-Bs9IsTCH.js → LogsPanel-sXc__trb.js} +1 -1
- package/ui/dist/assets/McpPanel-DEr8pZwB.js +1 -0
- package/ui/dist/assets/{MemoryGraphPanel-F1P9COyY.js → MemoryGraphPanel-DZl7USJ6.js} +1 -1
- package/ui/dist/assets/MemoryWikiPanel-DTNVWvVT.js +11 -0
- package/ui/dist/assets/{MeshPanel-CgIAh5ZX.js → MeshPanel-b77zyQx4.js} +1 -1
- package/ui/dist/assets/MissionCanvas-D_quSl5F.js +69 -0
- package/ui/dist/assets/MissionChat-rQ5rUPAt.js +7 -0
- package/ui/dist/assets/MissionLibrary-Cxw7sLxv.js +5 -0
- package/ui/dist/assets/MissionStart-ChMWO1rr.js +1 -0
- package/ui/dist/assets/Modal-Dl5b6Yaj.js +1 -0
- package/ui/dist/assets/NvidiaPanel-D7qFbbeJ.js +1 -0
- package/ui/dist/assets/{OrganismPanel-CNZM3eof.js → OrganismPanel-EiQ9Gfqx.js} +1 -1
- package/ui/dist/assets/OverviewPanel-Bectj1Z5.js +1 -0
- package/ui/dist/assets/{PageHeader-llOXO43Z.js → PageHeader-CiuoyvDR.js} +1 -1
- package/ui/dist/assets/{PersonaProfilesPanel-CfsQTMra.js → PersonaProfilesPanel-BO87DSVG.js} +1 -1
- package/ui/dist/assets/{PersonasPanel-DIac_kZ3.js → PersonasPanel-BNogLwM0.js} +1 -1
- package/ui/dist/assets/RecipesPanel-DyqfWlaY.js +1 -0
- package/ui/dist/assets/{SecurityPanel-_gqMk8hJ.js → SecurityPanel-OMC-Tvbz.js} +1 -1
- package/ui/dist/assets/SelfImprovePanel-DXIfG_df.js +1 -0
- package/ui/dist/assets/{SelfProposalsPanel-DuA0zsGH.js → SelfProposalsPanel-CmgEz1TQ.js} +2 -2
- package/ui/dist/assets/SessionsPanel-gJXosEGf.js +1 -0
- package/ui/dist/assets/{SessionsTab-bFYaFvTZ.js → SessionsTab-BOVzzjTO.js} +1 -1
- package/ui/dist/assets/SettingsPanel-DifFGEvi.js +1 -0
- package/ui/dist/assets/SettingsView-BNpj0iym.js +2 -0
- package/ui/dist/assets/SkeletonLoader-CNSsfH3f.js +1 -0
- package/ui/dist/assets/{SkillsPanel-D08WcRGp.js → SkillsPanel-6fbRov-m.js} +1 -1
- package/ui/dist/assets/{SomaView-CYukE7Fp.js → SomaView-D3boPl5D.js} +1 -1
- package/ui/dist/assets/{StatCard-DT4Bx1mY.js → StatCard-2BmOlamJ.js} +1 -1
- package/ui/dist/assets/StatusBadge-CdVHF1nG.js +1 -0
- package/ui/dist/assets/Tabs-DW9ytf9H.js +1 -0
- package/ui/dist/assets/{TeamsPanel-DOBe72TK.js → TeamsPanel-CNaQ8Dbw.js} +1 -1
- package/ui/dist/assets/TelemetryPanel-DL81QlAW.js +1 -0
- package/ui/dist/assets/TitanCanvas-DLIN_wV3.js +1152 -0
- package/ui/dist/assets/ToolsView-BlChEE4n.js +2 -0
- package/ui/dist/assets/Tooltip-oR5JnwoN.js +6 -0
- package/ui/dist/assets/{TraceViewer-0bCz0x_a.js → TraceViewer-DtJXjCXV.js} +1 -1
- package/ui/dist/assets/TrainingPanel-4XMGaRN_.js +1 -0
- package/ui/dist/assets/{VoiceOverlay-B08Sw918.js → VoiceOverlay-B3fXJ-EC.js} +1 -1
- package/ui/dist/assets/{VramPanel-CJiic8y4.js → VramPanel-HisAYiqI.js} +1 -1
- package/ui/dist/assets/{WatchView-CaQMDEAy.js → WatchView-C92y0hU7.js} +1 -1
- package/ui/dist/assets/{WorkTab-D5bi2WRi.js → WorkTab-BYAWcvA1.js} +1 -1
- package/ui/dist/assets/{WorkflowsPanel-DdY6E93C.js → WorkflowsPanel-D2DiOBLA.js} +1 -1
- package/ui/dist/assets/{arrow-left-CHJyrO8S.js → arrow-left-thXQMJQ6.js} +1 -1
- package/ui/dist/assets/{briefcase-ROIEmiah.js → briefcase-ByMhDqlL.js} +1 -1
- package/ui/dist/assets/{chart-column-wFkjV3Ni.js → chart-column-CYHJVuxN.js} +1 -1
- package/ui/dist/assets/{check-BcOm5jPO.js → check-ChWLFWCx.js} +1 -1
- package/ui/dist/assets/{chevron-down-CM11GPpR.js → chevron-down-CU_O_5h3.js} +1 -1
- package/ui/dist/assets/{chevron-right-aFDDgFsL.js → chevron-right-DAiXsK24.js} +1 -1
- package/ui/dist/assets/{chevron-up-D6g7FB6i.js → chevron-up-BudRYwvy.js} +1 -1
- package/ui/dist/assets/{circle-check-big-DPz64kZW.js → circle-check-big-gkA-V2JW.js} +1 -1
- package/ui/dist/assets/{clock-BbCCaukN.js → clock-qO5kIED9.js} +1 -1
- package/ui/dist/assets/clsx-B-dksMZM.js +1 -0
- package/ui/dist/assets/{dollar-sign-Dy5-qkg3.js → dollar-sign-1gQNJK3V.js} +1 -1
- package/ui/dist/assets/{download-DjheLEns.js → download-DMD1m87c.js} +1 -1
- package/ui/dist/assets/{external-link-BgxGF7ZQ.js → external-link-ByafQkZ7.js} +1 -1
- package/ui/dist/assets/{folder-DY4eeMdo.js → folder-DeG4RuOD.js} +1 -1
- package/ui/dist/assets/{funnel-LWmcwhj-.js → funnel-wE8i5V9Y.js} +1 -1
- package/ui/dist/assets/{git-branch-CPZciK10.js → git-branch-BWSuK1LU.js} +1 -1
- package/ui/dist/assets/index-B8YEFBRH.js +277 -0
- package/ui/dist/assets/index-Cb5bSxRB.css +1 -0
- package/ui/dist/assets/{layers-Bn7bgu2Z.js → layers-tN-wIpE0.js} +1 -1
- package/ui/dist/assets/{legacy-BVPeht6t.js → legacy-BSZBdyop.js} +2 -2
- package/ui/dist/assets/{lightbulb-tzuCyEQk.js → lightbulb-TUHKCda3.js} +1 -1
- package/ui/dist/assets/{list-todo-say4NbSw.js → list-todo-DtxcUGz2.js} +1 -1
- package/ui/dist/assets/{missions-DbC8UmMc.js → missions-C3EC4uKT.js} +1 -1
- package/ui/dist/assets/{network-v9O7MmZT.js → network-ephW7nah.js} +1 -1
- package/ui/dist/assets/{pause-CZo33QlA.js → pause-DeggUVZc.js} +1 -1
- package/ui/dist/assets/{play-DbCJhc5H.js → play-Dfc0ia6v.js} +1 -1
- package/ui/dist/assets/{plug-CtpCZg2z.js → plug-Bl_6i_9u.js} +1 -1
- package/ui/dist/assets/{plus-O61rB_YF.js → plus-CQwCh7WY.js} +1 -1
- package/ui/dist/assets/{proxy-BAUTNyg6.js → proxy-BjQ4lsww.js} +1 -1
- package/ui/dist/assets/{rotate-ccw-n5WkWR51.js → rotate-ccw-CUuN2ebi.js} +1 -1
- package/ui/dist/assets/{save-D2Qh8KyN.js → save-PlTUvsN5.js} +1 -1
- package/ui/dist/assets/{search-C9SdcZJq.js → search-BHgLDT-j.js} +1 -1
- package/ui/dist/assets/{send-DZ_5M5QQ.js → send-pflQNBoT.js} +1 -1
- package/ui/dist/assets/{shield-check-DyP5PmaY.js → shield-check-CrD-s89i.js} +1 -1
- package/ui/dist/assets/{target-CWfn705s.js → target-D4oh8GME.js} +1 -1
- package/ui/dist/assets/{terminal-CdhfA0Kr.js → terminal-BvJZ1R3x.js} +1 -1
- package/ui/dist/assets/{toggle-right-CmYQLQRS.js → toggle-right-Dl7x_ed7.js} +1 -1
- package/ui/dist/assets/{trash-2-TQcEJEv4.js → trash-2-lPcFwnW5.js} +1 -1
- package/ui/dist/assets/{trending-up-Cw22bM4J.js → trending-up-DqUbZfDe.js} +1 -1
- package/ui/dist/assets/{trophy-DUF8Bcas.js → trophy-Dv6zFfKM.js} +1 -1
- package/ui/dist/assets/{users-BX_ZaByZ.js → users-NPyAWbs5.js} +1 -1
- package/ui/dist/assets/{wrench-Cfp7nYhG.js → wrench-BCKgeHj3.js} +1 -1
- package/ui/dist/index.html +2 -2
- package/ui/dist/sw.js +1 -1
- package/ui/dist/assets/BackupPanel-BNxMlNIP.js +0 -1
- package/ui/dist/assets/CPActivity-CjIgehOq.js +0 -1
- package/ui/dist/assets/CPAgentDetail-oaymYRNI.js +0 -1
- package/ui/dist/assets/CPAgents-CWQ6iJfO.js +0 -1
- package/ui/dist/assets/CPApprovals-DzATBhtM.js +0 -1
- package/ui/dist/assets/CPCosts-xVQR148_.js +0 -1
- package/ui/dist/assets/CPDashboard-Dt6Jr7Zw.js +0 -1
- package/ui/dist/assets/CPGoals-Bt1a3C9T.js +0 -1
- package/ui/dist/assets/CPIssueDetail-DHxqtiuh.js +0 -1
- package/ui/dist/assets/CPIssues-D0l3fgUs.js +0 -1
- package/ui/dist/assets/CPOrg-DQ3tbYld.js +0 -8
- package/ui/dist/assets/CPRuns-0vncicUW.js +0 -1
- package/ui/dist/assets/ChannelsPanel-CiJs4gyw.js +0 -1
- package/ui/dist/assets/FleetPanel-Bg-f3_ip.js +0 -1
- package/ui/dist/assets/InfraView-CHYaZqMK.js +0 -2
- package/ui/dist/assets/InlineEditableField-BOLRKQq1.js +0 -1
- package/ui/dist/assets/Input-4pNffAw-.js +0 -1
- package/ui/dist/assets/IntegrationsPanel-CFucvr-Q.js +0 -1
- package/ui/dist/assets/IntelligenceView-Dh-Wuf13.js +0 -2
- package/ui/dist/assets/LearningPanel-CtlougkF.js +0 -1
- package/ui/dist/assets/McpPanel-BobqpLsh.js +0 -1
- package/ui/dist/assets/MemoryWikiPanel-BfYksjoI.js +0 -11
- package/ui/dist/assets/MissionCanvas-0V_vtqiu.js +0 -50
- package/ui/dist/assets/MissionChat-9hGhc9Zw.js +0 -7
- package/ui/dist/assets/MissionLibrary-dFkT8pGp.js +0 -5
- package/ui/dist/assets/MissionStart-CZSEie0Q.js +0 -1
- package/ui/dist/assets/Modal-V2yNnkKk.js +0 -1
- package/ui/dist/assets/NvidiaPanel-qLJysI9M.js +0 -1
- package/ui/dist/assets/OverviewPanel-Be6LukJ_.js +0 -1
- package/ui/dist/assets/RecipesPanel-DDYOCx3w.js +0 -1
- package/ui/dist/assets/SelfImprovePanel-B8o_fO30.js +0 -1
- package/ui/dist/assets/SessionsPanel-DeCBkSgY.js +0 -1
- package/ui/dist/assets/SettingsPanel-BlsRDAt7.js +0 -1
- package/ui/dist/assets/SettingsView-Dsa7Zr4e.js +0 -2
- package/ui/dist/assets/SkeletonLoader-BkQoZllJ.js +0 -1
- package/ui/dist/assets/StatusBadge-BFEHCdgi.js +0 -1
- package/ui/dist/assets/Tabs-g2Gs9sK0.js +0 -1
- package/ui/dist/assets/TelemetryPanel-DXAGuV0R.js +0 -1
- package/ui/dist/assets/TitanCanvas-DCl3gxSl.js +0 -1072
- package/ui/dist/assets/ToolsView-CnUjq0Js.js +0 -2
- package/ui/dist/assets/Tooltip-DJ99YlFF.js +0 -6
- package/ui/dist/assets/TrainingPanel-BLf_KMaE.js +0 -1
- package/ui/dist/assets/eye-off-CYR4nH5I.js +0 -6
- package/ui/dist/assets/index-PwwKjgT3.css +0 -1
- package/ui/dist/assets/index-slWylU2o.js +0 -244
- package/ui/dist/assets/loader-circle-E8aLxmMz.js +0 -6
|
@@ -4,8 +4,14 @@ import { join, resolve } from "path";
|
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
import { registerSkill } from "../registry.js";
|
|
6
6
|
import { resolveImageReferences } from "../../agent/imageRegistry.js";
|
|
7
|
+
import {
|
|
8
|
+
verifyFileWrite,
|
|
9
|
+
recordVerificationEvent,
|
|
10
|
+
formatVerificationSuffix
|
|
11
|
+
} from "../../agent/verification.js";
|
|
7
12
|
const BLOCKED_PATHS = ["/etc", "/root", "/sys", "/proc", "/dev", "/boot", "/var/log", "/var/run"];
|
|
8
13
|
const BLOCKED_PATTERNS = [".ssh", ".gnupg", ".aws", ".env", "id_rsa", "id_ed25519", ".netrc", ".npmrc"];
|
|
14
|
+
const MAX_DIR_ENTRIES = 5e4;
|
|
9
15
|
function expandPath(filePath) {
|
|
10
16
|
const expanded = filePath.startsWith("~/") ? join(homedir(), filePath.slice(2)) : filePath;
|
|
11
17
|
return resolve(expanded);
|
|
@@ -15,12 +21,8 @@ function isWithinDir(child, parent) {
|
|
|
15
21
|
const parentWithSep = parent.endsWith("/") ? parent : parent + "/";
|
|
16
22
|
return child.startsWith(parentWithSep);
|
|
17
23
|
}
|
|
18
|
-
function
|
|
24
|
+
function getPathBlockReason(filePath) {
|
|
19
25
|
const resolved = expandPath(filePath);
|
|
20
|
-
const home = homedir();
|
|
21
|
-
if (!isWithinDir(resolved, home) && !isWithinDir(resolved, "/tmp")) {
|
|
22
|
-
return `Access denied: path must be within home directory or /tmp`;
|
|
23
|
-
}
|
|
24
26
|
for (const pattern of BLOCKED_PATTERNS) {
|
|
25
27
|
if (resolved.includes(pattern)) {
|
|
26
28
|
return `Access denied: cannot access ${pattern} files`;
|
|
@@ -33,6 +35,17 @@ function validatePath(filePath) {
|
|
|
33
35
|
}
|
|
34
36
|
return null;
|
|
35
37
|
}
|
|
38
|
+
function isPathBlocked(filePath) {
|
|
39
|
+
return getPathBlockReason(filePath) !== null;
|
|
40
|
+
}
|
|
41
|
+
function validatePath(filePath) {
|
|
42
|
+
const resolved = expandPath(filePath);
|
|
43
|
+
const home = homedir();
|
|
44
|
+
if (!isWithinDir(resolved, home) && !isWithinDir(resolved, "/tmp")) {
|
|
45
|
+
return `Access denied: path must be within home directory or /tmp`;
|
|
46
|
+
}
|
|
47
|
+
return getPathBlockReason(resolved);
|
|
48
|
+
}
|
|
36
49
|
const READ_FILE_MAX_BYTES = (() => {
|
|
37
50
|
const v = process.env.TITAN_READ_FILE_MAX_BYTES;
|
|
38
51
|
const n = v ? parseInt(v, 10) : NaN;
|
|
@@ -186,7 +199,10 @@ NEVER output the file content as a plain code block in the chat when the user as
|
|
|
186
199
|
}
|
|
187
200
|
}
|
|
188
201
|
writeFileSync(filePath, newContent, "utf-8");
|
|
189
|
-
|
|
202
|
+
const written = Buffer.byteLength(newContent, "utf-8");
|
|
203
|
+
const result = verifyFileWrite(filePath, written, newContent);
|
|
204
|
+
recordVerificationEvent({ kind: "file_write", result, artifactRef: filePath });
|
|
205
|
+
return `Successfully wrote ${newContent.length} bytes to ${filePath}${formatVerificationSuffix(result)}`;
|
|
190
206
|
} catch (e) {
|
|
191
207
|
return `Error writing file: ${e.message}`;
|
|
192
208
|
}
|
|
@@ -230,8 +246,12 @@ Errors: same set as write_file (EACCES, BLOCKED: sensitive path, ENOSPC).`,
|
|
|
230
246
|
const dir = join(filePath, "..");
|
|
231
247
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
232
248
|
const resolved = resolveImageReferences(args.content);
|
|
249
|
+
const preSize = existsSync(filePath) ? statSync(filePath).size : 0;
|
|
233
250
|
appendFileSync(filePath, resolved, "utf-8");
|
|
234
|
-
|
|
251
|
+
const appended = Buffer.byteLength(resolved, "utf-8");
|
|
252
|
+
const result = verifyFileWrite(filePath, preSize + appended);
|
|
253
|
+
recordVerificationEvent({ kind: "file_append", result, artifactRef: filePath });
|
|
254
|
+
return `Successfully appended ${appended} bytes to ${filePath}${formatVerificationSuffix(result)}`;
|
|
235
255
|
} catch (e) {
|
|
236
256
|
return `Error appending to file: ${e.message}`;
|
|
237
257
|
}
|
|
@@ -315,7 +335,13 @@ NEVER guess at the target \u2014 read_file first, always.`,
|
|
|
315
335
|
const after = contentLines.slice(endIdx + 1).join("\n");
|
|
316
336
|
content = before + (before ? "\n" : "") + args.replacement + (after ? "\n" : "") + after;
|
|
317
337
|
writeFileSync(filePath, content, "utf-8");
|
|
318
|
-
|
|
338
|
+
const fuzzResult = verifyFileWrite(
|
|
339
|
+
filePath,
|
|
340
|
+
Buffer.byteLength(content, "utf-8"),
|
|
341
|
+
content
|
|
342
|
+
);
|
|
343
|
+
recordVerificationEvent({ kind: "file_edit", result: fuzzResult, artifactRef: filePath });
|
|
344
|
+
return `Successfully edited ${filePath} (fuzzy whitespace match applied)${formatVerificationSuffix(fuzzResult)}`;
|
|
319
345
|
}
|
|
320
346
|
}
|
|
321
347
|
const targetWords = target.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
|
|
@@ -336,7 +362,13 @@ TIP: Use read_file with startLine/endLine to get the exact text, then copy it pr
|
|
|
336
362
|
const resolvedReplacement = resolveImageReferences(args.replacement);
|
|
337
363
|
content = content.split(target).join(resolvedReplacement);
|
|
338
364
|
writeFileSync(filePath, content, "utf-8");
|
|
339
|
-
|
|
365
|
+
const editResult = verifyFileWrite(
|
|
366
|
+
filePath,
|
|
367
|
+
Buffer.byteLength(content, "utf-8"),
|
|
368
|
+
content
|
|
369
|
+
);
|
|
370
|
+
recordVerificationEvent({ kind: "file_edit", result: editResult, artifactRef: filePath });
|
|
371
|
+
return `Successfully edited ${filePath}${formatVerificationSuffix(editResult)}`;
|
|
340
372
|
} catch (e) {
|
|
341
373
|
return `Error editing file: ${e.message}`;
|
|
342
374
|
}
|
|
@@ -380,18 +412,35 @@ Errors:
|
|
|
380
412
|
if (dirErr) return dirErr;
|
|
381
413
|
if (!existsSync(dirPath)) return `Error: Directory not found: ${dirPath}`;
|
|
382
414
|
try {
|
|
383
|
-
const
|
|
384
|
-
const lines =
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
415
|
+
const recursive = args.recursive === true;
|
|
416
|
+
const lines = [];
|
|
417
|
+
let truncated = false;
|
|
418
|
+
const addEntries = (currentPath, prefix = "") => {
|
|
419
|
+
const entries = readdirSync(currentPath, { withFileTypes: true });
|
|
420
|
+
for (const entry of entries) {
|
|
421
|
+
if (lines.length >= MAX_DIR_ENTRIES) {
|
|
422
|
+
truncated = true;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const fullPath = join(currentPath, entry.name);
|
|
426
|
+
if (isPathBlocked(fullPath)) continue;
|
|
427
|
+
const displayName = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
428
|
+
if (entry.isDirectory()) {
|
|
429
|
+
lines.push(`\u{1F4C1} ${displayName}/`);
|
|
430
|
+
if (recursive) addEntries(fullPath, displayName);
|
|
431
|
+
if (truncated) return;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const stat = statSync(fullPath);
|
|
435
|
+
const size = stat.size < 1024 ? `${stat.size}B` : stat.size < 1048576 ? `${(stat.size / 1024).toFixed(1)}KB` : `${(stat.size / 1048576).toFixed(1)}MB`;
|
|
436
|
+
lines.push(`\u{1F4C4} ${displayName} (${size})`);
|
|
388
437
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
return `Directory: ${dirPath}
|
|
394
|
-
${lines.join("\n")}`;
|
|
438
|
+
};
|
|
439
|
+
addEntries(dirPath);
|
|
440
|
+
const suffix = truncated ? `
|
|
441
|
+
... [truncated at ${MAX_DIR_ENTRIES} entries]` : "";
|
|
442
|
+
return `Directory: ${dirPath}${recursive ? " (recursive)" : ""}
|
|
443
|
+
${lines.join("\n")}${suffix}`;
|
|
395
444
|
} catch (e) {
|
|
396
445
|
return `Error listing directory: ${e.message}`;
|
|
397
446
|
}
|
|
@@ -472,6 +521,7 @@ ${content}`;
|
|
|
472
521
|
);
|
|
473
522
|
}
|
|
474
523
|
export {
|
|
524
|
+
isPathBlocked,
|
|
475
525
|
isWithinDir,
|
|
476
526
|
registerFilesystemSkill,
|
|
477
527
|
validatePath
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/skills/builtin/filesystem.ts"],"sourcesContent":["/**\n * TITAN — Filesystem Skill (Built-in)\n * Read, write, edit, and list files and directories.\n */\nimport { readFileSync, writeFileSync, existsSync, readdirSync, statSync, mkdirSync, openSync, readSync, closeSync } from 'fs';\nimport { join, resolve } from 'path';\nimport { homedir } from 'os';\nimport { registerSkill } from '../registry.js';\nimport { resolveImageReferences } from '../../agent/imageRegistry.js';\n\n/** S4: Block access to sensitive system paths */\nconst BLOCKED_PATHS = ['/etc', '/root', '/sys', '/proc', '/dev', '/boot', '/var/log', '/var/run'];\nconst BLOCKED_PATTERNS = ['.ssh', '.gnupg', '.aws', '.env', 'id_rsa', 'id_ed25519', '.netrc', '.npmrc'];\n\n/** Expand ~ and resolve to absolute path */\nfunction expandPath(filePath: string): string {\n const expanded = filePath.startsWith('~/') ? join(homedir(), filePath.slice(2)) : filePath;\n return resolve(expanded);\n}\n\n/**\n * Hunt Finding #32 (2026-04-14): helper to check if `child` is the same as\n * or contained within `parent` on the filesystem, WITHOUT the `startsWith`\n * trap. A naive `child.startsWith(parent)` returns true for siblings that\n * share a prefix (e.g. `/tmpfoo` starts with `/tmp`, `/home/djacob` starts\n * with `/home/dj`). We require either exact match or a path-separator\n * boundary after `parent`.\n */\nexport function isWithinDir(child: string, parent: string): boolean {\n if (child === parent) return true;\n // Ensure trailing separator on parent so /tmpfoo doesn't match /tmp\n const parentWithSep = parent.endsWith('/') ? parent : parent + '/';\n return child.startsWith(parentWithSep);\n}\n\nexport function validatePath(filePath: string): string | null {\n const resolved = expandPath(filePath);\n const home = homedir();\n // Allow access within home directory and /tmp only.\n // Hunt Finding #32: must check path-separator boundary, not raw startsWith.\n if (!isWithinDir(resolved, home) && !isWithinDir(resolved, '/tmp')) {\n return `Access denied: path must be within home directory or /tmp`;\n }\n // Block sensitive paths even within home\n for (const pattern of BLOCKED_PATTERNS) {\n if (resolved.includes(pattern)) {\n return `Access denied: cannot access ${pattern} files`;\n }\n }\n // Block system directories\n for (const blocked of BLOCKED_PATHS) {\n if (isWithinDir(resolved, blocked)) {\n return `Access denied: cannot access system directory ${blocked}`;\n }\n }\n return null; // valid\n}\n\n/**\n * Hunt Finding #36 (2026-04-14): max bytes returned by read_file in a single\n * call. A Phase 5.5 test read a 1MB file and the full content got pumped into\n * the model context, exploding it to 213K tokens and driving the model into\n * 21-tool-call pathological exploration before returning a hallucinated\n * wrong answer. Now: files over this threshold return a preview + stats +\n * usage hint; the caller can use byteOffset/byteLimit to page through.\n * Tunable via env var TITAN_READ_FILE_MAX_BYTES.\n */\nconst READ_FILE_MAX_BYTES = (() => {\n const v = process.env.TITAN_READ_FILE_MAX_BYTES;\n const n = v ? parseInt(v, 10) : NaN;\n if (Number.isFinite(n) && n > 0 && n <= 10_000_000) return n;\n return 100_000; // 100 KB default — enough for most source files, small enough to not blow context\n})();\n\n/** Read the first N bytes of a file without loading the whole thing. */\nfunction readFirstBytes(filePath: string, maxBytes: number): string {\n const fd = openSync(filePath, 'r');\n try {\n const buf = Buffer.alloc(maxBytes);\n const bytesRead = readSync(fd, buf, 0, maxBytes, 0);\n return buf.subarray(0, bytesRead).toString('utf-8');\n } finally {\n closeSync(fd);\n }\n}\n\nexport function registerFilesystemSkill(): void {\n // Read File\n registerSkill(\n { name: 'read_file', description: 'Read file contents', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'read_file',\n description: `Read a file from the local filesystem and return its contents prefixed with 1-based line numbers (cat -n format). Use this before any edit so you can quote exact lines back to edit_file.\n\nUSE WHEN: \"read X\" / \"show me X\" / \"what's in X\" / \"open X\" / \"check X\" / \"look at X\" — anything where the answer comes from a local file. Also: ALWAYS as the first step before edit_file or append_file on an existing file.\n\nDO NOT USE FOR:\n- Listing a directory → use list_dir\n- Reading a URL → use web_fetch\n- Reading a file inside an archive → unzip first via shell, then read_file\n\nParameters:\n- path (string, required) — absolute or ~-prefixed path. Relative paths resolve from cwd.\n- startLine (number, optional, 1-indexed) — first line to return; defaults to 1.\n- endLine (number, optional, 1-indexed inclusive) — last line; defaults to file end or +2000 lines if very large.\n\nReturns: { content: string, path: string, totalLines: number, truncated: boolean, sizeBytes: number }. content is the line-numbered text. truncated=true means the file is bigger than the returned window; call again with a higher startLine/endLine to read the rest.\n\nErrors:\n- \"ENOENT: file not found\" — verify the path; the user may have given a typo. Try list_dir on the parent directory first.\n- \"EACCES: permission denied\" — TITAN can't read the file; surface to the user, suggest chmod or running TITAN with the right user.\n- \"BLOCKED: sensitive path\" — file matched a blocklist pattern (.ssh, .env, /etc, etc.). Do not retry; tell the user this path is off-limits.\n\nNEVER hallucinate file content. If read_file failed, say so — do not fabricate.`,\n parameters: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'Absolute or relative path to the file' },\n startLine: { type: 'number', description: 'Start line (1-indexed, optional)' },\n endLine: { type: 'number', description: 'End line (1-indexed, optional)' },\n },\n required: ['path'],\n },\n execute: async (args) => {\n const filePath = expandPath(args.path as string);\n const pathErr = validatePath(filePath);\n if (pathErr) return pathErr;\n if (!existsSync(filePath)) return `Error: File not found: ${filePath}`;\n try {\n // Hunt Finding #36 (2026-04-14): check file size BEFORE reading\n // the full content. Previous version called readFileSync\n // unconditionally and a 1MB file exploded context to 213K\n // tokens and drove the model into 21-call pathological\n // exploration before hallucinating a wrong answer.\n const stat = statSync(filePath);\n const fileSize = stat.size;\n const oversized = fileSize > READ_FILE_MAX_BYTES;\n\n // If the caller supplied startLine/endLine they're opting\n // into scoped reads — let them read even from a large file,\n // but still cap the total bytes returned.\n const rawStart = args.startLine as number | undefined;\n const rawEnd = args.endLine as number | undefined;\n const hasScope = (rawStart !== undefined) || (rawEnd !== undefined);\n\n if (oversized && !hasScope) {\n // Return a preview of the first READ_FILE_MAX_BYTES\n // plus file stats + a usage hint. This keeps context\n // bounded and points the model at the right next action.\n const preview = readFirstBytes(filePath, READ_FILE_MAX_BYTES);\n const previewLines = preview.split('\\n');\n const humanSize = fileSize >= 1_000_000\n ? `${(fileSize / 1_000_000).toFixed(2)} MB`\n : `${(fileSize / 1_000).toFixed(1)} KB`;\n return [\n `File: ${filePath}`,\n `Size: ${humanSize} (${fileSize} bytes) — TRUNCATED`,\n `Showing first ${READ_FILE_MAX_BYTES} bytes (~${previewLines.length} lines). The file is too large to load in full.`,\n `To read more, call read_file again with startLine/endLine parameters.`,\n `---`,\n ...previewLines.slice(0, 500).map((l, i) => `${i + 1}: ${l}`),\n ].join('\\n');\n }\n\n const content = readFileSync(filePath, 'utf-8');\n const lines = content.split('\\n');\n const start = rawStart || 1;\n const end = rawEnd || lines.length;\n const selected = lines.slice(start - 1, end);\n\n // Even with scoped reads, cap the returned size so a file\n // with one million characters on a single line doesn't\n // blow up context through the startLine=1,endLine=1 path.\n const output = `File: ${filePath} (${lines.length} lines)\\n---\\n${selected.map((l, i) => `${start + i}: ${l}`).join('\\n')}`;\n if (output.length > READ_FILE_MAX_BYTES * 2) {\n return output.slice(0, READ_FILE_MAX_BYTES * 2) +\n `\\n\\n... [output truncated: ${output.length - READ_FILE_MAX_BYTES * 2} bytes omitted. Use narrower startLine/endLine to paginate.]`;\n }\n return output;\n } catch (e) { return `Error reading file: ${(e as Error).message}`; }\n },\n },\n );\n\n // Write File\n registerSkill(\n { name: 'write_file', description: 'Write/create a file', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'write_file',\n description: `Write a file to the local filesystem (creates parent directories as needed). Overwrites any existing file at the path. A shadow-git snapshot is taken automatically before the write, so an unwanted overwrite can be rolled back via checkpoint_restore.\n\nUSE WHEN: \"create file X\" / \"write X to a file\" / \"save this to X\" / \"make a file called X\" / \"create X with this content\" — anything where the deliverable is a new file or a full rewrite.\n\nDO NOT USE FOR:\n- Modifying an existing file → read_file first, then edit_file (preserves surrounding content, atomic).\n- Appending to a log → use append_file (no overwrite).\n- Creating files in /etc, ~/.ssh, or .env paths → BLOCKED by the path guard; surface to the user.\n\nParameters:\n- path (string, required) — absolute or ~-prefixed.\n- content (string, required) — exact bytes to write. Use \\\\n for newlines; do not write \\\\r\\\\n unless the user explicitly asks for Windows line endings.\n\nReturns: { path: string, bytesWritten: number, created: boolean, checkpointId: string }. created=true means the file didn't exist before. checkpointId can be passed to checkpoint_restore to revert.\n\nErrors:\n- \"EACCES: permission denied\" — directory not writable; suggest chmod or a different path.\n- \"BLOCKED: sensitive path\" — write rejected by the path guard. Do not retry.\n- \"ENOSPC: no space left\" — disk full; surface to the user, do not silently succeed.\n\nNEVER output the file content as a plain code block in the chat when the user asked to write a file — call write_file. Outputting raw content is the #1 wrong-tool failure mode.`,\n parameters: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'Path to the file' },\n content: { type: 'string', description: 'Content to write' },\n },\n required: ['path', 'content'],\n },\n execute: async (args) => {\n const filePath = expandPath(args.path as string);\n const pathErr = validatePath(filePath);\n if (pathErr) return pathErr;\n try {\n const dir = join(filePath, '..');\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n // v6.1.0-alpha.56 — substitute `tdi://hash` reference\n // tokens from download_image with their real data\n // URLs RIGHT before writing to disk. The LLM never\n // had to handle the full base64 in its context.\n const newContent = resolveImageReferences(args.content as string);\n // Guard: prevent destructive overwrites of existing files.\n // Use newContent.length-vs-existing.length ratio rather\n // than line count, because resolved data URLs span one\n // very long line and would trigger false-positive\n // \"destructive expansion\" warnings on the line-count check.\n if (existsSync(filePath)) {\n const existing = readFileSync(filePath, 'utf-8');\n const existingLines = existing.split('\\n').length;\n const newLines = newContent.split('\\n').length;\n const containsDataUrls = /data:image\\/[^;]+;base64,/.test(newContent);\n if (!containsDataUrls) {\n if (existingLines > 20 && newLines < existingLines * 0.4) {\n return `Error: write_file would replace ${existingLines} lines with only ${newLines} lines (${Math.round(newLines / existingLines * 100)}% of original). This looks destructive. Use edit_file to make surgical changes instead of rewriting the entire file.`;\n }\n if (existingLines > 20 && newLines > existingLines * 3) {\n return `Error: write_file would expand ${existingLines} lines to ${newLines} lines (${Math.round(newLines / existingLines * 100)}% of original). This looks like accidental file duplication. Use edit_file to make targeted changes instead of rewriting the entire file.`;\n }\n }\n }\n writeFileSync(filePath, newContent, 'utf-8');\n return `Successfully wrote ${newContent.length} bytes to ${filePath}`;\n } catch (e) { return `Error writing file: ${(e as Error).message}`; }\n },\n },\n );\n\n // Append to File\n registerSkill(\n { name: 'append_file', description: 'Append content to end of a file', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'append_file',\n description: `Append content to the end of an existing file (creates it if missing). Atomic + idempotent on retry only when the content already ends in a newline — otherwise repeated calls duplicate the last line. Use this for building files incrementally (logs, accumulating reports, HTML section-by-section) without the round-trip cost of read_file + write_file.\n\nUSE WHEN: \"append X to Y\" / \"add X to the bottom of Y\" / \"log X\" / when iteratively building a large file (write_file the skeleton, then append_file each section).\n\nDO NOT USE FOR:\n- Inserting into the middle of a file → use edit_file (find-and-replace).\n- Overwriting → use write_file.\n- Appending to a structured file (JSON, YAML) where the result must remain valid — append_file is byte-level and won't keep JSON arrays valid.\n\nParameters:\n- path (string, required) — absolute or ~-prefixed.\n- content (string, required) — exact bytes to append. End with \\\\n if you want a trailing newline.\n\nReturns: { path: string, bytesAppended: number, totalBytes: number, created: boolean }.\n\nErrors: same set as write_file (EACCES, BLOCKED: sensitive path, ENOSPC).`,\n parameters: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'Path to the file' },\n content: { type: 'string', description: 'Content to append' },\n },\n required: ['path', 'content'],\n },\n execute: async (args) => {\n const filePath = expandPath(args.path as string);\n const pathErr = validatePath(filePath);\n if (pathErr) return pathErr;\n try {\n const { appendFileSync } = await import('fs');\n const dir = join(filePath, '..');\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n // v6.1.0-alpha.56 — resolve tdi:// image references before append.\n const resolved = resolveImageReferences(args.content as string);\n appendFileSync(filePath, resolved, 'utf-8');\n return `Successfully appended ${resolved.length} bytes to ${filePath}`;\n } catch (e) { return `Error appending to file: ${(e as Error).message}`; }\n },\n },\n );\n\n // Edit File\n registerSkill(\n { name: 'edit_file', description: 'Search and replace in a file', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'edit_file',\n description: `Edit a file by replacing one exact string with another (surgical, atomic). Preserves all surrounding content. Shadow-git checkpoints before and after, so any edit is rollback-able.\n\nUSE WHEN: \"edit X\" / \"change X to Y in the file\" / \"update X\" / \"fix X in the file\" / \"modify X\" / \"rename foo to bar in X\" — anything where you want a precise change without rewriting the whole file.\n\nWORKFLOW (non-negotiable):\n1. Call read_file first to see the current content.\n2. Copy the exact target string from the read_file output (whitespace, indentation, line endings all matter).\n3. Call edit_file with target + replacement.\n\nDO NOT USE FOR:\n- A full rewrite → use write_file (it's faster).\n- A target string that occurs more than once in the file → split it into two edits, each with enough surrounding context to be unique.\n- A target you haven't seen via read_file → blind edits routinely fail with \"target not found\".\n\nParameters:\n- path (string, required) — absolute or ~-prefixed.\n- target (string, required) — exact text to find. Must match byte-for-byte AND be unique in the file.\n- replacement (string, required) — text to substitute. Can be longer or shorter than the target.\n\nReturns: { path: string, bytesBefore: number, bytesAfter: number, occurrencesReplaced: number, checkpointId: string }.\n\nErrors:\n- \"Target string not found\" — your target doesn't match the file content. Call read_file again, copy more carefully (mind tabs vs spaces, trailing whitespace).\n- \"Target string is not unique (N matches)\" — add more surrounding context to the target until it's unique, OR call edit_file once per occurrence with disambiguating context.\n- Same EACCES/BLOCKED errors as write_file.\n\nNEVER guess at the target — read_file first, always.`,\n parameters: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'Path to the file' },\n target: { type: 'string', description: 'Exact string to find and replace' },\n replacement: { type: 'string', description: 'Replacement content' },\n },\n required: ['path', 'target', 'replacement'],\n },\n execute: async (args) => {\n const filePath = expandPath(args.path as string);\n const pathErr2 = validatePath(filePath);\n if (pathErr2) return pathErr2;\n if (!existsSync(filePath)) return `Error: File not found: ${filePath}`;\n try {\n let content = readFileSync(filePath, 'utf-8');\n const target = args.target as string;\n if (!content.includes(target)) {\n // Fuzzy matching: try to find the closest matching block\n const _targetLines = target.split('\\n').map((l: string) => l.trim()).filter(Boolean);\n const contentLines = content.split('\\n');\n\n // Try normalized whitespace match first\n const normalizedTarget = target.replace(/\\s+/g, ' ').trim();\n const normalizedContent = content.replace(/\\s+/g, ' ').trim();\n if (normalizedContent.includes(normalizedTarget)) {\n // Whitespace-only difference — find and replace with original whitespace context\n const targetChunks = target.split('\\n');\n const firstLine = targetChunks[0].trim();\n const lastLine = targetChunks[targetChunks.length - 1].trim();\n let startIdx = -1, endIdx = -1;\n for (let i = 0; i < contentLines.length; i++) {\n if (contentLines[i].trim() === firstLine) { startIdx = i; break; }\n }\n if (startIdx >= 0) {\n for (let i = startIdx; i < contentLines.length; i++) {\n if (contentLines[i].trim() === lastLine) { endIdx = i; break; }\n }\n }\n if (startIdx >= 0 && endIdx >= 0) {\n const before = contentLines.slice(0, startIdx).join('\\n');\n const after = contentLines.slice(endIdx + 1).join('\\n');\n content = before + (before ? '\\n' : '') + (args.replacement as string) + (after ? '\\n' : '') + after;\n writeFileSync(filePath, content, 'utf-8');\n return `Successfully edited ${filePath} (fuzzy whitespace match applied)`;\n }\n }\n\n // Line-by-line similarity: find best matching region\n const targetWords = target.toLowerCase().split(/\\s+/).filter((w: string) => w.length > 3);\n let bestLine = -1, bestScore = 0;\n for (let i = 0; i < contentLines.length; i++) {\n const score = targetWords.filter((w: string) => contentLines[i].toLowerCase().includes(w)).length;\n if (score > bestScore) { bestScore = score; bestLine = i; }\n }\n const nearby = bestLine >= 0\n ? contentLines.slice(Math.max(0, bestLine - 3), bestLine + 4).map((l: string, i: number) => ` ${Math.max(1, bestLine - 2) + i}: ${l}`).join('\\n')\n : contentLines.slice(0, 10).map((l: string, i: number) => ` ${i + 1}: ${l}`).join('\\n');\n return `Error: Target string not found in ${filePath}. The target must match EXACTLY (including whitespace/indentation). Closest match area:\\n${nearby}\\n\\nTIP: Use read_file with startLine/endLine to get the exact text, then copy it precisely as the target.`;\n }\n // v6.1.0-alpha.56 — resolve tdi:// references in the\n // replacement before splicing so the LLM can paste a\n // download_image token into edit_file's replacement\n // and get the full data URL on disk.\n const resolvedReplacement = resolveImageReferences(args.replacement as string);\n content = content.split(target).join(resolvedReplacement);\n writeFileSync(filePath, content, 'utf-8');\n return `Successfully edited ${filePath}`;\n } catch (e) { return `Error editing file: ${(e as Error).message}`; }\n },\n },\n );\n\n // List Directory\n registerSkill(\n { name: 'list_dir', description: 'List directory contents', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'list_dir',\n description: `List the contents of a directory with file sizes and types (file vs directory). One-level by default; set recursive:true only on small trees.\n\nUSE WHEN: \"list files in X\" / \"what's in the X folder\" / \"show me X directory\" / \"ls X\" / \"what files are in X\" / \"is there a Y file here\" — anything where the answer comes from filesystem structure.\n\nDO NOT USE FOR:\n- Listing a huge tree (node_modules, ~/Library) → use recursive:false then drill in manually, or shell + find -maxdepth.\n- File contents → use read_file.\n- Searching by name → use shell with find or grep -r.\n\nParameters:\n- path (string, required) — absolute or ~-prefixed. Path to the directory.\n- recursive (boolean, optional) — default false. true returns a flat tree of every file under path. Capped at MAX_DIR_ENTRIES (50_000) to prevent runaway scans.\n\nReturns: { path: string, entries: Array<{ name, type: \"file\"|\"directory\", sizeBytes, modifiedAt }>, count: number, truncated: boolean }. truncated=true means MAX_DIR_ENTRIES was hit; narrow the path or use recursive:false.\n\nErrors:\n- \"ENOENT: directory not found\" — verify the path; suggest list_dir on the parent.\n- \"ENOTDIR: not a directory\" — caller passed a file path; use read_file instead.\n- \"BLOCKED: sensitive path\" — path is in the blocklist (/etc, ~/.ssh, etc.). Do not retry.`,\n parameters: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'Path to the directory' },\n recursive: { type: 'boolean', description: 'List recursively (default: false)' },\n },\n required: ['path'],\n },\n execute: async (args) => {\n const dirPath = expandPath(args.path as string);\n const dirErr = validatePath(dirPath);\n if (dirErr) return dirErr;\n if (!existsSync(dirPath)) return `Error: Directory not found: ${dirPath}`;\n try {\n const entries = readdirSync(dirPath, { withFileTypes: true });\n const lines = entries.map((entry) => {\n const fullPath = join(dirPath, entry.name);\n if (entry.isDirectory()) {\n return `📁 ${entry.name}/`;\n }\n const stat = statSync(fullPath);\n const size = stat.size < 1024 ? `${stat.size}B` : stat.size < 1048576 ? `${(stat.size / 1024).toFixed(1)}KB` : `${(stat.size / 1048576).toFixed(1)}MB`;\n return `📄 ${entry.name} (${size})`;\n });\n return `Directory: ${dirPath}\\n${lines.join('\\n')}`;\n } catch (e) { return `Error listing directory: ${(e as Error).message}`; }\n },\n },\n );\n\n // List Uploaded Files\n registerSkill(\n { name: 'list_uploads', description: 'List uploaded files for a session', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'list_uploads',\n description: 'List files uploaded by the user in the current session.\\n\\nUSE THIS WHEN user mentions \"my file\", \"the file I uploaded\", \"uploaded document\", or wants to work with an attachment.',\n parameters: {\n type: 'object',\n properties: {\n session: { type: 'string', description: 'Session ID (defaults to \"default\")' },\n },\n },\n execute: async (args) => {\n const uploadsDir = join(homedir(), '.titan', 'uploads', (args.session as string) || 'default');\n if (!existsSync(uploadsDir)) return 'No files uploaded yet in this session.';\n try {\n const entries = readdirSync(uploadsDir);\n if (entries.length === 0) return 'No files uploaded yet in this session.';\n const lines = entries.map(name => {\n const stat = statSync(join(uploadsDir, name));\n const size = stat.size < 1024 ? `${stat.size}B` : `${(stat.size / 1024).toFixed(1)}KB`;\n return `📎 ${name} (${size})`;\n });\n return `Uploaded files:\\n${lines.join('\\n')}`;\n } catch (e) { return `Error: ${(e as Error).message}`; }\n },\n },\n );\n\n // Read Uploaded File\n registerSkill(\n { name: 'read_upload', description: 'Read an uploaded file', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'read_upload',\n description: 'Read the contents of a file uploaded by the user.\\n\\nUSE THIS WHEN user asks about the contents of a file they uploaded, or says \"read my file\", \"what does the file say\", \"analyze this document\".\\n\\nSupports: text, CSV, JSON, and other text-based formats. For binary files (PDF, images), returns metadata.',\n parameters: {\n type: 'object',\n properties: {\n filename: { type: 'string', description: 'Name of the uploaded file' },\n session: { type: 'string', description: 'Session ID (defaults to \"default\")' },\n },\n required: ['filename'],\n },\n execute: async (args) => {\n const uploadsDir = join(homedir(), '.titan', 'uploads', (args.session as string) || 'default');\n const filePath = join(uploadsDir, (args.filename as string).replace(/[^a-zA-Z0-9._-]/g, '_'));\n\n if (!filePath.startsWith(uploadsDir)) return 'Access denied';\n if (!existsSync(filePath)) return `File not found: ${args.filename}`;\n\n const stat = statSync(filePath);\n const ext = (args.filename as string).split('.').pop()?.toLowerCase() || '';\n const textExts = ['txt', 'md', 'csv', 'json', 'xml', 'html', 'js', 'ts', 'py', 'yaml', 'yml', 'toml', 'ini', 'log', 'sql', 'sh', 'env'];\n\n if (textExts.includes(ext) || stat.size < 100000) {\n try {\n const content = readFileSync(filePath, 'utf-8');\n if (content.length > 50000) {\n return `File: ${args.filename} (${(stat.size / 1024).toFixed(1)}KB)\\n---\\n${content.slice(0, 50000)}\\n\\n... [truncated, ${content.length} chars total]`;\n }\n return `File: ${args.filename} (${(stat.size / 1024).toFixed(1)}KB)\\n---\\n${content}`;\n } catch {\n return `Binary file: ${args.filename} (${(stat.size / 1024).toFixed(1)}KB, .${ext}). Cannot display as text.`;\n }\n }\n return `Binary file: ${args.filename} (${(stat.size / 1024).toFixed(1)}KB, .${ext}). Use a specialized tool to process this file type.`;\n },\n },\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/skills/builtin/filesystem.ts"],"sourcesContent":["/**\n * TITAN — Filesystem Skill (Built-in)\n * Read, write, edit, and list files and directories.\n */\nimport { readFileSync, writeFileSync, existsSync, readdirSync, statSync, mkdirSync, openSync, readSync, closeSync } from 'fs';\nimport { join, resolve } from 'path';\nimport { homedir } from 'os';\nimport { registerSkill } from '../registry.js';\nimport { resolveImageReferences } from '../../agent/imageRegistry.js';\nimport {\n verifyFileWrite,\n recordVerificationEvent,\n formatVerificationSuffix,\n} from '../../agent/verification.js';\n\n/** S4: Block access to sensitive system paths */\nconst BLOCKED_PATHS = ['/etc', '/root', '/sys', '/proc', '/dev', '/boot', '/var/log', '/var/run'];\nconst BLOCKED_PATTERNS = ['.ssh', '.gnupg', '.aws', '.env', 'id_rsa', 'id_ed25519', '.netrc', '.npmrc'];\nconst MAX_DIR_ENTRIES = 50_000;\n\n/** Expand ~ and resolve to absolute path */\nfunction expandPath(filePath: string): string {\n const expanded = filePath.startsWith('~/') ? join(homedir(), filePath.slice(2)) : filePath;\n return resolve(expanded);\n}\n\n/**\n * Hunt Finding #32 (2026-04-14): helper to check if `child` is the same as\n * or contained within `parent` on the filesystem, WITHOUT the `startsWith`\n * trap. A naive `child.startsWith(parent)` returns true for siblings that\n * share a prefix (e.g. `/tmpfoo` starts with `/tmp`, `/home/djacob` starts\n * with `/home/dj`). We require either exact match or a path-separator\n * boundary after `parent`.\n */\nexport function isWithinDir(child: string, parent: string): boolean {\n if (child === parent) return true;\n // Ensure trailing separator on parent so /tmpfoo doesn't match /tmp\n const parentWithSep = parent.endsWith('/') ? parent : parent + '/';\n return child.startsWith(parentWithSep);\n}\n\nfunction getPathBlockReason(filePath: string): string | null {\n const resolved = expandPath(filePath);\n for (const pattern of BLOCKED_PATTERNS) {\n if (resolved.includes(pattern)) {\n return `Access denied: cannot access ${pattern} files`;\n }\n }\n for (const blocked of BLOCKED_PATHS) {\n if (isWithinDir(resolved, blocked)) {\n return `Access denied: cannot access system directory ${blocked}`;\n }\n }\n return null;\n}\n\nexport function isPathBlocked(filePath: string): boolean {\n return getPathBlockReason(filePath) !== null;\n}\n\nexport function validatePath(filePath: string): string | null {\n const resolved = expandPath(filePath);\n const home = homedir();\n // Allow access within home directory and /tmp only.\n // Hunt Finding #32: must check path-separator boundary, not raw startsWith.\n if (!isWithinDir(resolved, home) && !isWithinDir(resolved, '/tmp')) {\n return `Access denied: path must be within home directory or /tmp`;\n }\n return getPathBlockReason(resolved); // null means valid\n}\n\n/**\n * Hunt Finding #36 (2026-04-14): max bytes returned by read_file in a single\n * call. A Phase 5.5 test read a 1MB file and the full content got pumped into\n * the model context, exploding it to 213K tokens and driving the model into\n * 21-tool-call pathological exploration before returning a hallucinated\n * wrong answer. Now: files over this threshold return a preview + stats +\n * usage hint; the caller can use byteOffset/byteLimit to page through.\n * Tunable via env var TITAN_READ_FILE_MAX_BYTES.\n */\nconst READ_FILE_MAX_BYTES = (() => {\n const v = process.env.TITAN_READ_FILE_MAX_BYTES;\n const n = v ? parseInt(v, 10) : NaN;\n if (Number.isFinite(n) && n > 0 && n <= 10_000_000) return n;\n return 100_000; // 100 KB default — enough for most source files, small enough to not blow context\n})();\n\n/** Read the first N bytes of a file without loading the whole thing. */\nfunction readFirstBytes(filePath: string, maxBytes: number): string {\n const fd = openSync(filePath, 'r');\n try {\n const buf = Buffer.alloc(maxBytes);\n const bytesRead = readSync(fd, buf, 0, maxBytes, 0);\n return buf.subarray(0, bytesRead).toString('utf-8');\n } finally {\n closeSync(fd);\n }\n}\n\nexport function registerFilesystemSkill(): void {\n // Read File\n registerSkill(\n { name: 'read_file', description: 'Read file contents', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'read_file',\n description: `Read a file from the local filesystem and return its contents prefixed with 1-based line numbers (cat -n format). Use this before any edit so you can quote exact lines back to edit_file.\n\nUSE WHEN: \"read X\" / \"show me X\" / \"what's in X\" / \"open X\" / \"check X\" / \"look at X\" — anything where the answer comes from a local file. Also: ALWAYS as the first step before edit_file or append_file on an existing file.\n\nDO NOT USE FOR:\n- Listing a directory → use list_dir\n- Reading a URL → use web_fetch\n- Reading a file inside an archive → unzip first via shell, then read_file\n\nParameters:\n- path (string, required) — absolute or ~-prefixed path. Relative paths resolve from cwd.\n- startLine (number, optional, 1-indexed) — first line to return; defaults to 1.\n- endLine (number, optional, 1-indexed inclusive) — last line; defaults to file end or +2000 lines if very large.\n\nReturns: { content: string, path: string, totalLines: number, truncated: boolean, sizeBytes: number }. content is the line-numbered text. truncated=true means the file is bigger than the returned window; call again with a higher startLine/endLine to read the rest.\n\nErrors:\n- \"ENOENT: file not found\" — verify the path; the user may have given a typo. Try list_dir on the parent directory first.\n- \"EACCES: permission denied\" — TITAN can't read the file; surface to the user, suggest chmod or running TITAN with the right user.\n- \"BLOCKED: sensitive path\" — file matched a blocklist pattern (.ssh, .env, /etc, etc.). Do not retry; tell the user this path is off-limits.\n\nNEVER hallucinate file content. If read_file failed, say so — do not fabricate.`,\n parameters: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'Absolute or relative path to the file' },\n startLine: { type: 'number', description: 'Start line (1-indexed, optional)' },\n endLine: { type: 'number', description: 'End line (1-indexed, optional)' },\n },\n required: ['path'],\n },\n execute: async (args) => {\n const filePath = expandPath(args.path as string);\n const pathErr = validatePath(filePath);\n if (pathErr) return pathErr;\n if (!existsSync(filePath)) return `Error: File not found: ${filePath}`;\n try {\n // Hunt Finding #36 (2026-04-14): check file size BEFORE reading\n // the full content. Previous version called readFileSync\n // unconditionally and a 1MB file exploded context to 213K\n // tokens and drove the model into 21-call pathological\n // exploration before hallucinating a wrong answer.\n const stat = statSync(filePath);\n const fileSize = stat.size;\n const oversized = fileSize > READ_FILE_MAX_BYTES;\n\n // If the caller supplied startLine/endLine they're opting\n // into scoped reads — let them read even from a large file,\n // but still cap the total bytes returned.\n const rawStart = args.startLine as number | undefined;\n const rawEnd = args.endLine as number | undefined;\n const hasScope = (rawStart !== undefined) || (rawEnd !== undefined);\n\n if (oversized && !hasScope) {\n // Return a preview of the first READ_FILE_MAX_BYTES\n // plus file stats + a usage hint. This keeps context\n // bounded and points the model at the right next action.\n const preview = readFirstBytes(filePath, READ_FILE_MAX_BYTES);\n const previewLines = preview.split('\\n');\n const humanSize = fileSize >= 1_000_000\n ? `${(fileSize / 1_000_000).toFixed(2)} MB`\n : `${(fileSize / 1_000).toFixed(1)} KB`;\n return [\n `File: ${filePath}`,\n `Size: ${humanSize} (${fileSize} bytes) — TRUNCATED`,\n `Showing first ${READ_FILE_MAX_BYTES} bytes (~${previewLines.length} lines). The file is too large to load in full.`,\n `To read more, call read_file again with startLine/endLine parameters.`,\n `---`,\n ...previewLines.slice(0, 500).map((l, i) => `${i + 1}: ${l}`),\n ].join('\\n');\n }\n\n const content = readFileSync(filePath, 'utf-8');\n const lines = content.split('\\n');\n const start = rawStart || 1;\n const end = rawEnd || lines.length;\n const selected = lines.slice(start - 1, end);\n\n // Even with scoped reads, cap the returned size so a file\n // with one million characters on a single line doesn't\n // blow up context through the startLine=1,endLine=1 path.\n const output = `File: ${filePath} (${lines.length} lines)\\n---\\n${selected.map((l, i) => `${start + i}: ${l}`).join('\\n')}`;\n if (output.length > READ_FILE_MAX_BYTES * 2) {\n return output.slice(0, READ_FILE_MAX_BYTES * 2) +\n `\\n\\n... [output truncated: ${output.length - READ_FILE_MAX_BYTES * 2} bytes omitted. Use narrower startLine/endLine to paginate.]`;\n }\n return output;\n } catch (e) { return `Error reading file: ${(e as Error).message}`; }\n },\n },\n );\n\n // Write File\n registerSkill(\n { name: 'write_file', description: 'Write/create a file', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'write_file',\n description: `Write a file to the local filesystem (creates parent directories as needed). Overwrites any existing file at the path. A shadow-git snapshot is taken automatically before the write, so an unwanted overwrite can be rolled back via checkpoint_restore.\n\nUSE WHEN: \"create file X\" / \"write X to a file\" / \"save this to X\" / \"make a file called X\" / \"create X with this content\" — anything where the deliverable is a new file or a full rewrite.\n\nDO NOT USE FOR:\n- Modifying an existing file → read_file first, then edit_file (preserves surrounding content, atomic).\n- Appending to a log → use append_file (no overwrite).\n- Creating files in /etc, ~/.ssh, or .env paths → BLOCKED by the path guard; surface to the user.\n\nParameters:\n- path (string, required) — absolute or ~-prefixed.\n- content (string, required) — exact bytes to write. Use \\\\n for newlines; do not write \\\\r\\\\n unless the user explicitly asks for Windows line endings.\n\nReturns: { path: string, bytesWritten: number, created: boolean, checkpointId: string }. created=true means the file didn't exist before. checkpointId can be passed to checkpoint_restore to revert.\n\nErrors:\n- \"EACCES: permission denied\" — directory not writable; suggest chmod or a different path.\n- \"BLOCKED: sensitive path\" — write rejected by the path guard. Do not retry.\n- \"ENOSPC: no space left\" — disk full; surface to the user, do not silently succeed.\n\nNEVER output the file content as a plain code block in the chat when the user asked to write a file — call write_file. Outputting raw content is the #1 wrong-tool failure mode.`,\n parameters: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'Path to the file' },\n content: { type: 'string', description: 'Content to write' },\n },\n required: ['path', 'content'],\n },\n execute: async (args) => {\n const filePath = expandPath(args.path as string);\n const pathErr = validatePath(filePath);\n if (pathErr) return pathErr;\n try {\n const dir = join(filePath, '..');\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n // v6.1.0-alpha.56 — substitute `tdi://hash` reference\n // tokens from download_image with their real data\n // URLs RIGHT before writing to disk. The LLM never\n // had to handle the full base64 in its context.\n const newContent = resolveImageReferences(args.content as string);\n // Guard: prevent destructive overwrites of existing files.\n // Use newContent.length-vs-existing.length ratio rather\n // than line count, because resolved data URLs span one\n // very long line and would trigger false-positive\n // \"destructive expansion\" warnings on the line-count check.\n if (existsSync(filePath)) {\n const existing = readFileSync(filePath, 'utf-8');\n const existingLines = existing.split('\\n').length;\n const newLines = newContent.split('\\n').length;\n const containsDataUrls = /data:image\\/[^;]+;base64,/.test(newContent);\n if (!containsDataUrls) {\n if (existingLines > 20 && newLines < existingLines * 0.4) {\n return `Error: write_file would replace ${existingLines} lines with only ${newLines} lines (${Math.round(newLines / existingLines * 100)}% of original). This looks destructive. Use edit_file to make surgical changes instead of rewriting the entire file.`;\n }\n if (existingLines > 20 && newLines > existingLines * 3) {\n return `Error: write_file would expand ${existingLines} lines to ${newLines} lines (${Math.round(newLines / existingLines * 100)}% of original). This looks like accidental file duplication. Use edit_file to make targeted changes instead of rewriting the entire file.`;\n }\n }\n }\n writeFileSync(filePath, newContent, 'utf-8');\n // beta.9 — verify-after-write. Reads back, checks\n // size + a 256-byte head sample. Failed checks do\n // NOT throw (the file is already on disk) but are\n // surfaced in the return string AND pushed to the\n // verification ring so the driver loop's\n // consecutiveAssertionFailures counter can escalate.\n const written = Buffer.byteLength(newContent, 'utf-8');\n const result = verifyFileWrite(filePath, written, newContent);\n recordVerificationEvent({ kind: 'file_write', result, artifactRef: filePath });\n return `Successfully wrote ${newContent.length} bytes to ${filePath}${formatVerificationSuffix(result)}`;\n } catch (e) { return `Error writing file: ${(e as Error).message}`; }\n },\n },\n );\n\n // Append to File\n registerSkill(\n { name: 'append_file', description: 'Append content to end of a file', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'append_file',\n description: `Append content to the end of an existing file (creates it if missing). Atomic + idempotent on retry only when the content already ends in a newline — otherwise repeated calls duplicate the last line. Use this for building files incrementally (logs, accumulating reports, HTML section-by-section) without the round-trip cost of read_file + write_file.\n\nUSE WHEN: \"append X to Y\" / \"add X to the bottom of Y\" / \"log X\" / when iteratively building a large file (write_file the skeleton, then append_file each section).\n\nDO NOT USE FOR:\n- Inserting into the middle of a file → use edit_file (find-and-replace).\n- Overwriting → use write_file.\n- Appending to a structured file (JSON, YAML) where the result must remain valid — append_file is byte-level and won't keep JSON arrays valid.\n\nParameters:\n- path (string, required) — absolute or ~-prefixed.\n- content (string, required) — exact bytes to append. End with \\\\n if you want a trailing newline.\n\nReturns: { path: string, bytesAppended: number, totalBytes: number, created: boolean }.\n\nErrors: same set as write_file (EACCES, BLOCKED: sensitive path, ENOSPC).`,\n parameters: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'Path to the file' },\n content: { type: 'string', description: 'Content to append' },\n },\n required: ['path', 'content'],\n },\n execute: async (args) => {\n const filePath = expandPath(args.path as string);\n const pathErr = validatePath(filePath);\n if (pathErr) return pathErr;\n try {\n const { appendFileSync } = await import('fs');\n const dir = join(filePath, '..');\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n // v6.1.0-alpha.56 — resolve tdi:// image references before append.\n const resolved = resolveImageReferences(args.content as string);\n // beta.9 — capture pre-append size BEFORE the write\n // so we can assert post-size == pre-size + appended\n // bytes. Catches partial writes, mount-readonly\n // silent failures, and anyone wedging a hook\n // between us and the disk.\n const preSize = existsSync(filePath) ? statSync(filePath).size : 0;\n appendFileSync(filePath, resolved, 'utf-8');\n const appended = Buffer.byteLength(resolved, 'utf-8');\n const result = verifyFileWrite(filePath, preSize + appended);\n recordVerificationEvent({ kind: 'file_append', result, artifactRef: filePath });\n return `Successfully appended ${appended} bytes to ${filePath}${formatVerificationSuffix(result)}`;\n } catch (e) { return `Error appending to file: ${(e as Error).message}`; }\n },\n },\n );\n\n // Edit File\n registerSkill(\n { name: 'edit_file', description: 'Search and replace in a file', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'edit_file',\n description: `Edit a file by replacing one exact string with another (surgical, atomic). Preserves all surrounding content. Shadow-git checkpoints before and after, so any edit is rollback-able.\n\nUSE WHEN: \"edit X\" / \"change X to Y in the file\" / \"update X\" / \"fix X in the file\" / \"modify X\" / \"rename foo to bar in X\" — anything where you want a precise change without rewriting the whole file.\n\nWORKFLOW (non-negotiable):\n1. Call read_file first to see the current content.\n2. Copy the exact target string from the read_file output (whitespace, indentation, line endings all matter).\n3. Call edit_file with target + replacement.\n\nDO NOT USE FOR:\n- A full rewrite → use write_file (it's faster).\n- A target string that occurs more than once in the file → split it into two edits, each with enough surrounding context to be unique.\n- A target you haven't seen via read_file → blind edits routinely fail with \"target not found\".\n\nParameters:\n- path (string, required) — absolute or ~-prefixed.\n- target (string, required) — exact text to find. Must match byte-for-byte AND be unique in the file.\n- replacement (string, required) — text to substitute. Can be longer or shorter than the target.\n\nReturns: { path: string, bytesBefore: number, bytesAfter: number, occurrencesReplaced: number, checkpointId: string }.\n\nErrors:\n- \"Target string not found\" — your target doesn't match the file content. Call read_file again, copy more carefully (mind tabs vs spaces, trailing whitespace).\n- \"Target string is not unique (N matches)\" — add more surrounding context to the target until it's unique, OR call edit_file once per occurrence with disambiguating context.\n- Same EACCES/BLOCKED errors as write_file.\n\nNEVER guess at the target — read_file first, always.`,\n parameters: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'Path to the file' },\n target: { type: 'string', description: 'Exact string to find and replace' },\n replacement: { type: 'string', description: 'Replacement content' },\n },\n required: ['path', 'target', 'replacement'],\n },\n execute: async (args) => {\n const filePath = expandPath(args.path as string);\n const pathErr2 = validatePath(filePath);\n if (pathErr2) return pathErr2;\n if (!existsSync(filePath)) return `Error: File not found: ${filePath}`;\n try {\n let content = readFileSync(filePath, 'utf-8');\n const target = args.target as string;\n if (!content.includes(target)) {\n // Fuzzy matching: try to find the closest matching block\n const _targetLines = target.split('\\n').map((l: string) => l.trim()).filter(Boolean);\n const contentLines = content.split('\\n');\n\n // Try normalized whitespace match first\n const normalizedTarget = target.replace(/\\s+/g, ' ').trim();\n const normalizedContent = content.replace(/\\s+/g, ' ').trim();\n if (normalizedContent.includes(normalizedTarget)) {\n // Whitespace-only difference — find and replace with original whitespace context\n const targetChunks = target.split('\\n');\n const firstLine = targetChunks[0].trim();\n const lastLine = targetChunks[targetChunks.length - 1].trim();\n let startIdx = -1, endIdx = -1;\n for (let i = 0; i < contentLines.length; i++) {\n if (contentLines[i].trim() === firstLine) { startIdx = i; break; }\n }\n if (startIdx >= 0) {\n for (let i = startIdx; i < contentLines.length; i++) {\n if (contentLines[i].trim() === lastLine) { endIdx = i; break; }\n }\n }\n if (startIdx >= 0 && endIdx >= 0) {\n const before = contentLines.slice(0, startIdx).join('\\n');\n const after = contentLines.slice(endIdx + 1).join('\\n');\n content = before + (before ? '\\n' : '') + (args.replacement as string) + (after ? '\\n' : '') + after;\n writeFileSync(filePath, content, 'utf-8');\n const fuzzResult = verifyFileWrite(\n filePath,\n Buffer.byteLength(content, 'utf-8'),\n content,\n );\n recordVerificationEvent({ kind: 'file_edit', result: fuzzResult, artifactRef: filePath });\n return `Successfully edited ${filePath} (fuzzy whitespace match applied)${formatVerificationSuffix(fuzzResult)}`;\n }\n }\n\n // Line-by-line similarity: find best matching region\n const targetWords = target.toLowerCase().split(/\\s+/).filter((w: string) => w.length > 3);\n let bestLine = -1, bestScore = 0;\n for (let i = 0; i < contentLines.length; i++) {\n const score = targetWords.filter((w: string) => contentLines[i].toLowerCase().includes(w)).length;\n if (score > bestScore) { bestScore = score; bestLine = i; }\n }\n const nearby = bestLine >= 0\n ? contentLines.slice(Math.max(0, bestLine - 3), bestLine + 4).map((l: string, i: number) => ` ${Math.max(1, bestLine - 2) + i}: ${l}`).join('\\n')\n : contentLines.slice(0, 10).map((l: string, i: number) => ` ${i + 1}: ${l}`).join('\\n');\n return `Error: Target string not found in ${filePath}. The target must match EXACTLY (including whitespace/indentation). Closest match area:\\n${nearby}\\n\\nTIP: Use read_file with startLine/endLine to get the exact text, then copy it precisely as the target.`;\n }\n // v6.1.0-alpha.56 — resolve tdi:// references in the\n // replacement before splicing so the LLM can paste a\n // download_image token into edit_file's replacement\n // and get the full data URL on disk.\n const resolvedReplacement = resolveImageReferences(args.replacement as string);\n content = content.split(target).join(resolvedReplacement);\n writeFileSync(filePath, content, 'utf-8');\n // beta.9 — verify-after-edit. Full content sample\n // here is safe because we already hold the post-edit\n // bytes in memory; readback confirms disk matches.\n const editResult = verifyFileWrite(\n filePath,\n Buffer.byteLength(content, 'utf-8'),\n content,\n );\n recordVerificationEvent({ kind: 'file_edit', result: editResult, artifactRef: filePath });\n return `Successfully edited ${filePath}${formatVerificationSuffix(editResult)}`;\n } catch (e) { return `Error editing file: ${(e as Error).message}`; }\n },\n },\n );\n\n // List Directory\n registerSkill(\n { name: 'list_dir', description: 'List directory contents', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'list_dir',\n description: `List the contents of a directory with file sizes and types (file vs directory). One-level by default; set recursive:true only on small trees.\n\nUSE WHEN: \"list files in X\" / \"what's in the X folder\" / \"show me X directory\" / \"ls X\" / \"what files are in X\" / \"is there a Y file here\" — anything where the answer comes from filesystem structure.\n\nDO NOT USE FOR:\n- Listing a huge tree (node_modules, ~/Library) → use recursive:false then drill in manually, or shell + find -maxdepth.\n- File contents → use read_file.\n- Searching by name → use shell with find or grep -r.\n\nParameters:\n- path (string, required) — absolute or ~-prefixed. Path to the directory.\n- recursive (boolean, optional) — default false. true returns a flat tree of every file under path. Capped at MAX_DIR_ENTRIES (50_000) to prevent runaway scans.\n\nReturns: { path: string, entries: Array<{ name, type: \"file\"|\"directory\", sizeBytes, modifiedAt }>, count: number, truncated: boolean }. truncated=true means MAX_DIR_ENTRIES was hit; narrow the path or use recursive:false.\n\nErrors:\n- \"ENOENT: directory not found\" — verify the path; suggest list_dir on the parent.\n- \"ENOTDIR: not a directory\" — caller passed a file path; use read_file instead.\n- \"BLOCKED: sensitive path\" — path is in the blocklist (/etc, ~/.ssh, etc.). Do not retry.`,\n parameters: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'Path to the directory' },\n recursive: { type: 'boolean', description: 'List recursively (default: false)' },\n },\n required: ['path'],\n },\n execute: async (args) => {\n const dirPath = expandPath(args.path as string);\n const dirErr = validatePath(dirPath);\n if (dirErr) return dirErr;\n if (!existsSync(dirPath)) return `Error: Directory not found: ${dirPath}`;\n try {\n const recursive = args.recursive === true;\n const lines: string[] = [];\n let truncated = false;\n const addEntries = (currentPath: string, prefix = ''): void => {\n const entries = readdirSync(currentPath, { withFileTypes: true });\n for (const entry of entries) {\n if (lines.length >= MAX_DIR_ENTRIES) {\n truncated = true;\n return;\n }\n const fullPath = join(currentPath, entry.name);\n if (isPathBlocked(fullPath)) continue;\n const displayName = prefix ? `${prefix}/${entry.name}` : entry.name;\n if (entry.isDirectory()) {\n lines.push(`📁 ${displayName}/`);\n if (recursive) addEntries(fullPath, displayName);\n if (truncated) return;\n continue;\n }\n const stat = statSync(fullPath);\n const size = stat.size < 1024 ? `${stat.size}B` : stat.size < 1048576 ? `${(stat.size / 1024).toFixed(1)}KB` : `${(stat.size / 1048576).toFixed(1)}MB`;\n lines.push(`📄 ${displayName} (${size})`);\n }\n };\n addEntries(dirPath);\n const suffix = truncated ? `\\n... [truncated at ${MAX_DIR_ENTRIES} entries]` : '';\n return `Directory: ${dirPath}${recursive ? ' (recursive)' : ''}\\n${lines.join('\\n')}${suffix}`;\n } catch (e) { return `Error listing directory: ${(e as Error).message}`; }\n },\n },\n );\n\n // List Uploaded Files\n registerSkill(\n { name: 'list_uploads', description: 'List uploaded files for a session', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'list_uploads',\n description: 'List files uploaded by the user in the current session.\\n\\nUSE THIS WHEN user mentions \"my file\", \"the file I uploaded\", \"uploaded document\", or wants to work with an attachment.',\n parameters: {\n type: 'object',\n properties: {\n session: { type: 'string', description: 'Session ID (defaults to \"default\")' },\n },\n },\n execute: async (args) => {\n const uploadsDir = join(homedir(), '.titan', 'uploads', (args.session as string) || 'default');\n if (!existsSync(uploadsDir)) return 'No files uploaded yet in this session.';\n try {\n const entries = readdirSync(uploadsDir);\n if (entries.length === 0) return 'No files uploaded yet in this session.';\n const lines = entries.map(name => {\n const stat = statSync(join(uploadsDir, name));\n const size = stat.size < 1024 ? `${stat.size}B` : `${(stat.size / 1024).toFixed(1)}KB`;\n return `📎 ${name} (${size})`;\n });\n return `Uploaded files:\\n${lines.join('\\n')}`;\n } catch (e) { return `Error: ${(e as Error).message}`; }\n },\n },\n );\n\n // Read Uploaded File\n registerSkill(\n { name: 'read_upload', description: 'Read an uploaded file', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'read_upload',\n description: 'Read the contents of a file uploaded by the user.\\n\\nUSE THIS WHEN user asks about the contents of a file they uploaded, or says \"read my file\", \"what does the file say\", \"analyze this document\".\\n\\nSupports: text, CSV, JSON, and other text-based formats. For binary files (PDF, images), returns metadata.',\n parameters: {\n type: 'object',\n properties: {\n filename: { type: 'string', description: 'Name of the uploaded file' },\n session: { type: 'string', description: 'Session ID (defaults to \"default\")' },\n },\n required: ['filename'],\n },\n execute: async (args) => {\n const uploadsDir = join(homedir(), '.titan', 'uploads', (args.session as string) || 'default');\n const filePath = join(uploadsDir, (args.filename as string).replace(/[^a-zA-Z0-9._-]/g, '_'));\n\n if (!filePath.startsWith(uploadsDir)) return 'Access denied';\n if (!existsSync(filePath)) return `File not found: ${args.filename}`;\n\n const stat = statSync(filePath);\n const ext = (args.filename as string).split('.').pop()?.toLowerCase() || '';\n const textExts = ['txt', 'md', 'csv', 'json', 'xml', 'html', 'js', 'ts', 'py', 'yaml', 'yml', 'toml', 'ini', 'log', 'sql', 'sh', 'env'];\n\n if (textExts.includes(ext) || stat.size < 100000) {\n try {\n const content = readFileSync(filePath, 'utf-8');\n if (content.length > 50000) {\n return `File: ${args.filename} (${(stat.size / 1024).toFixed(1)}KB)\\n---\\n${content.slice(0, 50000)}\\n\\n... [truncated, ${content.length} chars total]`;\n }\n return `File: ${args.filename} (${(stat.size / 1024).toFixed(1)}KB)\\n---\\n${content}`;\n } catch {\n return `Binary file: ${args.filename} (${(stat.size / 1024).toFixed(1)}KB, .${ext}). Cannot display as text.`;\n }\n }\n return `Binary file: ${args.filename} (${(stat.size / 1024).toFixed(1)}KB, .${ext}). Use a specialized tool to process this file type.`;\n },\n },\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/skills/registry.js
CHANGED
|
@@ -149,6 +149,13 @@ function parseSkillMd(content, fallbackName) {
|
|
|
149
149
|
}
|
|
150
150
|
async function initBuiltinSkills() {
|
|
151
151
|
logger.info(COMPONENT, "Loading built-in skills...");
|
|
152
|
+
try {
|
|
153
|
+
const { registerCanonicalContracts } = await import("./builtin/contracts.js");
|
|
154
|
+
registerCanonicalContracts();
|
|
155
|
+
logger.debug(COMPONENT, "Registered canonical tool contracts");
|
|
156
|
+
} catch (err) {
|
|
157
|
+
logger.warn(COMPONENT, `Failed to register canonical contracts: ${err.message}`);
|
|
158
|
+
}
|
|
152
159
|
const { registerShellSkill } = await import("./builtin/shell.js");
|
|
153
160
|
const { registerFilesystemSkill } = await import("./builtin/filesystem.js");
|
|
154
161
|
const { registerWebSearchSkill } = await import("./builtin/web_search.js");
|