@selesai/code 0.13.7 → 0.13.9

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/core/settings-manager.d.ts +11 -0
  3. package/dist/core/settings-manager.js +48 -0
  4. package/dist/extensions/auto-session-name.test.ts +13 -1
  5. package/dist/extensions/auto-session-name.ts +8 -4
  6. package/dist/extensions/ponytail/package.json +1 -1
  7. package/dist/modes/rpc/rpc-client.d.ts +130 -4
  8. package/dist/modes/rpc/rpc-client.js +156 -4
  9. package/dist/modes/rpc/rpc-mode.js +484 -1
  10. package/dist/modes/rpc/rpc-types.d.ts +249 -1
  11. package/dist/skills/unlazy/CHANGELOG.md +9 -0
  12. package/dist/skills/unlazy/CONTRIBUTING.md +6 -9
  13. package/dist/skills/unlazy/README.md +9 -23
  14. package/dist/skills/unlazy/SECURITY.md +7 -17
  15. package/dist/skills/unlazy/SKILL.md +4 -12
  16. package/dist/skills/unlazy/references/dispatch.md +14 -17
  17. package/dist/skills/unlazy/references/gates.md +3 -3
  18. package/dist/skills/unlazy/references/orchestration.md +0 -1
  19. package/dist/skills/unlazy/references/parallel.md +4 -16
  20. package/dist/skills/unlazy/references/token-economy.md +1 -1
  21. package/dist/skills/unlazy/research/validation-protocol.md +1 -1
  22. package/dist/skills/unlazy/scripts/gate-check.mjs +4 -19
  23. package/dist/skills/unlazy/scripts/gate-lint.mjs +1 -1
  24. package/dist/skills/unlazy/scripts/lib/gates.mjs +1 -5
  25. package/dist/skills/unlazy/templates/PLAN.md +1 -1
  26. package/dist/skills/unlazy/tests/dispatch-tests.mjs +9 -194
  27. package/dist/skills/unlazy/tests/hardening-tests.mjs +2 -5
  28. package/dist/skills/unlazy/tests/lint-tests.mjs +1 -1
  29. package/dist/skills/unlazy/tests/run-tests.mjs +1 -195
  30. package/dist/skills/unlazy/tests/self-check.mjs +0 -13
  31. package/dist/skills/unlazy/tests/stress-tests.mjs +0 -191
  32. package/docs/rpc.md +1 -1
  33. package/package.json +1 -1
  34. package/dist/skills/unlazy/scripts/install-hooks.mjs +0 -206
  35. package/dist/skills/unlazy/scripts/stop-hook.mjs +0 -174
@@ -11,10 +11,23 @@
11
11
  * - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
12
12
  */
13
13
  import * as crypto from "node:crypto";
14
+ import { spawnSync } from "node:child_process";
15
+ import { chmodSync, copyFileSync, existsSync, unlinkSync } from "node:fs";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
14
18
  import { generateHandoff } from "../../core/handoff.js";
19
+ import { buildGitCommandPrompt } from "../../core/git-command.js";
20
+ import { KeybindingsManager } from "../../core/keybindings.js";
15
21
  import { flushRawStdout, takeOverStdout, waitForRawStdoutBackpressure, writeRawStdout, } from "../../core/output-guard.js";
22
+ import { ProjectTrustStore, hasTrustRequiringProjectResources } from "../../core/trust-manager.js";
23
+ import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
24
+ import { createSyntheticSourceInfo } from "../../core/source-info.js";
16
25
  import { killTrackedDetachedChildren } from "../../utils/shell.js";
26
+ import { getChangelogPath, parseChangelog } from "../../utils/changelog.js";
27
+ import { VERSION, getAgentDir, getBundledDefaultsDir, getShareViewerUrl } from "../../config.js";
28
+ import { openBrowser } from "../../utils/open-browser.js";
17
29
  import { theme } from "../interactive/theme/theme.js";
30
+ import { getAvailableThemesWithPaths, setRegisteredThemes } from "../interactive/theme/theme.js";
18
31
  import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js";
