dopus 0.2.6 → 0.2.7
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/README.md +26 -4
- package/dist/cli.js +304 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/{version-Dm8IBEUF.js → version-DqzDElrt.js} +1 -1
- package/package.json +1 -1
- package/templates/history.json +35 -0
- package/templates/ui/agent-chat.css +15 -0
- package/templates/ui/agent-chat.tsx +28 -3
package/README.md
CHANGED
|
@@ -83,6 +83,27 @@ dopus init [--provider openai|anthropic|google] [--model <id>]
|
|
|
83
83
|
|
|
84
84
|
`--yes` runs without prompts. It needs `--provider` and skips the API key prompt, so add the key to `.env.local` yourself afterwards.
|
|
85
85
|
|
|
86
|
+
### Updating
|
|
87
|
+
|
|
88
|
+
`init` copies files into your project, so a new dopus version does not change them by itself. After upgrading the
|
|
89
|
+
package, bring them up to date:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pnpm add dopus@latest
|
|
93
|
+
pnpm dopus update # --dry-run to only see what would change
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`update` handles the files `init` copied: the chat UI (`agent-chat.tsx`, `agent-chat.css`), the route and the preset
|
|
97
|
+
prompt. It never touches `agent.config.ts`, `agent/prompts/app.ts` or `.env.local`.
|
|
98
|
+
|
|
99
|
+
- A file you never edited (it matches a template some dopus version shipped) is replaced.
|
|
100
|
+
- A file you edited is never replaced silently. You choose: see the diff, keep yours and get the new template next to
|
|
101
|
+
it as `<file>.new` to merge by hand, replace it, or skip. `--yes` always takes the `.new` route; `--force` replaces.
|
|
102
|
+
- Packages the new chat UI needs are installed (`--no-install` prints the command instead).
|
|
103
|
+
|
|
104
|
+
Commit first, so `git diff` shows exactly what changed. The [changelog](CHANGELOG.md) says what each version changed
|
|
105
|
+
and anything to do in `agent.config.ts`.
|
|
106
|
+
|
|
86
107
|
## Tools
|
|
87
108
|
|
|
88
109
|
```ts
|
|
@@ -206,7 +227,9 @@ Anthropic gets an explicit cache breakpoint after layer 3. OpenAI and Gemini cac
|
|
|
206
227
|
Both accept `api`, `suggestions`, `labels` (every UI string, for translation), `className`, and `channel` /
|
|
207
228
|
`maxJobFollowUps` for [background jobs](#background-jobs).
|
|
208
229
|
|
|
209
|
-
The UI shows each tool call as it runs.
|
|
230
|
+
The UI shows each tool call as it runs. When a reply finishes while the widget is closed (for example the agent
|
|
231
|
+
continuing after a [background job](#background-jobs)), the launcher shows a dot until it is opened (`labels.unread`
|
|
232
|
+
for screen readers; `onReply` if you want your own signal). It renders an approve/decline card for confirmed tools and continues automatically once the user answers. When a tool has run, it calls `router.refresh()` so server components show the changed data. Closing the widget keeps the conversation, and a "New chat" button clears it. Assistant replies render as Markdown (tables, lists, code) through `react-markdown` with `remark-gfm`, which `init` installs. Raw HTML is never rendered, images show only their alt text, and links keep only http(s), mailto and relative URLs. If a request is over the size limit (413), the UI says so and offers a new chat instead of a useless retry. It also has stop and retry buttons, and it does not send on Enter while an IME (Korean, Japanese, Chinese) is still composing.
|
|
210
233
|
|
|
211
234
|
Styling is one plain CSS file (`agent-chat.css`) whose classes all start with `dopus-`, themed through `--dopus-*` variables, with dark mode included. It is deliberately not a CSS Module: create-next-app's Tailwind setup routes every `*.css` file, modules included, through its own Turbopack loader, which left module classes unstyled. It needs no Tailwind and no component library. The file is yours, so restyle it or swap in your own components.
|
|
212
235
|
|
|
@@ -490,8 +513,6 @@ dopus is pre-1.0, so expect breaking changes before 1.0. It is tested on Next.js
|
|
|
490
513
|
|
|
491
514
|
Planned next, within the lightweight scope above:
|
|
492
515
|
|
|
493
|
-
- a command that updates your copies of the chat UI and route when the templates change (it also brings background
|
|
494
|
-
job support into projects created before it);
|
|
495
516
|
- tool input schemas inferred from TypeScript types.
|
|
496
517
|
|
|
497
518
|
## Development
|
|
@@ -501,7 +522,8 @@ pnpm install
|
|
|
501
522
|
pnpm build && pnpm lint && pnpm typecheck && pnpm test
|
|
502
523
|
pnpm example:build && pnpm example:e2e # next start + scripted model
|
|
503
524
|
pnpm example:live # real model from examples/next-app/.env.local (costs cents)
|
|
504
|
-
pnpm smoke # packed tarball → fresh app →
|
|
525
|
+
pnpm smoke # packed tarball → fresh app → init (older release) → update → next build
|
|
526
|
+
pnpm templates:history # after changing a template: record its hash for `dopus update`
|
|
505
527
|
pnpm aws:emu # OpenNext build → local Lambda streaming invoke
|
|
506
528
|
pnpm cf:e2e # dopus infra cloudflare → wrangler dev (workerd) → e2e
|
|
507
529
|
pnpm gcp:e2e # dopus infra gcp → docker build/run (Cloud Run style) → e2e
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as PROVIDERS, t as VERSION } from "./version-
|
|
2
|
+
import { n as PROVIDERS, t as VERSION } from "./version-DqzDElrt.js";
|
|
3
3
|
import { execFileSync, spawn } from "node:child_process";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
import * as p from "@clack/prompts";
|
|
6
6
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { dirname, join, relative, resolve } from "node:path";
|
|
8
|
-
import { randomBytes } from "node:crypto";
|
|
8
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
// src/init/detect.ts
|
|
11
11
|
var InitError = class extends Error {
|
|
@@ -808,11 +808,249 @@ async function runInfraGcp(options, io) {
|
|
|
808
808
|
].join("\n"), "Cloud Run notes");
|
|
809
809
|
return result;
|
|
810
810
|
}
|
|
811
|
+
// src/update/diff.ts
|
|
812
|
+
/** Line diff (LCS) in unified style with 3 lines of context. Small and dependency-free; templates are short. */
|
|
813
|
+
function unifiedDiff(before, after, context = 3) {
|
|
814
|
+
const a = before.replace(/\r\n/g, "\n").split("\n");
|
|
815
|
+
const b = after.replace(/\r\n/g, "\n").split("\n");
|
|
816
|
+
const n = a.length;
|
|
817
|
+
const m = b.length;
|
|
818
|
+
if (n * m > 4e6) return "(files too large to diff here)";
|
|
819
|
+
const table = new Uint32Array((n + 1) * (m + 1));
|
|
820
|
+
const lcs = (i, j) => table[i * (m + 1) + j] ?? 0;
|
|
821
|
+
for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) table[i * (m + 1) + j] = a[i] === b[j] ? lcs(i + 1, j + 1) + 1 : Math.max(lcs(i + 1, j), lcs(i, j + 1));
|
|
822
|
+
const lines = [];
|
|
823
|
+
let i = 0;
|
|
824
|
+
let j = 0;
|
|
825
|
+
while (i < n || j < m) if (i < n && j < m && a[i] === b[j]) {
|
|
826
|
+
lines.push({
|
|
827
|
+
op: " ",
|
|
828
|
+
text: a[i] ?? ""
|
|
829
|
+
});
|
|
830
|
+
i++;
|
|
831
|
+
j++;
|
|
832
|
+
} else if (i < n && (j >= m || lcs(i + 1, j) >= lcs(i, j + 1))) {
|
|
833
|
+
lines.push({
|
|
834
|
+
op: "-",
|
|
835
|
+
text: a[i] ?? ""
|
|
836
|
+
});
|
|
837
|
+
i++;
|
|
838
|
+
} else {
|
|
839
|
+
lines.push({
|
|
840
|
+
op: "+",
|
|
841
|
+
text: b[j] ?? ""
|
|
842
|
+
});
|
|
843
|
+
j++;
|
|
844
|
+
}
|
|
845
|
+
const changed = [];
|
|
846
|
+
lines.forEach((line, index) => {
|
|
847
|
+
if (line.op !== " ") changed.push(index);
|
|
848
|
+
});
|
|
849
|
+
const out = [];
|
|
850
|
+
let start = 0;
|
|
851
|
+
while (start < changed.length) {
|
|
852
|
+
let end = start;
|
|
853
|
+
while (end + 1 < changed.length && (changed[end + 1] ?? 0) - (changed[end] ?? 0) <= context * 2) end++;
|
|
854
|
+
const from = Math.max(0, (changed[start] ?? 0) - context);
|
|
855
|
+
const to = Math.min(lines.length - 1, (changed[end] ?? 0) + context);
|
|
856
|
+
out.push("@@");
|
|
857
|
+
for (const line of lines.slice(from, to + 1)) out.push(`${line.op}${line.text}`);
|
|
858
|
+
start = end + 1;
|
|
859
|
+
}
|
|
860
|
+
return out.join("\n");
|
|
861
|
+
}
|
|
862
|
+
// src/update/history.ts
|
|
863
|
+
/** The generated route imports the agent config by a path that differs per project. */
|
|
864
|
+
const CONFIG_IMPORT = /^import agent from ['"]([^'"]+)['"]/m;
|
|
865
|
+
/** Must match normalize() in scripts/template-history.mjs. */
|
|
866
|
+
function normalize(text) {
|
|
867
|
+
return text.replace(/\r\n/g, "\n").replace(CONFIG_IMPORT, "import agent from '<config>'");
|
|
868
|
+
}
|
|
869
|
+
function templateHash(text) {
|
|
870
|
+
return createHash("sha256").update(normalize(text)).digest("hex").slice(0, 16);
|
|
871
|
+
}
|
|
872
|
+
let cached;
|
|
873
|
+
function templateHistory() {
|
|
874
|
+
cached ??= JSON.parse(readTemplate("history.json"));
|
|
875
|
+
return cached;
|
|
876
|
+
}
|
|
877
|
+
/** The dopus version whose untouched template this is, or undefined if the user changed it. */
|
|
878
|
+
function shippedIn(key, text) {
|
|
879
|
+
return templateHistory()[key]?.[templateHash(text)];
|
|
880
|
+
}
|
|
881
|
+
// src/update/run.ts
|
|
882
|
+
/** Packages the chat UI imports (the rest were added by `init` and are needed by the route too). */
|
|
883
|
+
const UI_DEPENDENCIES = [
|
|
884
|
+
["@ai-sdk/react", "@ai-sdk/react@^4"],
|
|
885
|
+
["react-markdown", "react-markdown@^10"],
|
|
886
|
+
["remark-gfm", "remark-gfm@^4"]
|
|
887
|
+
];
|
|
888
|
+
/** Which preset an existing preset.ts came from: by its hash, else the one it shares most lines with. */
|
|
889
|
+
function presetKey(text) {
|
|
890
|
+
const keys = PRESETS.map((preset) => `prompts/presets/${preset.id}.ts`);
|
|
891
|
+
const exact = keys.find((key) => shippedIn(key, text));
|
|
892
|
+
if (exact) return exact;
|
|
893
|
+
const lines = new Set(text.split("\n").map((line) => line.trim()));
|
|
894
|
+
const shared = (key) => readTemplate(key).split("\n").filter((line) => line.trim() && lines.has(line.trim())).length;
|
|
895
|
+
return keys.reduce((best, key) => shared(key) > shared(best) ? key : best);
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Brings the files `dopus init` copied (chat UI, route, preset prompt) up to this version's templates.
|
|
899
|
+
* Untouched copies of an older version are replaced; copies the user edited are never replaced without
|
|
900
|
+
* asking. agent.config.ts, app.ts and .env.local are the user's and are never touched.
|
|
901
|
+
*/
|
|
902
|
+
async function runUpdate(options, io) {
|
|
903
|
+
const project = detectProject(options.cwd);
|
|
904
|
+
const { root } = project;
|
|
905
|
+
const at = (path) => join(project.configDir, path);
|
|
906
|
+
const read = (file) => readFileSync(join(root, file), "utf8");
|
|
907
|
+
const exists = (file) => existsSync(join(root, file));
|
|
908
|
+
const result = {
|
|
909
|
+
files: [],
|
|
910
|
+
installed: [],
|
|
911
|
+
missingDependencies: []
|
|
912
|
+
};
|
|
913
|
+
const chatFile = at("components/agent/agent-chat.tsx");
|
|
914
|
+
const routeFile = join(project.appDir, "api", "agent", "route.ts");
|
|
915
|
+
const presetFile = at("agent/prompts/preset.ts");
|
|
916
|
+
const targets = [{
|
|
917
|
+
file: chatFile,
|
|
918
|
+
key: "ui/agent-chat.tsx",
|
|
919
|
+
latest: readTemplate("ui/agent-chat.tsx")
|
|
920
|
+
}, {
|
|
921
|
+
file: at("components/agent/agent-chat.css"),
|
|
922
|
+
key: "ui/agent-chat.css",
|
|
923
|
+
latest: readTemplate("ui/agent-chat.css"),
|
|
924
|
+
create: exists(chatFile)
|
|
925
|
+
}];
|
|
926
|
+
if (exists(routeFile)) {
|
|
927
|
+
const configImport = read(routeFile).match(CONFIG_IMPORT)?.[1] ?? toImportPath(routeFile, at("agent.config"));
|
|
928
|
+
targets.push({
|
|
929
|
+
file: routeFile,
|
|
930
|
+
key: "route",
|
|
931
|
+
latest: routeTemplate(configImport)
|
|
932
|
+
});
|
|
933
|
+
} else result.files.push({
|
|
934
|
+
file: routeFile,
|
|
935
|
+
status: "missing"
|
|
936
|
+
});
|
|
937
|
+
if (exists(presetFile)) {
|
|
938
|
+
const key = presetKey(read(presetFile));
|
|
939
|
+
targets.push({
|
|
940
|
+
file: presetFile,
|
|
941
|
+
key,
|
|
942
|
+
latest: readTemplate(key)
|
|
943
|
+
});
|
|
944
|
+
} else result.files.push({
|
|
945
|
+
file: presetFile,
|
|
946
|
+
status: "missing"
|
|
947
|
+
});
|
|
948
|
+
const write = (file, content) => {
|
|
949
|
+
if (!options.dryRun) writeFileSync(join(root, file), content);
|
|
950
|
+
};
|
|
951
|
+
for (const target of targets) {
|
|
952
|
+
const { file, latest } = target;
|
|
953
|
+
if (!exists(file)) {
|
|
954
|
+
if (target.create) {
|
|
955
|
+
write(file, latest);
|
|
956
|
+
result.files.push({
|
|
957
|
+
file,
|
|
958
|
+
status: "created"
|
|
959
|
+
});
|
|
960
|
+
} else result.files.push({
|
|
961
|
+
file,
|
|
962
|
+
status: "missing"
|
|
963
|
+
});
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
const current = read(file);
|
|
967
|
+
if (normalize(current) === normalize(latest)) {
|
|
968
|
+
result.files.push({
|
|
969
|
+
file,
|
|
970
|
+
status: "up-to-date"
|
|
971
|
+
});
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
const from = shippedIn(target.key, current);
|
|
975
|
+
if (from) {
|
|
976
|
+
write(file, latest);
|
|
977
|
+
result.files.push({
|
|
978
|
+
file,
|
|
979
|
+
status: "updated",
|
|
980
|
+
from
|
|
981
|
+
});
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
let choice;
|
|
985
|
+
if (options.dryRun) {
|
|
986
|
+
result.files.push({
|
|
987
|
+
file,
|
|
988
|
+
status: "modified"
|
|
989
|
+
});
|
|
990
|
+
continue;
|
|
991
|
+
} else if (options.force) choice = "overwrite";
|
|
992
|
+
else if (options.yes) choice = "new";
|
|
993
|
+
else do {
|
|
994
|
+
choice = await io.choose(`${file} has your changes. Replace it with the new template?`, [
|
|
995
|
+
{
|
|
996
|
+
value: "diff",
|
|
997
|
+
label: "Show the diff (yours → new)"
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
value: "new",
|
|
1001
|
+
label: `Keep yours, save the new one as ${file}.new`
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
value: "overwrite",
|
|
1005
|
+
label: "Replace (your changes are lost)"
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
value: "skip",
|
|
1009
|
+
label: "Keep yours, skip"
|
|
1010
|
+
}
|
|
1011
|
+
]);
|
|
1012
|
+
if (choice === "diff") io.diff(unifiedDiff(current, latest));
|
|
1013
|
+
} while (choice === "diff");
|
|
1014
|
+
if (choice === "overwrite") {
|
|
1015
|
+
write(file, latest);
|
|
1016
|
+
result.files.push({
|
|
1017
|
+
file,
|
|
1018
|
+
status: "overwritten"
|
|
1019
|
+
});
|
|
1020
|
+
} else if (choice === "new") {
|
|
1021
|
+
write(`${file}.new`, latest);
|
|
1022
|
+
result.files.push({
|
|
1023
|
+
file,
|
|
1024
|
+
status: "saved-new"
|
|
1025
|
+
});
|
|
1026
|
+
} else result.files.push({
|
|
1027
|
+
file,
|
|
1028
|
+
status: "kept"
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
const chatReplaced = result.files.some((f) => f.file === chatFile && (f.status === "updated" || f.status === "overwritten"));
|
|
1032
|
+
const oldModule = at("components/agent/agent-chat.module.css");
|
|
1033
|
+
if (chatReplaced && exists(oldModule)) io.warn(`${oldModule} is no longer used by the chat UI (it now imports agent-chat.css). Delete it.`);
|
|
1034
|
+
if (exists(chatFile)) {
|
|
1035
|
+
const missing = UI_DEPENDENCIES.filter(([name]) => !project.dependencies[name]).map(([, spec]) => spec);
|
|
1036
|
+
if (missing.length > 0) {
|
|
1037
|
+
if (!options.dryRun && options.install !== false && (options.yes || options.force || await io.confirm(`Install ${missing.join(", ")}?`, true))) {
|
|
1038
|
+
await io.install(project.packageManager, missing, root);
|
|
1039
|
+
result.installed = missing;
|
|
1040
|
+
} else {
|
|
1041
|
+
result.missingDependencies = missing;
|
|
1042
|
+
io.warn(`The chat UI needs: ${project.packageManager} ${installArgs(project.packageManager, missing).join(" ")}`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return result;
|
|
1047
|
+
}
|
|
811
1048
|
// src/cli.ts
|
|
812
1049
|
const HELP = `dopus ${VERSION}
|
|
813
1050
|
|
|
814
1051
|
Usage:
|
|
815
1052
|
dopus init [options] Add an AI agent to this Next.js app
|
|
1053
|
+
dopus update [options] Update the chat UI, route and preset prompt that init copied
|
|
816
1054
|
dopus infra aws [options] Prepare the agent route for AWS Lambda (OpenNext streaming + SST snippet)
|
|
817
1055
|
dopus infra cloudflare Prepare the app for Cloudflare Workers (@opennextjs/cloudflare)
|
|
818
1056
|
dopus infra gcp Prepare the app for Cloud Run (standalone build + Dockerfile)
|
|
@@ -826,6 +1064,13 @@ Options for init:
|
|
|
826
1064
|
--force Overwrite generated files that already exist
|
|
827
1065
|
--cwd <dir> Project root (default: current directory)
|
|
828
1066
|
|
|
1067
|
+
Options for update (first install the new version: pnpm add dopus@latest):
|
|
1068
|
+
--dry-run Show what would change; write nothing
|
|
1069
|
+
-y, --yes Do not ask: files you edited get the new template next to them as <file>.new
|
|
1070
|
+
--force Replace files you edited too
|
|
1071
|
+
--no-install Do not install dependencies
|
|
1072
|
+
--cwd <dir> Project root
|
|
1073
|
+
|
|
829
1074
|
Options for infra:
|
|
830
1075
|
-y, --yes Do not ask before installing @opennextjs/aws
|
|
831
1076
|
--no-install Print the install command instead
|
|
@@ -928,6 +1173,62 @@ async function init(args) {
|
|
|
928
1173
|
p.note(steps.map((s, i) => `${i + 1}. ${s}`).join("\n"), "Next steps");
|
|
929
1174
|
p.outro("Done.");
|
|
930
1175
|
}
|
|
1176
|
+
const STATUS = {
|
|
1177
|
+
"up-to-date": "up to date",
|
|
1178
|
+
updated: "updated",
|
|
1179
|
+
created: "created",
|
|
1180
|
+
modified: "has your changes: would ask (run without --dry-run)",
|
|
1181
|
+
overwritten: "replaced (your changes are gone)",
|
|
1182
|
+
kept: "kept your version",
|
|
1183
|
+
"saved-new": "kept your version; new template saved as .new next to it",
|
|
1184
|
+
missing: "not found (not created by dopus init?)"
|
|
1185
|
+
};
|
|
1186
|
+
function colorDiff(text) {
|
|
1187
|
+
if (!process.stdout.isTTY) return text;
|
|
1188
|
+
return text.split("\n").map((line) => line.startsWith("+") ? `\x1b[32m${line}\x1b[0m` : line.startsWith("-") ? `\x1b[31m${line}\x1b[0m` : line.startsWith("@@") ? `\x1b[36m${line}\x1b[0m` : line).join("\n");
|
|
1189
|
+
}
|
|
1190
|
+
async function update(args) {
|
|
1191
|
+
const { values } = parseArgs({
|
|
1192
|
+
args,
|
|
1193
|
+
options: {
|
|
1194
|
+
"dry-run": { type: "boolean" },
|
|
1195
|
+
yes: {
|
|
1196
|
+
type: "boolean",
|
|
1197
|
+
short: "y"
|
|
1198
|
+
},
|
|
1199
|
+
force: { type: "boolean" },
|
|
1200
|
+
install: {
|
|
1201
|
+
type: "boolean",
|
|
1202
|
+
default: true
|
|
1203
|
+
},
|
|
1204
|
+
"no-install": { type: "boolean" },
|
|
1205
|
+
cwd: { type: "string" }
|
|
1206
|
+
},
|
|
1207
|
+
allowNegative: true
|
|
1208
|
+
});
|
|
1209
|
+
p.intro(`dopus ${VERSION} · update${values["dry-run"] ? " (dry run)" : ""}`);
|
|
1210
|
+
p.log.info(`Templates come from the installed dopus (${VERSION}). For the newest, run \`pnpm add dopus@latest\` first.`);
|
|
1211
|
+
const updateIO = {
|
|
1212
|
+
...io,
|
|
1213
|
+
async choose(message, options) {
|
|
1214
|
+
return cancelled(await p.select({
|
|
1215
|
+
message,
|
|
1216
|
+
options
|
|
1217
|
+
}));
|
|
1218
|
+
},
|
|
1219
|
+
diff: (text) => console.log(colorDiff(text || "(no differences)"))
|
|
1220
|
+
};
|
|
1221
|
+
const result = await runUpdate({
|
|
1222
|
+
cwd: values.cwd ?? process.cwd(),
|
|
1223
|
+
dryRun: values["dry-run"],
|
|
1224
|
+
yes: values.yes,
|
|
1225
|
+
force: values.force,
|
|
1226
|
+
install: values["no-install"] ? false : values.install
|
|
1227
|
+
}, updateIO);
|
|
1228
|
+
p.note(result.files.map(({ file, status, from }) => `${file}: ${STATUS[status]}${from ? ` (was ${from})` : ""}`).join("\n"), "Files");
|
|
1229
|
+
if (result.files.some((f) => f.status === "saved-new")) p.log.info("Merge each .new file into yours, then delete it.");
|
|
1230
|
+
p.outro("What changed: https://github.com/akodm/dopus/blob/main/CHANGELOG.md");
|
|
1231
|
+
}
|
|
931
1232
|
async function infra(args) {
|
|
932
1233
|
const [target, ...rest] = args;
|
|
933
1234
|
const runners = {
|
|
@@ -970,6 +1271,7 @@ async function main() {
|
|
|
970
1271
|
if (rest.includes("--help") || rest.includes("-h")) return console.log(HELP);
|
|
971
1272
|
if (command === "init") return init(rest);
|
|
972
1273
|
if (command === "infra") return infra(rest);
|
|
1274
|
+
if (command === "update") return update(rest);
|
|
973
1275
|
console.log(HELP);
|
|
974
1276
|
if (command && command !== "--help" && command !== "-h") process.exitCode = 1;
|
|
975
1277
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -216,5 +216,5 @@ interface ProviderInfo {
|
|
|
216
216
|
}
|
|
217
217
|
/** Verified against @ai-sdk/openai@4.0.111, @ai-sdk/anthropic@4.0.99, @ai-sdk/google@4.0.114. */
|
|
218
218
|
export declare const PROVIDERS: Record<ProviderId, ProviderInfo>;
|
|
219
|
-
export declare const VERSION = "0.2.
|
|
219
|
+
export declare const VERSION = "0.2.7";
|
|
220
220
|
export type { AgentConfig, AgentUser, ClientContext, ConfirmOption, DopusTool, DopusToolSet, FinishEvent, HandlerOptions, PromptLayers, ProviderId, ProviderInfo, RuntimeContext, ToolCallEvent, ToolContext, ToolOptions };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as PROVIDERS, t as VERSION } from "./version-
|
|
1
|
+
import { n as PROVIDERS, t as VERSION } from "./version-DqzDElrt.js";
|
|
2
2
|
import { convertToModelMessages, createUIMessageStreamResponse, getToolName, isStepCount, isToolUIPart, safeValidateUIMessages, streamText, toUIMessageStream, tool } from "ai";
|
|
3
3
|
// src/job.ts
|
|
4
4
|
/**
|
package/package.json
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ui/agent-chat.tsx": {
|
|
3
|
+
"ff662fe56e84cfd0": "0.1.0",
|
|
4
|
+
"89475c1e29eb011b": "0.2.2",
|
|
5
|
+
"af6a34c9f15d4bef": "0.2.5",
|
|
6
|
+
"e0ff5f54b6823dba": "0.2.6",
|
|
7
|
+
"a3542945427a3dfa": "0.2.7"
|
|
8
|
+
},
|
|
9
|
+
"ui/agent-chat.module.css": {
|
|
10
|
+
"8ecfa2904dee75e9": "0.1.0"
|
|
11
|
+
},
|
|
12
|
+
"prompts/presets/general.ts": {
|
|
13
|
+
"10068289d8dfbf09": "0.1.0",
|
|
14
|
+
"fc75d0ecdeb38b7f": "0.2.2"
|
|
15
|
+
},
|
|
16
|
+
"prompts/presets/customer-support.ts": {
|
|
17
|
+
"2fdd5a26b25c23ae": "0.1.0",
|
|
18
|
+
"95bd1451466fb32b": "0.2.2"
|
|
19
|
+
},
|
|
20
|
+
"prompts/presets/internal-ops.ts": {
|
|
21
|
+
"2b150cc147d78e44": "0.1.0",
|
|
22
|
+
"c4b308fa69493fb8": "0.2.2",
|
|
23
|
+
"d5e08199cf6b45c2": "0.2.3"
|
|
24
|
+
},
|
|
25
|
+
"route": {
|
|
26
|
+
"17498e892a470ee4": "0.1.0",
|
|
27
|
+
"c44baa5927649535": "0.2.4"
|
|
28
|
+
},
|
|
29
|
+
"ui/agent-chat.css": {
|
|
30
|
+
"72f30cfb90fe5255": "0.2.2",
|
|
31
|
+
"65d193ef3df15ecc": "0.2.5",
|
|
32
|
+
"a33f338eb017ad66": "0.2.6",
|
|
33
|
+
"155c72a570d1ee5e": "0.2.7"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -257,6 +257,7 @@
|
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
.dopus-launcher {
|
|
260
|
+
position: relative;
|
|
260
261
|
width: 52px;
|
|
261
262
|
height: 52px;
|
|
262
263
|
border-radius: 50%;
|
|
@@ -268,6 +269,20 @@
|
|
|
268
269
|
box-shadow: 0 6px 20px rgb(0 0 0 / 0.2);
|
|
269
270
|
}
|
|
270
271
|
|
|
272
|
+
/* A reply arrived while the panel was closed (e.g. a background job's follow-up). */
|
|
273
|
+
.dopus-launcher[data-unread]::after {
|
|
274
|
+
content: "";
|
|
275
|
+
position: absolute;
|
|
276
|
+
top: 2px;
|
|
277
|
+
right: 2px;
|
|
278
|
+
width: 12px;
|
|
279
|
+
height: 12px;
|
|
280
|
+
border-radius: 50%;
|
|
281
|
+
/* The launcher sits outside .dopus-chat, where the --dopus-* variables live, so its colors are fixed too. */
|
|
282
|
+
background: #dc2626;
|
|
283
|
+
border: 2px solid #ffffff;
|
|
284
|
+
}
|
|
285
|
+
|
|
271
286
|
/* Assistant replies (Markdown). Compact spacing for a chat bubble. */
|
|
272
287
|
.dopus-markdown {
|
|
273
288
|
white-space: normal;
|
|
@@ -49,6 +49,7 @@ const DEFAULT_LABELS = {
|
|
|
49
49
|
jobRunning: 'Working in the background',
|
|
50
50
|
jobDone: 'Background task finished',
|
|
51
51
|
open: 'Open assistant',
|
|
52
|
+
unread: 'New reply',
|
|
52
53
|
close: 'Close assistant',
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -70,6 +71,8 @@ export interface AgentChatProps {
|
|
|
70
71
|
channel?: AgentChannel
|
|
71
72
|
/** Follow-up turns the agent may take in a row on finished jobs before waiting for the user. Default 3. */
|
|
72
73
|
maxJobFollowUps?: number
|
|
74
|
+
/** Called when the agent finishes a reply. `AgentWidget` uses it to mark the closed launcher as unread. */
|
|
75
|
+
onReply?: () => void
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
export interface AgentChannel {
|
|
@@ -278,9 +281,15 @@ export function AgentChat({
|
|
|
278
281
|
className,
|
|
279
282
|
channel,
|
|
280
283
|
maxJobFollowUps = 3,
|
|
284
|
+
onReply,
|
|
281
285
|
}: AgentChatProps) {
|
|
282
286
|
const labels = { ...DEFAULT_LABELS, ...labelOverrides }
|
|
283
287
|
const router = useRouter()
|
|
288
|
+
// useChat keeps the callbacks it was created with; read the latest onReply through a ref.
|
|
289
|
+
const onReplyRef = useRef(onReply)
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
onReplyRef.current = onReply
|
|
292
|
+
})
|
|
284
293
|
const [transport] = useState(
|
|
285
294
|
() =>
|
|
286
295
|
new DefaultChatTransport({
|
|
@@ -308,6 +317,7 @@ export function AgentChat({
|
|
|
308
317
|
if (message.parts.some((part) => isToolUIPart(part) && part.state === 'output-available')) {
|
|
309
318
|
router.refresh()
|
|
310
319
|
}
|
|
320
|
+
onReplyRef.current?.()
|
|
311
321
|
},
|
|
312
322
|
})
|
|
313
323
|
const [input, setInput] = useState('')
|
|
@@ -478,19 +488,34 @@ export function AgentChat({
|
|
|
478
488
|
/** Floating button that opens the chat in a panel. Put it once in your root layout. */
|
|
479
489
|
export function AgentWidget(props: AgentChatProps) {
|
|
480
490
|
const [open, setOpen] = useState(false)
|
|
491
|
+
// A reply finished while the panel was closed, e.g. the agent continuing after a background job.
|
|
492
|
+
const [unread, setUnread] = useState(false)
|
|
481
493
|
const labels = { ...DEFAULT_LABELS, ...props.labels }
|
|
482
494
|
return (
|
|
483
495
|
<div className="dopus-widget">
|
|
484
496
|
{/* Hidden rather than unmounted, so closing the panel keeps the conversation. */}
|
|
485
497
|
<div hidden={!open}>
|
|
486
|
-
<AgentChat
|
|
498
|
+
<AgentChat
|
|
499
|
+
{...props}
|
|
500
|
+
className="dopus-panel"
|
|
501
|
+
onReply={() => {
|
|
502
|
+
if (!open) setUnread(true)
|
|
503
|
+
props.onReply?.()
|
|
504
|
+
}}
|
|
505
|
+
/>
|
|
487
506
|
</div>
|
|
488
507
|
<button
|
|
489
508
|
type="button"
|
|
490
509
|
className="dopus-launcher"
|
|
491
510
|
aria-expanded={open}
|
|
492
|
-
aria-label={
|
|
493
|
-
|
|
511
|
+
aria-label={
|
|
512
|
+
open ? labels.close : unread ? `${labels.open} (${labels.unread})` : labels.open
|
|
513
|
+
}
|
|
514
|
+
data-unread={!open && unread ? '' : undefined}
|
|
515
|
+
onClick={() => {
|
|
516
|
+
setOpen((value) => !value)
|
|
517
|
+
setUnread(false)
|
|
518
|
+
}}
|
|
494
519
|
>
|
|
495
520
|
{open ? '×' : '✦'}
|
|
496
521
|
</button>
|