@hraness/dawg 0.2.0 → 0.3.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/CHANGELOG.md +79 -0
- package/DAWG.md +196 -11
- package/README.md +79 -30
- package/core/diff.ts +249 -0
- package/core/key.ts +43 -0
- package/core/pitch.ts +60 -0
- package/core/score.ts +324 -1
- package/core/sdk/eval-child.ts +113 -0
- package/core/sdk/eval.ts +257 -0
- package/core/sdk/print.ts +393 -0
- package/core/sdk/v1.ts +954 -0
- package/core/slug.ts +19 -0
- package/package.json +8 -5
- package/src/agent/agent.ts +280 -14
- package/src/agent/brief.ts +23 -3
- package/src/agent/gateway.ts +215 -41
- package/src/agent/models.ts +633 -0
- package/src/agent/ops.ts +3 -18
- package/src/agent/planner.ts +38 -0
- package/src/agent/provider.ts +234 -106
- package/src/agent/sse.ts +31 -7
- package/src/agent/tools.ts +398 -1
- package/src/agent/usage.ts +296 -0
- package/src/agent/workspace.ts +683 -0
- package/src/agent/xcb-agent.ts +27 -12
- package/src/agent/xcb.ts +233 -21
- package/src/audio/click.ts +125 -0
- package/src/audio/engine.ts +407 -33
- package/src/audio/live.ts +152 -0
- package/src/audio/player.ts +19 -4
- package/src/audio/render-worker.ts +68 -0
- package/src/audio/renderer.ts +174 -0
- package/src/audio/sampler.ts +292 -0
- package/src/audio/samples.ts +683 -0
- package/src/audio/wav.ts +290 -76
- package/src/auth/cli.ts +146 -27
- package/src/auth/credentials.ts +167 -41
- package/src/auth/discover.ts +481 -0
- package/src/auth/login.ts +885 -128
- package/src/auth/openrouter.ts +206 -0
- package/src/auth/picker.ts +282 -0
- package/src/auth/runner.ts +25 -2
- package/src/auth/tui.ts +60 -43
- package/src/commands/edit.ts +170 -0
- package/src/commands/help.ts +247 -0
- package/src/commands/history.ts +32 -19
- package/src/commands/music.ts +24 -7
- package/src/commands/sample.ts +302 -0
- package/src/main.ts +1043 -166
- package/src/media/analyze.ts +364 -0
- package/src/media/backend.ts +253 -0
- package/src/media/cli.ts +173 -0
- package/src/media/download.ts +281 -0
- package/src/media/dsp.ts +281 -0
- package/src/media/import.ts +130 -0
- package/src/media/lyrics.ts +201 -0
- package/src/media/notes.ts +363 -0
- package/src/media/paths.ts +168 -0
- package/src/media/process.ts +226 -0
- package/src/media/registry.ts +9 -0
- package/src/media/sidecar.ts +72 -0
- package/src/media/stemdeck.ts +254 -0
- package/src/media/stems.ts +173 -0
- package/src/media/tools.ts +292 -0
- package/src/media/types.ts +92 -0
- package/src/media/vendor/basic-pitch.ts +261 -0
- package/src/media/vendor/drums.ts +817 -0
- package/src/media/vendor/grid.ts +203 -0
- package/src/media/vendor/util.ts +139 -0
- package/src/media/vendor/wav.ts +233 -0
- package/src/project/check.ts +80 -0
- package/src/project/init.ts +253 -0
- package/src/project/sync.ts +432 -0
- package/src/project/typecheck.ts +149 -0
- package/src/render.ts +28 -6
- package/src/session/attach.ts +3 -4
- package/src/session/daemon.ts +25 -8
- package/src/session/delta.ts +249 -0
- package/src/session/naming.ts +3 -37
- package/src/session/port.ts +26 -6
- package/src/session/rebase.ts +38 -8
- package/src/session/store.ts +116 -21
- package/src/tui/menu.ts +1083 -0
- package/src/tui/play-mode.ts +442 -0
- package/src/tui/play-session.ts +636 -0
- package/src/web/fetch.ts +340 -0
- package/src/web/http.ts +137 -0
- package/src/web/search.ts +681 -0
- package/tui/activity.ts +42 -3
- package/tui/app.ts +261 -18
- package/tui/drums.ts +44 -0
- package/tui/highway.ts +18 -2
- package/tui/layers.ts +14 -2
- package/tui/play-strip.ts +143 -0
package/src/agent/tools.ts
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
} from "../../core/score.ts";
|
|
8
8
|
import { AVAILABLE_INSTRUMENTS } from "../audio/wav.ts";
|
|
9
9
|
import type { ChatTool } from "./gateway.ts";
|
|
10
|
+
import { MEDIA_TOOLS } from "../media/tools.ts";
|
|
11
|
+
import type { MediaResult, MediaRunContext } from "../media/types.ts";
|
|
10
12
|
import { pitchToMidi } from "./ops.ts";
|
|
11
13
|
import {
|
|
12
14
|
DRUM_VOICES,
|
|
@@ -15,6 +17,30 @@ import {
|
|
|
15
17
|
parseDrumVoice,
|
|
16
18
|
type DrumVoice,
|
|
17
19
|
} from "../../core/drums.ts";
|
|
20
|
+
import { trackSlug } from "../../core/slug.ts";
|
|
21
|
+
import {
|
|
22
|
+
editFile,
|
|
23
|
+
listFiles,
|
|
24
|
+
readFile,
|
|
25
|
+
WORKSPACE_LIMITS,
|
|
26
|
+
WorkspaceError,
|
|
27
|
+
writeFile,
|
|
28
|
+
type WorkspaceScope,
|
|
29
|
+
} from "./workspace.ts";
|
|
30
|
+
import {
|
|
31
|
+
fetchUrl,
|
|
32
|
+
FETCH_LIMITS,
|
|
33
|
+
formatFetchedPage,
|
|
34
|
+
type Lookup,
|
|
35
|
+
} from "../web/fetch.ts";
|
|
36
|
+
import { WebError, type FetchLike } from "../web/http.ts";
|
|
37
|
+
import {
|
|
38
|
+
describeSearchProvider,
|
|
39
|
+
formatSearchResults,
|
|
40
|
+
SEARCH_LIMITS,
|
|
41
|
+
webSearch,
|
|
42
|
+
type SearchSpend,
|
|
43
|
+
} from "../web/search.ts";
|
|
18
44
|
|
|
19
45
|
/** What a validated tool call asks the host to do. */
|
|
20
46
|
export type ToolPlan =
|
|
@@ -29,7 +55,66 @@ export type ToolPlan =
|
|
|
29
55
|
action: "play" | "pause" | "toggle";
|
|
30
56
|
summary: string;
|
|
31
57
|
}>
|
|
32
|
-
| Readonly<{ kind: "explain"; text: string; summary: string }
|
|
58
|
+
| Readonly<{ kind: "explain"; text: string; summary: string }>
|
|
59
|
+
| Readonly<{
|
|
60
|
+
kind: "action";
|
|
61
|
+
summary: string;
|
|
62
|
+
/** Side effects outside the score (files, network); bounded and async. */
|
|
63
|
+
run: (context: ActionContext) => Promise<ActionResult>;
|
|
64
|
+
}>
|
|
65
|
+
/** A long-running local media job (download, stems, analysis, …). */
|
|
66
|
+
| Readonly<{
|
|
67
|
+
kind: "media";
|
|
68
|
+
summary: string;
|
|
69
|
+
run: (context: MediaRunContext) => Promise<MediaResult>;
|
|
70
|
+
}>;
|
|
71
|
+
|
|
72
|
+
/** The project directory the workspace tools operate in. */
|
|
73
|
+
export type WorkspaceHost = Readonly<{ root: string }>;
|
|
74
|
+
|
|
75
|
+
/** Injection points for the web tools; defaults are the real network. */
|
|
76
|
+
export type WebHost = Readonly<{
|
|
77
|
+
fetch?: FetchLike;
|
|
78
|
+
lookup?: Lookup;
|
|
79
|
+
/** Brave Web Search API key; defaults to `BRAVE_SEARCH_API_KEY` and overrides the chain. */
|
|
80
|
+
braveApiKey?: string;
|
|
81
|
+
/** AI Gateway key of the active provider; enables gateway search tools. */
|
|
82
|
+
gatewayApiKey?: string;
|
|
83
|
+
gatewayBaseUrl?: string;
|
|
84
|
+
/** Gateway search tool; defaults to `DAWG_WEB_SEARCH`, then `exa`. */
|
|
85
|
+
searchTool?: string;
|
|
86
|
+
/** OpenRouter key; defaults to `OPENROUTER_API_KEY`. */
|
|
87
|
+
openRouterApiKey?: string;
|
|
88
|
+
openRouterBaseUrl?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Billed searches (gateway tools, OpenRouter web plugin) are reported here
|
|
91
|
+
* so the host can add them to its spend ledger (`webHostFor` in
|
|
92
|
+
* `src/agent/usage.ts`).
|
|
93
|
+
*/
|
|
94
|
+
onSpend?: (spend: SearchSpend) => void;
|
|
95
|
+
}>;
|
|
96
|
+
|
|
97
|
+
/** What an `action` plan receives from the host when it runs. */
|
|
98
|
+
export type ActionContext = Readonly<{
|
|
99
|
+
workspace?: WorkspaceHost;
|
|
100
|
+
/**
|
|
101
|
+
* Called after a successful `write_file`/`edit_file` with the
|
|
102
|
+
* project-relative path. Returned text (for example typecheck
|
|
103
|
+
* diagnostics) is appended to the tool result.
|
|
104
|
+
*/
|
|
105
|
+
onWorkspaceWrite?: (path: string) => Promise<string | void> | string | void;
|
|
106
|
+
web?: WebHost;
|
|
107
|
+
signal?: AbortSignal;
|
|
108
|
+
}>;
|
|
109
|
+
|
|
110
|
+
export type ActionResult = Readonly<{
|
|
111
|
+
/** What the model reads; already bounded by the tool. */
|
|
112
|
+
content: string;
|
|
113
|
+
/** One line for the activity card. */
|
|
114
|
+
summary: string;
|
|
115
|
+
/** True when something outside the score changed (a file write). */
|
|
116
|
+
mutated?: boolean;
|
|
117
|
+
}>;
|
|
33
118
|
|
|
34
119
|
export type ToolContext = Readonly<{
|
|
35
120
|
score: TrackScore;
|
|
@@ -827,8 +912,320 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
827
912
|
return { kind: "explain", text, summary: text.slice(0, 80) };
|
|
828
913
|
},
|
|
829
914
|
},
|
|
915
|
+
{
|
|
916
|
+
name: "list_files",
|
|
917
|
+
description:
|
|
918
|
+
"List a project directory (default: the project root): directories first, then files with size and mtime. At most 500 entries; .dawg/ is hidden.",
|
|
919
|
+
parameters: {
|
|
920
|
+
type: "object",
|
|
921
|
+
properties: { path: pathSchema("Project-relative directory") },
|
|
922
|
+
additionalProperties: false,
|
|
923
|
+
},
|
|
924
|
+
plan(args, context) {
|
|
925
|
+
const path = optionalPath(args);
|
|
926
|
+
return {
|
|
927
|
+
kind: "action",
|
|
928
|
+
summary: `list ${path || "."}`,
|
|
929
|
+
run: async (action) => {
|
|
930
|
+
const scope = workspaceScope(action, context);
|
|
931
|
+
const result = await listFiles(scope, path);
|
|
932
|
+
return { content: result.text, summary: result.summary };
|
|
933
|
+
},
|
|
934
|
+
};
|
|
935
|
+
},
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
name: "read_file",
|
|
939
|
+
description: `Read a UTF-8 text file from the project by line range (offset is 1-based, limit is a line count). Output is capped at ${WORKSPACE_LIMITS.maxReadBytes / 1024} KiB; binary files report size and type.`,
|
|
940
|
+
parameters: {
|
|
941
|
+
type: "object",
|
|
942
|
+
properties: {
|
|
943
|
+
path: pathSchema("Project-relative file"),
|
|
944
|
+
offset: {
|
|
945
|
+
type: "integer",
|
|
946
|
+
minimum: 1,
|
|
947
|
+
description: "First line (1-based)",
|
|
948
|
+
},
|
|
949
|
+
limit: {
|
|
950
|
+
type: "integer",
|
|
951
|
+
minimum: 1,
|
|
952
|
+
maximum: WORKSPACE_LIMITS.maxReadLines,
|
|
953
|
+
description: "Number of lines",
|
|
954
|
+
},
|
|
955
|
+
},
|
|
956
|
+
required: ["path"],
|
|
957
|
+
additionalProperties: false,
|
|
958
|
+
},
|
|
959
|
+
plan(args, context) {
|
|
960
|
+
const path = requiredPath(args);
|
|
961
|
+
const offset = optionalNumber(args, "offset", { min: 1, integer: true });
|
|
962
|
+
const limit = optionalNumber(args, "limit", {
|
|
963
|
+
min: 1,
|
|
964
|
+
max: WORKSPACE_LIMITS.maxReadLines,
|
|
965
|
+
integer: true,
|
|
966
|
+
});
|
|
967
|
+
return {
|
|
968
|
+
kind: "action",
|
|
969
|
+
summary: `read ${path}`,
|
|
970
|
+
run: async (action) => {
|
|
971
|
+
const scope = workspaceScope(action, context);
|
|
972
|
+
const result = await readFile(scope, path, {
|
|
973
|
+
...(offset !== undefined ? { offset } : {}),
|
|
974
|
+
...(limit !== undefined ? { limit } : {}),
|
|
975
|
+
});
|
|
976
|
+
return { content: result.text, summary: result.summary };
|
|
977
|
+
},
|
|
978
|
+
};
|
|
979
|
+
},
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
name: "write_file",
|
|
983
|
+
description: `Create or replace a file atomically with the full content (at most ${WORKSPACE_LIMITS.maxWriteBytes / 1024 / 1024} MiB). Writable: song.ts and the focused track's tracks/<slug>/ directory, where notes.md is your scratchpad. Parent directories are created.`,
|
|
984
|
+
parameters: {
|
|
985
|
+
type: "object",
|
|
986
|
+
properties: {
|
|
987
|
+
path: pathSchema("Project-relative file"),
|
|
988
|
+
content: { type: "string", description: "Entire new file content" },
|
|
989
|
+
},
|
|
990
|
+
required: ["path", "content"],
|
|
991
|
+
additionalProperties: false,
|
|
992
|
+
},
|
|
993
|
+
plan(args, context) {
|
|
994
|
+
const path = requiredPath(args);
|
|
995
|
+
if (typeof args.content !== "string")
|
|
996
|
+
throw new ToolArgumentError("content must be a string");
|
|
997
|
+
const content = args.content;
|
|
998
|
+
return {
|
|
999
|
+
kind: "action",
|
|
1000
|
+
summary: `write ${path}`,
|
|
1001
|
+
run: async (action) => {
|
|
1002
|
+
const scope = workspaceScope(action, context);
|
|
1003
|
+
const result = await writeFile(scope, path, content);
|
|
1004
|
+
return {
|
|
1005
|
+
content: await afterWrite(action, result.rel, result.text),
|
|
1006
|
+
summary: result.summary,
|
|
1007
|
+
mutated: true,
|
|
1008
|
+
};
|
|
1009
|
+
},
|
|
1010
|
+
};
|
|
1011
|
+
},
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
name: "edit_file",
|
|
1015
|
+
description:
|
|
1016
|
+
"Replace one exact occurrence of old with new in a writable file (same scope as write_file). old must match exactly once; otherwise the result tells you the count. Prefer this over many note tools for large edits or restructuring of tracks/<slug>/track.ts.",
|
|
1017
|
+
parameters: {
|
|
1018
|
+
type: "object",
|
|
1019
|
+
properties: {
|
|
1020
|
+
path: pathSchema("Project-relative file"),
|
|
1021
|
+
old: {
|
|
1022
|
+
type: "string",
|
|
1023
|
+
minLength: 1,
|
|
1024
|
+
description: "Exact text to replace",
|
|
1025
|
+
},
|
|
1026
|
+
new: { type: "string", description: "Replacement text" },
|
|
1027
|
+
},
|
|
1028
|
+
required: ["path", "old", "new"],
|
|
1029
|
+
additionalProperties: false,
|
|
1030
|
+
},
|
|
1031
|
+
plan(args, context) {
|
|
1032
|
+
const path = requiredPath(args);
|
|
1033
|
+
if (typeof args.old !== "string" || args.old.length === 0)
|
|
1034
|
+
throw new ToolArgumentError("old must be a non-empty string");
|
|
1035
|
+
if (typeof args.new !== "string")
|
|
1036
|
+
throw new ToolArgumentError("new must be a string");
|
|
1037
|
+
const { old: oldText, new: newText } = args;
|
|
1038
|
+
return {
|
|
1039
|
+
kind: "action",
|
|
1040
|
+
summary: `edit ${path}`,
|
|
1041
|
+
run: async (action) => {
|
|
1042
|
+
const scope = workspaceScope(action, context);
|
|
1043
|
+
const result = await editFile(scope, path, oldText, newText);
|
|
1044
|
+
return {
|
|
1045
|
+
content: await afterWrite(action, result.rel, result.text),
|
|
1046
|
+
summary: result.summary,
|
|
1047
|
+
mutated: true,
|
|
1048
|
+
};
|
|
1049
|
+
},
|
|
1050
|
+
};
|
|
1051
|
+
},
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
name: "web_search",
|
|
1055
|
+
description: `Search the web; returns up to ${SEARCH_LIMITS.defaultCount} results as title, url and snippet. Follow up with fetch_url to read a page.`,
|
|
1056
|
+
parameters: {
|
|
1057
|
+
type: "object",
|
|
1058
|
+
properties: {
|
|
1059
|
+
query: { type: "string", maxLength: SEARCH_LIMITS.maxQueryChars },
|
|
1060
|
+
count: { type: "integer", minimum: 1, maximum: SEARCH_LIMITS.maxCount },
|
|
1061
|
+
},
|
|
1062
|
+
required: ["query"],
|
|
1063
|
+
additionalProperties: false,
|
|
1064
|
+
},
|
|
1065
|
+
plan(args) {
|
|
1066
|
+
if (typeof args.query !== "string" || args.query.trim().length === 0)
|
|
1067
|
+
throw new ToolArgumentError("query must be a non-empty string");
|
|
1068
|
+
if (args.query.length > SEARCH_LIMITS.maxQueryChars)
|
|
1069
|
+
throw new ToolArgumentError(
|
|
1070
|
+
`query must be at most ${SEARCH_LIMITS.maxQueryChars} characters`,
|
|
1071
|
+
);
|
|
1072
|
+
const query = args.query.trim();
|
|
1073
|
+
const count = optionalNumber(args, "count", {
|
|
1074
|
+
min: 1,
|
|
1075
|
+
max: SEARCH_LIMITS.maxCount,
|
|
1076
|
+
integer: true,
|
|
1077
|
+
});
|
|
1078
|
+
return {
|
|
1079
|
+
kind: "action",
|
|
1080
|
+
summary: `search ${query.slice(0, 60)}`,
|
|
1081
|
+
run: async (action) => {
|
|
1082
|
+
const web = action.web ?? {};
|
|
1083
|
+
const outcome = await webSearch(query, {
|
|
1084
|
+
...(count !== undefined ? { count } : {}),
|
|
1085
|
+
braveApiKey: web.braveApiKey ?? process.env.BRAVE_SEARCH_API_KEY,
|
|
1086
|
+
...(web.gatewayApiKey ? { gatewayApiKey: web.gatewayApiKey } : {}),
|
|
1087
|
+
...(web.gatewayBaseUrl
|
|
1088
|
+
? { gatewayBaseUrl: web.gatewayBaseUrl }
|
|
1089
|
+
: {}),
|
|
1090
|
+
searchTool: web.searchTool ?? process.env.DAWG_WEB_SEARCH,
|
|
1091
|
+
openRouterApiKey:
|
|
1092
|
+
web.openRouterApiKey ?? process.env.OPENROUTER_API_KEY,
|
|
1093
|
+
...(web.openRouterBaseUrl
|
|
1094
|
+
? { openRouterBaseUrl: web.openRouterBaseUrl }
|
|
1095
|
+
: {}),
|
|
1096
|
+
...(web.fetch ? { fetch: web.fetch } : {}),
|
|
1097
|
+
...(web.onSpend ? { onSpend: web.onSpend } : {}),
|
|
1098
|
+
...(action.signal ? { signal: action.signal } : {}),
|
|
1099
|
+
});
|
|
1100
|
+
const via = describeSearchProvider(outcome);
|
|
1101
|
+
return {
|
|
1102
|
+
content: formatSearchResults(outcome, query),
|
|
1103
|
+
summary: `searched via ${via}${outcome.fallbackFrom ? " (fallback)" : ""} · ${outcome.results.length} result${outcome.results.length === 1 ? "" : "s"}`,
|
|
1104
|
+
};
|
|
1105
|
+
},
|
|
1106
|
+
};
|
|
1107
|
+
},
|
|
1108
|
+
},
|
|
1109
|
+
{
|
|
1110
|
+
name: "fetch_url",
|
|
1111
|
+
description: `Fetch a public http(s) page and return its readable text (HTML reduced to headings, text and links; at most ${FETCH_LIMITS.maxOutputChars / 1024} KiB). Private and local addresses are refused.`,
|
|
1112
|
+
parameters: {
|
|
1113
|
+
type: "object",
|
|
1114
|
+
properties: {
|
|
1115
|
+
url: { type: "string", maxLength: FETCH_LIMITS.maxUrlChars },
|
|
1116
|
+
},
|
|
1117
|
+
required: ["url"],
|
|
1118
|
+
additionalProperties: false,
|
|
1119
|
+
},
|
|
1120
|
+
plan(args) {
|
|
1121
|
+
if (typeof args.url !== "string" || args.url.trim().length === 0)
|
|
1122
|
+
throw new ToolArgumentError("url must be a non-empty string");
|
|
1123
|
+
if (args.url.length > FETCH_LIMITS.maxUrlChars)
|
|
1124
|
+
throw new ToolArgumentError(
|
|
1125
|
+
`url must be at most ${FETCH_LIMITS.maxUrlChars} characters`,
|
|
1126
|
+
);
|
|
1127
|
+
const url = args.url.trim();
|
|
1128
|
+
let host = url;
|
|
1129
|
+
try {
|
|
1130
|
+
host = new URL(url).hostname;
|
|
1131
|
+
} catch {
|
|
1132
|
+
// admitUrl reports the diagnostic when the action runs.
|
|
1133
|
+
}
|
|
1134
|
+
return {
|
|
1135
|
+
kind: "action",
|
|
1136
|
+
summary: `fetch ${host.slice(0, 60)}`,
|
|
1137
|
+
run: async (action) => {
|
|
1138
|
+
const page = await fetchUrl(url, {
|
|
1139
|
+
...(action.web?.fetch ? { fetch: action.web.fetch } : {}),
|
|
1140
|
+
...(action.web?.lookup ? { lookup: action.web.lookup } : {}),
|
|
1141
|
+
...(action.signal ? { signal: action.signal } : {}),
|
|
1142
|
+
});
|
|
1143
|
+
return {
|
|
1144
|
+
content: formatFetchedPage(page),
|
|
1145
|
+
summary: `fetched ${new URL(page.url).hostname} (${page.bytes} bytes${page.truncated ? ", truncated" : ""})`,
|
|
1146
|
+
};
|
|
1147
|
+
},
|
|
1148
|
+
};
|
|
1149
|
+
},
|
|
1150
|
+
},
|
|
1151
|
+
...MEDIA_TOOLS,
|
|
830
1152
|
] satisfies AgentTool[]);
|
|
831
1153
|
|
|
1154
|
+
/** Errors an `action` plan may raise that are safe to show to the model. */
|
|
1155
|
+
export function isActionDiagnostic(error: unknown): boolean {
|
|
1156
|
+
return (
|
|
1157
|
+
error instanceof WorkspaceError ||
|
|
1158
|
+
error instanceof WebError ||
|
|
1159
|
+
error instanceof ToolArgumentError
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const MAX_HOOK_CHARS = 4_000;
|
|
1164
|
+
|
|
1165
|
+
async function afterWrite(
|
|
1166
|
+
action: ActionContext,
|
|
1167
|
+
rel: string,
|
|
1168
|
+
text: string,
|
|
1169
|
+
): Promise<string> {
|
|
1170
|
+
if (!action.onWorkspaceWrite) return text;
|
|
1171
|
+
try {
|
|
1172
|
+
const note = await action.onWorkspaceWrite(rel);
|
|
1173
|
+
return typeof note === "string" && note.trim()
|
|
1174
|
+
? `${text}\n${note.trim().slice(0, MAX_HOOK_CHARS)}`
|
|
1175
|
+
: text;
|
|
1176
|
+
} catch (error) {
|
|
1177
|
+
const message = (
|
|
1178
|
+
error instanceof Error ? error.message : String(error)
|
|
1179
|
+
).slice(0, MAX_HOOK_CHARS);
|
|
1180
|
+
return `${text}\nafter write: ${message}`;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/** The slug of the focused track: its name, or its id for a draft track. */
|
|
1185
|
+
export function focusedTrackSlug(
|
|
1186
|
+
context: Pick<ToolContext, "score" | "focusedTrackId">,
|
|
1187
|
+
): string {
|
|
1188
|
+
const track = context.score.tracks.find(
|
|
1189
|
+
(candidate) => candidate.id === context.focusedTrackId,
|
|
1190
|
+
);
|
|
1191
|
+
return trackSlug(track?.name ?? context.focusedTrackId);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function workspaceScope(
|
|
1195
|
+
action: ActionContext,
|
|
1196
|
+
context: ToolContext,
|
|
1197
|
+
): WorkspaceScope {
|
|
1198
|
+
if (!action.workspace)
|
|
1199
|
+
throw new WorkspaceError("file tools are unavailable in this session");
|
|
1200
|
+
return { root: action.workspace.root, trackSlug: focusedTrackSlug(context) };
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function pathSchema(description: string) {
|
|
1204
|
+
return {
|
|
1205
|
+
type: "string",
|
|
1206
|
+
maxLength: WORKSPACE_LIMITS.maxPathChars,
|
|
1207
|
+
description,
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function requiredPath(args: Record<string, unknown>): string {
|
|
1212
|
+
const path = optionalPath(args);
|
|
1213
|
+
if (path === undefined || path.length === 0)
|
|
1214
|
+
throw new ToolArgumentError("path is required");
|
|
1215
|
+
return path;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function optionalPath(args: Record<string, unknown>): string | undefined {
|
|
1219
|
+
if (args.path === undefined) return undefined;
|
|
1220
|
+
if (typeof args.path !== "string")
|
|
1221
|
+
throw new ToolArgumentError("path must be a string");
|
|
1222
|
+
if (args.path.length > WORKSPACE_LIMITS.maxPathChars)
|
|
1223
|
+
throw new ToolArgumentError(
|
|
1224
|
+
`path must be at most ${WORKSPACE_LIMITS.maxPathChars} characters`,
|
|
1225
|
+
);
|
|
1226
|
+
return args.path;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
832
1229
|
const TOOLS_BY_NAME = new Map(AGENT_TOOLS.map((tool) => [tool.name, tool]));
|
|
833
1230
|
|
|
834
1231
|
export function findAgentTool(
|