19
32
  /**
20
33
  * Run in RPC mode.
@@ -28,6 +41,43 @@ export async function runRpcMode(runtimeHost) {
28
41
  const output = (obj) => {
29
42
  writeRawStdout(serializeJsonLine(obj));
30
43
  };
44
+ /**
45
+ * Filter a session tree with the same semantics as the TUI tree selector
46
+ * (tree-selector.ts FilterMode). Hidden leaf pruning is preserved.
47
+ */
48
+ const filterTree = (nodes, mode) => {
49
+ const passes = (node) => {
50
+ const entry = node.entry;
51
+ const isSettingsEntry = entry.type === "label" ||
52
+ entry.type === "custom" ||
53
+ entry.type === "model_change" ||
54
+ entry.type === "thinking_level_change" ||
55
+ entry.type === "session_info";
56
+ switch (mode) {
57
+ case "user-only":
58
+ return entry.type === "message" && entry.message.role === "user";
59
+ case "no-tools":
60
+ return !isSettingsEntry && !(entry.type === "message" && entry.message.role === "toolResult");
61
+ case "labeled-only":
62
+ return node.label !== undefined;
63
+ case "all":
64
+ return true;
65
+ default:
66
+ return !isSettingsEntry;
67
+ }
68
+ };
69
+ const visit = (nodes) => {
70
+ const result = [];
71
+ for (const node of nodes) {
72
+ if (!passes(node))
73
+ continue;
74
+ const children = visit(node.children);
75
+ result.push({ ...node, children });
76
+ }
77
+ return result;
78
+ };
79
+ return visit(nodes);
80
+ };
31
81
  const success = (id, command, data) => {
32
82
  if (data === undefined) {
33
83
  return { id, type: "response", command, success: true };
@@ -576,7 +626,10 @@ export async function runRpcMode(runtimeHost) {
576
626
  }
577
627
  case "get_tree": {
578
628
  const sessionManager = session.sessionManager;
579
- return success(id, "get_tree", { tree: sessionManager.getTree(), leafId: sessionManager.getLeafId() });
629
+ const tree = sessionManager.getTree();
630
+ const filter = command.filter ?? "default";
631
+ const filtered = filter === "all" ? tree : filterTree(tree, filter);
632
+ return success(id, "get_tree", { tree: filtered, leafId: sessionManager.getLeafId() });
580
633
  }
581
634
  case "get_last_assistant_text": {
582
635
  const text = session.getLastAssistantText();
@@ -617,8 +670,438 @@ export async function runRpcMode(runtimeHost) {
617
670
  sourceInfo: template.sourceInfo,
618
671
  });
619
672
  }
673
+ // Built-in TUI commands: display-only, cannot be invoked via prompt.
674
+ for (const builtin of BUILTIN_SLASH_COMMANDS) {
675
+ commands.push({
676
+ name: builtin.name,
677
+ description: builtin.description
678
+ + (builtin.argumentHint ? ` ${builtin.argumentHint}` : ""),
679
+ source: "builtin",
680
+ sourceInfo: createSyntheticSourceInfo(`builtin:${builtin.name}`, { source: "builtin" }),
681
+ interactiveOnly: true,
682
+ });
683
+ }
620
684
  return success(id, "get_commands", { commands });
621
685
  }
686
+ // =================================================================
687
+ // Settings
688
+ // =================================================================
689
+ case "get_settings": {
690
+ const scope = command.scope ?? "effective";
691
+ const settings = scope === "global"
692
+ ? session.settingsManager.getGlobalSettings()
693
+ : scope === "project"
694
+ ? session.settingsManager.getProjectSettings()
695
+ : session.settingsManager.getEffectiveSettings();
696
+ return success(id, "get_settings", { scope, settings });
697
+ }
698
+ case "set_settings": {
699
+ const scope = command.scope ?? "global";
700
+ try {
701
+ if (scope === "project") {
702
+ session.settingsManager.setProjectSettings(command.values);
703
+ }
704
+ else {
705
+ session.settingsManager.setGlobalSettings(command.values);
706
+ }
707
+ await session.settingsManager.flush();
708
+ return success(id, "set_settings", { scope, values: structuredClone(command.values) });
709
+ }
710
+ catch (writeError) {
711
+ return error(id, "set_settings", writeError instanceof Error ? writeError.message : String(writeError));
712
+ }
713
+ }
714
+ case "factory_reset_settings": {
715
+ if (session.isStreaming) {
716
+ return error(id, "factory_reset_settings", "Wait for the current response to finish before resetting settings.");
717
+ }
718
+ if (session.isCompacting) {
719
+ return error(id, "factory_reset_settings", "Wait for compaction to finish before resetting settings.");
720
+ }
721
+ try {
722
+ const agentDir = getAgentDir();
723
+ const settingsPath = join(agentDir, "settings.json");
724
+ const bundledPath = join(getBundledDefaultsDir(), "settings.json");
725
+ if (!existsSync(bundledPath)) {
726
+ return error(id, "factory_reset_settings", `Bundled defaults not found at ${bundledPath}`);
727
+ }
728
+ if (existsSync(settingsPath)) {
729
+ copyFileSync(settingsPath, `${settingsPath}.bak`);
730
+ }
731
+ copyFileSync(bundledPath, settingsPath);
732
+ try {
733
+ chmodSync(settingsPath, 0o600);
734
+ }
735
+ catch {
736
+ // best-effort
737
+ }
738
+ await session.reload();
739
+ return success(id, "factory_reset_settings");
740
+ }
741
+ catch (resetError) {
742
+ return error(id, "factory_reset_settings", resetError instanceof Error ? resetError.message : String(resetError));
743
+ }
744
+ }
745
+ // =================================================================
746
+ // Auth
747
+ // =================================================================
748
+ case "get_auth_providers": {
749
+ const providers = session.modelRuntime
750
+ .getProviders()
751
+ .map((provider) => {
752
+ const authTypes = [];
753
+ if (provider.auth.oauth)
754
+ authTypes.push("oauth");
755
+ if (provider.auth.apiKey)
756
+ authTypes.push("api_key");
757
+ return { providerId: provider.id, name: provider.name, authTypes };
758
+ })
759
+ .sort((a, b) => a.name.localeCompare(b.name));
760
+ return success(id, "get_auth_providers", { providers });
761
+ }
762
+ case "login": {
763
+ const provider = session.modelRuntime.getProvider(command.providerId);
764
+ if (!provider) {
765
+ return error(id, "login", `Unknown provider: ${command.providerId}`);
766
+ }
767
+ if (command.authType === "oauth" && !provider.auth.oauth) {
768
+ return error(id, "login", `Provider ${command.providerId} does not support OAuth login`);
769
+ }
770
+ if (command.authType === "api_key" && !provider.auth.apiKey) {
771
+ return error(id, "login", `Provider ${command.providerId} does not support API key login`);
772
+ }
773
+ try {
774
+ const abortController = new AbortController();
775
+ await session.modelRuntime.login(command.providerId, command.authType, {
776
+ signal: abortController.signal,
777
+ prompt: async (prompt) => {
778
+ if (prompt.signal?.aborted)
779
+ throw new Error("Login cancelled");
780
+ const id = crypto.randomUUID();
781
+ const result = await new Promise((resolve, reject) => {
782
+ const onAbort = () => {
783
+ cleanup();
784
+ resolve({ type: "extension_ui_response", id, cancelled: true });
785
+ };
786
+ const cleanup = () => {
787
+ prompt.signal?.removeEventListener("abort", onAbort);
788
+ pendingExtensionRequests.delete(id);
789
+ };
790
+ prompt.signal?.addEventListener("abort", onAbort, { once: true });
791
+ pendingExtensionRequests.set(id, {
792
+ resolve: (response) => {
793
+ cleanup();
794
+ resolve(response);
795
+ },
796
+ reject: (err) => {
797
+ cleanup();
798
+ reject(err);
799
+ },
800
+ });
801
+ const request = {
802
+ type: "extension_ui_request",
803
+ id,
804
+ method: prompt.type === "select" ? "select" : "input",
805
+ title: prompt.message,
806
+ };
807
+ if (prompt.type === "select") {
808
+ request.options = prompt.options.map((option) => option.description ? `${option.label} (${option.description})` : option.label);
809
+ }
810
+ else {
811
+ if (prompt.placeholder)
812
+ request.placeholder = prompt.placeholder;
813
+ if (prompt.type === "secret")
814
+ request.inputKind = "secret";
815
+ }
816
+ output(request);
817
+ });
818
+ if (result.type === "extension_ui_response" && "cancelled" in result && result.cancelled) {
819
+ throw new Error("Login cancelled");
820
+ }
821
+ if ("value" in result)
822
+ return result.value;
823
+ if ("values" in result)
824
+ return (result.values ? result.values[0] : undefined);
825
+ return undefined;
826
+ },
827
+ notify: (event) => {
828
+ if (event.type === "auth_url") {
829
+ openBrowser(event.url);
830
+ output({
831
+ type: "extension_ui_request",
832
+ id: crypto.randomUUID(),
833
+ method: "notify",
834
+ message: event.instructions
835
+ ? `Open in your browser: ${event.url}\n${event.instructions}`
836
+ : `Open in your browser: ${event.url}`,
837
+ notifyType: "info",
838
+ });
839
+ }
840
+ else if (event.type === "device_code") {
841
+ openBrowser(event.verificationUri);
842
+ output({
843
+ type: "extension_ui_request",
844
+ id: crypto.randomUUID(),
845
+ method: "notify",
846
+ message: `Open ${event.verificationUri} and enter code: ${event.userCode}`,
847
+ notifyType: "info",
848
+ });
849
+ }
850
+ else if (event.type === "info") {
851
+ output({
852
+ type: "extension_ui_request",
853
+ id: crypto.randomUUID(),
854
+ method: "notify",
855
+ message: event.message,
856
+ notifyType: "info",
857
+ });
858
+ }
859
+ else if (event.type === "progress") {
860
+ output({
861
+ type: "extension_ui_request",
862
+ id: crypto.randomUUID(),
863
+ method: "notify",
864
+ message: event.message,
865
+ notifyType: "info",
866
+ });
867
+ }
868
+ },
869
+ });
870
+ const actionLabel = command.authType === "oauth" ? `Logged in to ${provider.name}` : `Saved API key for ${provider.name}`;
871
+ const authType = command.authType;
872
+ const isUnknownModel = session.model?.provider === "unknown" && session.model?.id === "unknown";
873
+ if (isUnknownModel) {
874
+ const available = await session.modelRuntime.getAvailable();
875
+ const providerModels = available.filter((model) => model.provider === command.providerId);
876
+ if (providerModels.length > 0) {
877
+ await session.setModel(providerModels[0]);
878
+ }
879
+ }
880
+ return success(id, "login", {
881
+ providerId: command.providerId,
882
+ authType,
883
+ message: `${actionLabel}. Credentials saved.`,
884
+ });
885
+ }
886
+ catch (loginError) {
887
+ const message = loginError instanceof Error ? loginError.message : String(loginError);
888
+ if (message === "Login cancelled") {
889
+ return error(id, "login", "Login cancelled");
890
+ }
891
+ return error(id, "login", message);
892
+ }
893
+ }
894
+ case "logout": {
895
+ const provider = session.modelRuntime.getProvider(command.providerId);
896
+ if (!provider) {
897
+ return error(id, "logout", `Unknown provider: ${command.providerId}`);
898
+ }
899
+ try {
900
+ await session.modelRuntime.logout(command.providerId);
901
+ return success(id, "logout", { providerId: command.providerId });
902
+ }
903
+ catch (logoutError) {
904
+ return error(id, "logout", logoutError instanceof Error ? logoutError.message : String(logoutError));
905
+ }
906
+ }
907
+ case "get_auth_state": {
908
+ try {
909
+ const credentials = await session.modelRuntime.listCredentials();
910
+ const providers = credentials.map(({ providerId, type }) => ({
911
+ providerId,
912
+ authType: type,
913
+ source: "stored",
914
+ }));
915
+ // Environment/configured auth not in the credential store.
916
+ for (const provider of session.modelRuntime.getProviders()) {
917
+ if (providers.some((entry) => entry.providerId === provider.id))
918
+ continue;
919
+ const status = session.modelRuntime.getProviderAuthStatus(provider.id);
920
+ if (status.configured) {
921
+ providers.push({
922
+ providerId: provider.id,
923
+ authType: session.modelRuntime.isUsingOAuth(provider.id) ? "oauth" : "api_key",
924
+ source: status.label ?? status.source ?? "environment",
925
+ });
926
+ }
927
+ }
928
+ return success(id, "get_auth_state", { providers });
929
+ }
930
+ catch (authStateError) {
931
+ return error(id, "get_auth_state", authStateError instanceof Error ? authStateError.message : String(authStateError));
932
+ }
933
+ }
934
+ // =================================================================
935
+ // Scoped Models
936
+ // =================================================================
937
+ case "get_scoped_models": {
938
+ const enabled = session.settingsManager.getEnabledModels() ?? [];
939
+ const scoped = session.scopedModels.map((entry) => entry.model);
940
+ const models = scoped.length > 0 ? scoped : await session.modelRuntime.getAvailable();
941
+ return success(id, "get_scoped_models", { enabled, models: [...models] });
942
+ }
943
+ case "set_scoped_models": {
944
+ if (command.enabled !== undefined) {
945
+ session.settingsManager.setEnabledModels(command.enabled.length > 0 ? command.enabled : undefined);
946
+ }
947
+ const enabled = session.settingsManager.getEnabledModels() ?? [];
948
+ if (command.reorder !== undefined && command.reorder.length > 0) {
949
+ const patternSet = new Set(command.reorder);
950
+ const kept = enabled.filter((pattern) => patternSet.has(pattern));
951
+ const added = command.reorder.filter((pattern) => !kept.includes(pattern));
952
+ session.settingsManager.setEnabledModels([...kept, ...added]);
953
+ }
954
+ await session.settingsManager.flush();
955
+ return success(id, "set_scoped_models", { enabled: session.settingsManager.getEnabledModels() ?? [] });
956
+ }
957
+ // =================================================================
958
+ // Session Import
959
+ // =================================================================
960
+ case "import_jsonl": {
961
+ try {
962
+ await runtimeHost.importFromJsonl(command.path);
963
+ await rebindSession();
964
+ const sessionFile = session.sessionFile;
965
+ return success(id, "import_jsonl", { sessionPath: sessionFile ?? command.path });
966
+ }
967
+ catch (importError) {
968
+ return error(id, "import_jsonl", importError instanceof Error ? importError.message : String(importError));
969
+ }
970
+ }
971
+ // =================================================================
972
+ // Git helper
973
+ // =================================================================
974
+ case "git": {
975
+ const prompt = buildGitCommandPrompt(command.command);
976
+ let gitPreflightSucceeded = false;
977
+ void session
978
+ .prompt(prompt, {
979
+ streamingBehavior: "steer",
980
+ source: "rpc",
981
+ preflightResult: (didSucceed) => {
982
+ if (didSucceed) {
983
+ gitPreflightSucceeded = true;
984
+ output(success(id, "git"));
985
+ }
986
+ },
987
+ })
988
+ .catch((e) => {
989
+ if (!gitPreflightSucceeded) {
990
+ output(error(id, "git", e.message));
991
+ }
992
+ });
993
+ return undefined;
994
+ }
995
+ // =================================================================
996
+ // Reload
997
+ // =================================================================
998
+ case "reload": {
999
+ if (session.isStreaming) {
1000
+ return error(id, "reload", "Wait for the current response to finish before reloading.");
1001
+ }
1002
+ if (session.isCompacting) {
1003
+ return error(id, "reload", "Wait for compaction to finish before reloading.");
1004
+ }
1005
+ try {
1006
+ await session.reload();
1007
+ return success(id, "reload");
1008
+ }
1009
+ catch (reloadError) {
1010
+ return error(id, "reload", reloadError instanceof Error ? reloadError.message : String(reloadError));
1011
+ }
1012
+ }
1013
+ // =================================================================
1014
+ // Tree ops
1015
+ // =================================================================
1016
+ case "set_entry_label": {
1017
+ try {
1018
+ const labelId = session.sessionManager.appendLabelChange(command.entryId, command.label);
1019
+ return success(id, "set_entry_label", { labelId });
1020
+ }
1021
+ catch (labelError) {
1022
+ return error(id, "set_entry_label", labelError instanceof Error ? labelError.message : String(labelError));
1023
+ }
1024
+ }
1025
+ // =================================================================
1026
+ // Trust
1027
+ // =================================================================
1028
+ case "get_trust": {
1029
+ const cwd = session.sessionManager.getCwd();
1030
+ const trustStore = new ProjectTrustStore(runtimeHost.services.agentDir);
1031
+ const entry = trustStore.getEntry(cwd);
1032
+ return success(id, "get_trust", {
1033
+ cwd,
1034
+ savedDecision: entry ? { path: entry.path, decision: entry.decision } : null,
1035
+ projectTrusted: session.settingsManager.isProjectTrusted(),
1036
+ trustRequired: hasTrustRequiringProjectResources(cwd),
1037
+ });
1038
+ }
1039
+ case "set_trust": {
1040
+ const cwd = session.sessionManager.getCwd();
1041
+ const trustStore = new ProjectTrustStore(runtimeHost.services.agentDir);
1042
+ try {
1043
+ if (command.decision === null) {
1044
+ trustStore.set(cwd, null);
1045
+ }
1046
+ else {
1047
+ trustStore.set(cwd, command.decision);
1048
+ }
1049
+ return success(id, "set_trust", { decision: command.decision, restartRequired: command.decision !== null });
1050
+ }
1051
+ catch (trustError) {
1052
+ return error(id, "set_trust", trustError instanceof Error ? trustError.message : String(trustError));
1053
+ }
1054
+ }
1055
+ // =================================================================
1056
+ // Hotkeys / version / share
1057
+ // =================================================================
1058
+ case "get_hotkeys": {
1059
+ const keybindings = KeybindingsManager.create();
1060
+ const hotkeys = keybindings.getEffectiveConfig();
1061
+ return success(id, "get_hotkeys", { hotkeys });
1062
+ }
1063
+ case "get_available_themes": {
1064
+ // Mirror TUI startup: register extension/package themes before listing.
1065
+ setRegisteredThemes(session.resourceLoader.getThemes().themes);
1066
+ const themes = getAvailableThemesWithPaths().map(({ name, path }) => ({ name, path }));
1067
+ const currentSetting = session.settingsManager.getThemeSetting();
1068
+ const current = currentSetting?.includes("/") ? undefined : currentSetting;
1069
+ return success(id, "get_available_themes", { themes, current });
1070
+ }
1071
+ case "get_version_info": {
1072
+ const changelog = parseChangelog(getChangelogPath());
1073
+ return success(id, "get_version_info", { version: VERSION, changelog });
1074
+ }
1075
+ case "share_gist": {
1076
+ try {
1077
+ const authResult = spawnSync("gh", ["auth", "status"], { encoding: "utf-8" });
1078
+ if (authResult.status !== 0) {
1079
+ return error(id, "share_gist", "GitHub CLI is not logged in. Run 'gh auth login' first.");
1080
+ }
1081
+ const tmpFile = join(tmpdir(), `selesai-session-${crypto.randomUUID()}.html`);
1082
+ await session.exportToHtml(tmpFile);
1083
+ const result = spawnSync("gh", ["gist", "create", "--public=false", tmpFile], { encoding: "utf-8" });
1084
+ try {
1085
+ unlinkSync(tmpFile);
1086
+ }
1087
+ catch {
1088
+ // best-effort cleanup
1089
+ }
1090
+ if (result.status !== 0) {
1091
+ return error(id, "share_gist", (result.stderr ?? "Unknown error").trim() || "Failed to create gist");
1092
+ }
1093
+ const gistUrl = (result.stdout ?? "").trim();
1094
+ const gistId = gistUrl.split("/").pop();
1095
+ if (!gistId) {
1096
+ return error(id, "share_gist", "Failed to parse gist ID from gh output");
1097
+ }
1098
+ const previewUrl = getShareViewerUrl(gistId);
1099
+ return success(id, "share_gist", { url: previewUrl, gistUrl });
1100
+ }
1101
+ catch (shareError) {
1102
+ return error(id, "share_gist", shareError instanceof Error ? shareError.message : String(shareError));
1103
+ }
1104
+ }
622
1105
  default: {
623
1106
  const unknownCommand = command;
624
1107
  return error(id, unknownCommand.type, `Unknown command: ${unknownCommand.type}`);