@cmetech/otto 1.0.8 → 1.1.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.
Files changed (94) hide show
  1. package/dist/cli-args.d.ts +2 -0
  2. package/dist/cli-args.js +6 -0
  3. package/dist/cli.js +27 -0
  4. package/dist/help-text.js +15 -0
  5. package/dist/onboarding.d.ts +19 -0
  6. package/dist/onboarding.js +133 -1
  7. package/dist/resources/.managed-resources-content-hash +1 -1
  8. package/dist/resources/extensions/otto/commands/release-notes/_data.js +171 -0
  9. package/dist/resources/extensions/otto/commands/release-notes/command.js +114 -0
  10. package/dist/resources/extensions/otto/commands/theme/command.js +75 -0
  11. package/dist/resources/extensions/otto/extension-manifest.json +1 -1
  12. package/dist/resources/extensions/otto/index.js +6 -0
  13. package/dist/resources/extensions/subagent/agents.js +160 -8
  14. package/dist/resources/extensions/subagent/index.js +45 -4
  15. package/dist/resources/extensions/subagent/skill-tool-stub.js +23 -0
  16. package/dist/seed-defaults.d.ts +66 -0
  17. package/dist/seed-defaults.js +294 -0
  18. package/package.json +8 -7
  19. package/packages/contracts/package.json +1 -1
  20. package/packages/daemon/package.json +3 -3
  21. package/packages/mcp-server/package.json +3 -3
  22. package/packages/native/package.json +1 -1
  23. package/packages/pi-agent-core/package.json +1 -1
  24. package/packages/pi-ai/package.json +1 -1
  25. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  26. package/packages/pi-coding-agent/dist/core/agent-session.js +3 -0
  27. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  28. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +17 -0
  29. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  30. package/packages/pi-coding-agent/dist/core/extensions/runner.js +89 -1
  31. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  32. package/packages/pi-coding-agent/dist/core/package-manager.d.ts +9 -0
  33. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  34. package/packages/pi-coding-agent/dist/core/package-manager.js +105 -67
  35. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  36. package/packages/pi-coding-agent/dist/core/resolve-config-value.d.ts.map +1 -1
  37. package/packages/pi-coding-agent/dist/core/resolve-config-value.js +20 -2
  38. package/packages/pi-coding-agent/dist/core/resolve-config-value.js.map +1 -1
  39. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +28 -0
  40. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  41. package/packages/pi-coding-agent/dist/core/settings-manager.js +47 -0
  42. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  43. package/packages/pi-coding-agent/dist/core/skills.d.ts +11 -0
  44. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  45. package/packages/pi-coding-agent/dist/core/skills.js +22 -0
  46. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  47. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  48. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  49. package/packages/pi-coding-agent/dist/index.js +1 -1
  50. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  51. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  52. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -2
  53. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  54. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  55. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +4 -0
  56. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
  57. package/packages/pi-coding-agent/package.json +2 -2
  58. package/packages/pi-coding-agent/src/core/agent-session.ts +3 -0
  59. package/packages/pi-coding-agent/src/core/extensions/runner.ts +90 -1
  60. package/packages/pi-coding-agent/src/core/package-manager.ts +131 -64
  61. package/packages/pi-coding-agent/src/core/resolve-config-value.ts +20 -2
  62. package/packages/pi-coding-agent/src/core/settings-manager.ts +65 -0
  63. package/packages/pi-coding-agent/src/core/skills.ts +23 -1
  64. package/packages/pi-coding-agent/src/index.ts +4 -0
  65. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +13 -2
  66. package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +4 -0
  67. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  68. package/packages/pi-tui/dist/autocomplete.d.ts +9 -0
  69. package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
  70. package/packages/pi-tui/dist/autocomplete.js +2 -0
  71. package/packages/pi-tui/dist/autocomplete.js.map +1 -1
  72. package/packages/pi-tui/dist/components/select-list.d.ts +10 -0
  73. package/packages/pi-tui/dist/components/select-list.d.ts.map +1 -1
  74. package/packages/pi-tui/dist/components/select-list.js +30 -17
  75. package/packages/pi-tui/dist/components/select-list.js.map +1 -1
  76. package/packages/pi-tui/package.json +1 -1
  77. package/packages/pi-tui/src/autocomplete.ts +11 -0
  78. package/packages/pi-tui/src/components/select-list.ts +41 -17
  79. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  80. package/packages/rpc-client/package.json +2 -2
  81. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  82. package/pkg/dist/modes/interactive/theme/theme.js +4 -0
  83. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
  84. package/pkg/package.json +1 -1
  85. package/src/resources/extensions/otto/commands/release-notes/_data.ts +187 -0
  86. package/src/resources/extensions/otto/commands/release-notes/command.ts +141 -0
  87. package/src/resources/extensions/otto/commands/theme/command.ts +89 -0
  88. package/src/resources/extensions/otto/extension-manifest.json +1 -1
  89. package/src/resources/extensions/otto/index.ts +8 -0
  90. package/src/resources/extensions/subagent/agents.ts +166 -8
  91. package/src/resources/extensions/subagent/index.ts +46 -6
  92. package/src/resources/extensions/subagent/skill-tool-stub.ts +28 -0
  93. package/src/resources/extensions/subagent/tests/parse-agent-tools.test.ts +52 -0
  94. package/src/resources/extensions/subagent/tests/skill-tool-stub.test.ts +23 -0
@@ -807,12 +807,13 @@ export class DefaultPackageManager implements PackageManager {
807
807
  const parsed = this.parseSource(source);
808
808
  const scope: SourceScope = options?.local ? "project" : "user";
809
809
  await this.withProgress("install", source, `Installing ${source}...`, async () => {
810
+ // Explicit user-initiated install — stream npm/git progress to the terminal.
810
811
  if (parsed.type === "npm") {
811
- await this.installNpm(parsed, scope, false);
812
+ await this.installNpm(parsed, scope, false, true);
812
813
  return;
813
814
  }
814
815
  if (parsed.type === "git") {
815
- await this.installGit(parsed, scope);
816
+ await this.installGit(parsed, scope, true);
816
817
  return;
817
818
  }
818
819
  if (parsed.type === "local") {
@@ -831,7 +832,7 @@ export class DefaultPackageManager implements PackageManager {
831
832
  const scope: SourceScope = options?.local ? "project" : "user";
832
833
  await this.withProgress("remove", source, `Removing ${source}...`, async () => {
833
834
  if (parsed.type === "npm") {
834
- await this.uninstallNpm(parsed, scope);
835
+ await this.uninstallNpm(parsed, scope, true);
835
836
  return;
836
837
  }
837
838
  if (parsed.type === "git") {
@@ -869,15 +870,16 @@ export class DefaultPackageManager implements PackageManager {
869
870
  const parsed = this.parseSource(source);
870
871
  if (parsed.type === "npm") {
871
872
  if (parsed.pinned) return;
873
+ // Explicit `pi package update` — stream npm progress to terminal.
872
874
  await this.withProgress("update", source, `Updating ${source}...`, async () => {
873
- await this.installNpm(parsed, scope, false);
875
+ await this.installNpm(parsed, scope, false, true);
874
876
  });
875
877
  return;
876
878
  }
877
879
  if (parsed.type === "git") {
878
880
  if (parsed.pinned) return;
879
881
  await this.withProgress("update", source, `Updating ${source}...`, async () => {
880
- await this.updateGit(parsed, scope);
882
+ await this.updateGit(parsed, scope, true);
881
883
  });
882
884
  return;
883
885
  }
@@ -890,54 +892,75 @@ export class DefaultPackageManager implements PackageManager {
890
892
  ): Promise<void> {
891
893
  for (const { pkg, scope } of sources) {
892
894
  const sourceStr = typeof pkg === "string" ? pkg : pkg.source;
893
- const filter = typeof pkg === "object" ? pkg : undefined;
894
- const parsed = this.parseSource(sourceStr);
895
- const metadata: PathMetadata = { source: sourceStr, scope, origin: "package" };
896
-
897
- if (parsed.type === "local") {
898
- const baseDir = this.getBaseDirForScope(scope);
899
- this.resolveLocalExtensionSource(parsed, accumulator, filter, metadata, baseDir);
900
- continue;
895
+ try {
896
+ await this.resolveOnePackageSource(pkg, scope, accumulator, onMissing);
897
+ } catch (error) {
898
+ // One bad source (typo, 404, network glitch, unreachable git host)
899
+ // must not abort startup or block resolution of the remaining
900
+ // packages. Warn loudly so the user knows, then continue.
901
+ const message = error instanceof Error ? error.message : String(error);
902
+ process.stderr.write(
903
+ `[pi] Skipping package "${sourceStr}" — ${message}. ` +
904
+ `Run \`pi remove ${sourceStr}\` if you want to drop it permanently.\n`,
905
+ );
901
906
  }
907
+ }
908
+ }
902
909
 
903
- const installMissing = async (): Promise<boolean> => {
904
- if (isOfflineModeEnabled()) {
905
- return false;
906
- }
907
- if (!onMissing) {
908
- await this.installParsedSource(parsed, scope);
909
- return true;
910
- }
911
- const action = await onMissing(sourceStr);
912
- if (action === "skip") return false;
913
- if (action === "error") throw new Error(`Missing source: ${sourceStr}`);
910
+ private async resolveOnePackageSource(
911
+ pkg: PackageSource,
912
+ scope: SourceScope,
913
+ accumulator: ResourceAccumulator,
914
+ onMissing?: (source: string) => Promise<MissingSourceAction>,
915
+ ): Promise<void> {
916
+ const sourceStr = typeof pkg === "string" ? pkg : pkg.source;
917
+ const filter = typeof pkg === "object" ? pkg : undefined;
918
+ const parsed = this.parseSource(sourceStr);
919
+ const metadata: PathMetadata = { source: sourceStr, scope, origin: "package" };
920
+
921
+ if (parsed.type === "local") {
922
+ const baseDir = this.getBaseDirForScope(scope);
923
+ this.resolveLocalExtensionSource(parsed, accumulator, filter, metadata, baseDir);
924
+ return;
925
+ }
926
+
927
+ const installMissing = async (): Promise<boolean> => {
928
+ if (isOfflineModeEnabled()) {
929
+ return false;
930
+ }
931
+ if (!onMissing) {
914
932
  await this.installParsedSource(parsed, scope);
915
933
  return true;
916
- };
934
+ }
935
+ const action = await onMissing(sourceStr);
936
+ if (action === "skip") return false;
937
+ if (action === "error") throw new Error(`Missing source: ${sourceStr}`);
938
+ await this.installParsedSource(parsed, scope);
939
+ return true;
940
+ };
917
941
 
918
- if (parsed.type === "npm") {
919
- const installedPath = this.getNpmInstallPath(parsed, scope);
920
- const needsInstall = !existsSync(installedPath) || (await this.npmNeedsUpdate(parsed, installedPath));
921
- if (needsInstall) {
922
- const installed = await installMissing();
923
- if (!installed) continue;
924
- }
925
- metadata.baseDir = installedPath;
926
- this.collectPackageResources(installedPath, accumulator, filter, metadata);
927
- continue;
942
+ if (parsed.type === "npm") {
943
+ const installedPath = this.getNpmInstallPath(parsed, scope);
944
+ const needsInstall = !existsSync(installedPath) || (await this.npmNeedsUpdate(parsed, installedPath));
945
+ if (needsInstall) {
946
+ const installed = await installMissing();
947
+ if (!installed) return;
928
948
  }
949
+ metadata.baseDir = installedPath;
950
+ this.collectPackageResources(installedPath, accumulator, filter, metadata);
951
+ return;
952
+ }
929
953
 
930
- if (parsed.type === "git") {
931
- const installedPath = this.getGitInstallPath(parsed, scope);
932
- if (!existsSync(installedPath)) {
933
- const installed = await installMissing();
934
- if (!installed) continue;
935
- } else if (scope === "temporary" && !parsed.pinned && !isOfflineModeEnabled()) {
936
- await this.refreshTemporaryGitSource(parsed, sourceStr);
937
- }
938
- metadata.baseDir = installedPath;
939
- this.collectPackageResources(installedPath, accumulator, filter, metadata);
954
+ if (parsed.type === "git") {
955
+ const installedPath = this.getGitInstallPath(parsed, scope);
956
+ if (!existsSync(installedPath)) {
957
+ const installed = await installMissing();
958
+ if (!installed) return;
959
+ } else if (scope === "temporary" && !parsed.pinned && !isOfflineModeEnabled()) {
960
+ await this.refreshTemporaryGitSource(parsed, sourceStr);
940
961
  }
962
+ metadata.baseDir = installedPath;
963
+ this.collectPackageResources(installedPath, accumulator, filter, metadata);
941
964
  }
942
965
  }
943
966
 
@@ -1169,21 +1192,34 @@ export class DefaultPackageManager implements PackageManager {
1169
1192
  return { name, version };
1170
1193
  }
1171
1194
 
1172
- private async installNpm(source: NpmSource, scope: SourceScope, temporary: boolean): Promise<void> {
1195
+ private async installNpm(
1196
+ source: NpmSource,
1197
+ scope: SourceScope,
1198
+ temporary: boolean,
1199
+ interactive: boolean = false,
1200
+ ): Promise<void> {
1173
1201
  const installRoot = this.getNpmInstallRoot(scope, temporary);
1174
1202
  this.ensureNpmProject(installRoot);
1175
- await this.runCommand("npm", ["install", source.spec, "--prefix", installRoot]);
1203
+ await this.runCommand("npm", ["install", source.spec, "--prefix", installRoot], { interactive });
1176
1204
  }
1177
1205
 
1178
- private async uninstallNpm(source: NpmSource, scope: SourceScope): Promise<void> {
1206
+ private async uninstallNpm(
1207
+ source: NpmSource,
1208
+ scope: SourceScope,
1209
+ interactive: boolean = false,
1210
+ ): Promise<void> {
1179
1211
  const installRoot = this.getNpmInstallRoot(scope, false);
1180
1212
  if (!existsSync(installRoot)) {
1181
1213
  return;
1182
1214
  }
1183
- await this.runCommand("npm", ["uninstall", source.name, "--prefix", installRoot]);
1215
+ await this.runCommand("npm", ["uninstall", source.name, "--prefix", installRoot], { interactive });
1184
1216
  }
1185
1217
 
1186
- private async installGit(source: GitSource, scope: SourceScope): Promise<void> {
1218
+ private async installGit(
1219
+ source: GitSource,
1220
+ scope: SourceScope,
1221
+ interactive: boolean = false,
1222
+ ): Promise<void> {
1187
1223
  const targetDir = this.getGitInstallPath(source, scope);
1188
1224
  if (existsSync(targetDir)) {
1189
1225
  return;
@@ -1194,40 +1230,44 @@ export class DefaultPackageManager implements PackageManager {
1194
1230
  }
1195
1231
  mkdirSync(dirname(targetDir), { recursive: true });
1196
1232
 
1197
- await this.runCommand("git", ["clone", source.repo, targetDir]);
1233
+ await this.runCommand("git", ["clone", source.repo, targetDir], { interactive });
1198
1234
  if (source.ref) {
1199
- await this.runCommand("git", ["checkout", source.ref], { cwd: targetDir });
1235
+ await this.runCommand("git", ["checkout", source.ref], { cwd: targetDir, interactive });
1200
1236
  }
1201
1237
  const packageJsonPath = join(targetDir, "package.json");
1202
1238
  if (existsSync(packageJsonPath)) {
1203
- await this.runCommand("npm", ["install"], { cwd: targetDir });
1239
+ await this.runCommand("npm", ["install"], { cwd: targetDir, interactive });
1204
1240
  }
1205
1241
  }
1206
1242
 
1207
- private async updateGit(source: GitSource, scope: SourceScope): Promise<void> {
1243
+ private async updateGit(
1244
+ source: GitSource,
1245
+ scope: SourceScope,
1246
+ interactive: boolean = false,
1247
+ ): Promise<void> {
1208
1248
  const targetDir = this.getGitInstallPath(source, scope);
1209
1249
  if (!existsSync(targetDir)) {
1210
- await this.installGit(source, scope);
1250
+ await this.installGit(source, scope, interactive);
1211
1251
  return;
1212
1252
  }
1213
1253
 
1214
1254
  // Fetch latest from remote (handles force-push by getting new history)
1215
- await this.runCommand("git", ["fetch", "--prune", "origin"], { cwd: targetDir });
1255
+ await this.runCommand("git", ["fetch", "--prune", "origin"], { cwd: targetDir, interactive });
1216
1256
 
1217
1257
  // Reset to tracking branch. Fall back to origin/HEAD when no upstream is configured.
1218
1258
  try {
1219
- await this.runCommand("git", ["reset", "--hard", "@{upstream}"], { cwd: targetDir });
1259
+ await this.runCommand("git", ["reset", "--hard", "@{upstream}"], { cwd: targetDir, interactive });
1220
1260
  } catch {
1221
- await this.runCommand("git", ["remote", "set-head", "origin", "-a"], { cwd: targetDir }).catch(() => {});
1222
- await this.runCommand("git", ["reset", "--hard", "origin/HEAD"], { cwd: targetDir });
1261
+ await this.runCommand("git", ["remote", "set-head", "origin", "-a"], { cwd: targetDir, interactive }).catch(() => {});
1262
+ await this.runCommand("git", ["reset", "--hard", "origin/HEAD"], { cwd: targetDir, interactive });
1223
1263
  }
1224
1264
 
1225
1265
  // Clean untracked files (extensions should be pristine)
1226
- await this.runCommand("git", ["clean", "-fdx"], { cwd: targetDir });
1266
+ await this.runCommand("git", ["clean", "-fdx"], { cwd: targetDir, interactive });
1227
1267
 
1228
1268
  const packageJsonPath = join(targetDir, "package.json");
1229
1269
  if (existsSync(packageJsonPath)) {
1230
- await this.runCommand("npm", ["install"], { cwd: targetDir });
1270
+ await this.runCommand("npm", ["install"], { cwd: targetDir, interactive });
1231
1271
  }
1232
1272
  }
1233
1273
 
@@ -1819,19 +1859,46 @@ export class DefaultPackageManager implements PackageManager {
1819
1859
  };
1820
1860
  }
1821
1861
 
1822
- private runCommand(command: string, args: string[], options?: { cwd?: string }): Promise<void> {
1862
+ /**
1863
+ * Run a child process. When `interactive` is true (the explicit user-facing
1864
+ * install/update path), stdio is inherited so npm/git progress shows in the
1865
+ * terminal. When false (the default — auto-resolve during launch), stdout
1866
+ * and stderr are captured. The captured stderr is appended to the failure
1867
+ * error so callers can surface the underlying cause without polluting the
1868
+ * TUI during startup.
1869
+ */
1870
+ private runCommand(
1871
+ command: string,
1872
+ args: string[],
1873
+ options?: { cwd?: string; interactive?: boolean },
1874
+ ): Promise<void> {
1875
+ const interactive = options?.interactive === true;
1823
1876
  return new Promise((resolvePromise, reject) => {
1824
1877
  const child = spawn(command, args, {
1825
1878
  cwd: options?.cwd,
1826
- stdio: "inherit",
1879
+ stdio: interactive ? "inherit" : ["ignore", "pipe", "pipe"],
1827
1880
  shell: process.platform === "win32",
1828
1881
  });
1882
+
1883
+ let stderr = "";
1884
+ if (!interactive) {
1885
+ child.stdout?.on("data", () => { /* drop — progress goes through withProgress */ });
1886
+ child.stderr?.on("data", (chunk: Buffer | string) => {
1887
+ stderr += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
1888
+ // Cap to avoid unbounded growth on chatty failures.
1889
+ if (stderr.length > 4_000) stderr = stderr.slice(-4_000);
1890
+ });
1891
+ }
1892
+
1829
1893
  child.on("error", reject);
1830
1894
  child.on("exit", (code) => {
1831
1895
  if (code === 0) {
1832
1896
  resolvePromise();
1833
1897
  } else {
1834
- reject(new Error(`${command} ${args.join(" ")} failed with code ${code}`));
1898
+ const detail = !interactive && stderr.trim().length > 0
1899
+ ? `: ${stderr.trim().split("\n").slice(-3).join(" ").slice(0, 500)}`
1900
+ : "";
1901
+ reject(new Error(`${command} ${args.join(" ")} failed with code ${code}${detail}`));
1835
1902
  }
1836
1903
  });
1837
1904
  });
@@ -24,6 +24,20 @@ export const SAFE_COMMAND_PREFIXES = [
24
24
  "lpass",
25
25
  ];
26
26
 
27
+ /**
28
+ * Whether to emit the per-blocked-command stderr warning. Off by default —
29
+ * extensions (e.g. piolium's anthropic-vertex registration) intentionally
30
+ * register !sh-style apiKey expressions that will never resolve on most user
31
+ * machines; the warning is noise. Opt back in by setting OTTO_LOG_BLOCKED_COMMANDS=1
32
+ * (or any truthy value) when actively debugging credential resolution.
33
+ */
34
+ function shouldLogBlockedCommands(): boolean {
35
+ const v = process.env.OTTO_LOG_BLOCKED_COMMANDS;
36
+ if (!v) return false;
37
+ const lv = v.toLowerCase();
38
+ return lv === "1" || lv === "true" || lv === "yes";
39
+ }
40
+
27
41
  /**
28
42
  * Active command prefix allowlist. Defaults to SAFE_COMMAND_PREFIXES but can be
29
43
  * overridden via setAllowedCommandPrefixes() (called from settings or env var).
@@ -70,13 +84,17 @@ function executeCommand(commandConfig: string): string | undefined {
70
84
  const tokens = command.split(/\s+/).filter(Boolean);
71
85
  const firstToken = tokens[0];
72
86
  if (!activeCommandPrefixes.includes(firstToken)) {
73
- process.stderr.write(`[resolve-config-value] Blocked disallowed command: "${firstToken}". Allowed: ${activeCommandPrefixes.join(", ")}\n`);
87
+ if (shouldLogBlockedCommands()) {
88
+ process.stderr.write(`[resolve-config-value] Blocked disallowed command: "${firstToken}". Allowed: ${activeCommandPrefixes.join(", ")}\n`);
89
+ }
74
90
  commandResultCache.set(commandConfig, undefined);
75
91
  return undefined;
76
92
  }
77
93
 
78
94
  if (SHELL_OPERATORS.test(command)) {
79
- process.stderr.write(`[resolve-config-value] Blocked shell operators in command: "${command}"\n`);
95
+ if (shouldLogBlockedCommands()) {
96
+ process.stderr.write(`[resolve-config-value] Blocked shell operators in command: "${command}"\n`);
97
+ }
80
98
  commandResultCache.set(commandConfig, undefined);
81
99
  return undefined;
82
100
  }
@@ -220,6 +220,12 @@ export interface Settings {
220
220
  allowedCommandPrefixes?: string[]; // Override built-in SAFE_COMMAND_PREFIXES for !command resolution (global-only — ignored in project settings)
221
221
  fetchAllowedUrls?: string[]; // Hostnames exempted from SSRF blocklist in fetch_page (global-only — ignored in project settings)
222
222
  hooks?: HooksSettings; // Layer 0 shell-command hooks. Project-scoped hooks require explicit trust (.pi/hooks.trusted).
223
+ seedDefaultsOnLaunch?: boolean; // When true, ship-with-OTTO default packages are added to `packages` on launch
224
+ seededDefaults?: string[]; // Sources already attempted to seed — prevents re-adding after the user removes one
225
+ enabledDefaultPackages?: string[]; // Subset of default sources the user opted into during onboarding. undefined = "all" (back-compat)
226
+ quietExtensions?: string[]; // Substring patterns matched against extension.path — ui.notify calls from matching extensions are silently dropped
227
+ seededQuietPatterns?: string[]; // Quiet patterns already attempted to seed — zombie-resurrection guard, mirrors seededDefaults
228
+ seededSkillPaths?: string[]; // Skill paths already attempted to seed (e.g. ~/.claude/skills) — zombie guard for harness skill auto-seeding
223
229
  }
224
230
 
225
231
  /** Settings keys that are only respected from global config — project settings cannot override these. */
@@ -938,6 +944,65 @@ export class SettingsManager {
938
944
  this.setProjectSetting("packages", packages);
939
945
  }
940
946
 
947
+ getSeedDefaultsOnLaunch(): boolean | undefined {
948
+ return this.settings.seedDefaultsOnLaunch;
949
+ }
950
+
951
+ setSeedDefaultsOnLaunch(enabled: boolean): void {
952
+ this.setGlobalSetting("seedDefaultsOnLaunch", enabled);
953
+ }
954
+
955
+ getSeededDefaults(): string[] {
956
+ return [...(this.settings.seededDefaults ?? [])];
957
+ }
958
+
959
+ setSeededDefaults(sources: string[]): void {
960
+ this.setGlobalSetting("seededDefaults", sources);
961
+ }
962
+
963
+ /**
964
+ * Returns undefined when the user has expressed no preference (back-compat:
965
+ * the seeder treats this as "install every default"). Returns a (possibly
966
+ * empty) array when the user has explicitly chosen a subset.
967
+ */
968
+ getEnabledDefaultPackages(): string[] | undefined {
969
+ const value = this.settings.enabledDefaultPackages;
970
+ return value === undefined ? undefined : [...value];
971
+ }
972
+
973
+ setEnabledDefaultPackages(sources: string[]): void {
974
+ this.setGlobalSetting("enabledDefaultPackages", sources);
975
+ }
976
+
977
+ /**
978
+ * Returns substring patterns matched (case-insensitive) against an extension's
979
+ * filesystem path. ui.notify calls from matching extensions are silently
980
+ * dropped at runtime — useful for muting noisy session_start banners.
981
+ */
982
+ getQuietExtensions(): string[] {
983
+ return [...(this.settings.quietExtensions ?? [])];
984
+ }
985
+
986
+ setQuietExtensions(patterns: string[]): void {
987
+ this.setGlobalSetting("quietExtensions", patterns);
988
+ }
989
+
990
+ getSeededQuietPatterns(): string[] {
991
+ return [...(this.settings.seededQuietPatterns ?? [])];
992
+ }
993
+
994
+ setSeededQuietPatterns(patterns: string[]): void {
995
+ this.setGlobalSetting("seededQuietPatterns", patterns);
996
+ }
997
+
998
+ getSeededSkillPaths(): string[] {
999
+ return [...(this.settings.seededSkillPaths ?? [])];
1000
+ }
1001
+
1002
+ setSeededSkillPaths(paths: string[]): void {
1003
+ this.setGlobalSetting("seededSkillPaths", paths);
1004
+ }
1005
+
941
1006
  getExtensionPaths(): string[] {
942
1007
  return [...(this.settings.extensions ?? [])];
943
1008
  }
@@ -25,6 +25,22 @@ export const ECOSYSTEM_PROJECT_SKILLS_DIR = ".agents";
25
25
  */
26
26
  const LEGACY_SKILLS_DIR = join(homedir(), CONFIG_DIR_NAME, "agent", "skills");
27
27
 
28
+ /**
29
+ * Conventional user-scope skill folders of other AI coding harnesses. Used by
30
+ * `getSource` to label skills with `harness:<id>` so the TUI can render an
31
+ * origin tag in the slash-command autocomplete. Adding an entry here makes a
32
+ * harness's skills self-identify; it does NOT control whether the directory is
33
+ * scanned (that's still driven by the user's `settings.skills` array — auto-
34
+ * seeded by `seed-defaults.ts` when the directory exists).
35
+ *
36
+ * Fork-specific edit. See docs/UPSTREAM-SYNC.md.
37
+ */
38
+ export const HARNESS_SOURCE_PATHS: Record<string, string> = {
39
+ claude: join(homedir(), ".claude", "skills"),
40
+ codex: join(homedir(), ".codex", "skills"),
41
+ kiro: join(homedir(), ".kiro", "skills"),
42
+ };
43
+
28
44
  /** Max name length per spec */
29
45
  const MAX_NAME_LENGTH = 64;
30
46
 
@@ -445,11 +461,17 @@ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
445
461
  return target.startsWith(prefix);
446
462
  };
447
463
 
448
- const getSource = (resolvedPath: string): "user" | "project" | "path" => {
464
+ const getSource = (resolvedPath: string): string => {
449
465
  if (!includeDefaults) {
450
466
  if (isUnderPath(resolvedPath, userSkillsDir)) return "user";
451
467
  if (isUnderPath(resolvedPath, projectSkillsDir)) return "project";
452
468
  }
469
+ // Tag skills that live under a known external-harness folder so the
470
+ // TUI can render their origin. Checked even when includeDefaults=true
471
+ // because harness paths are an opt-in third category, not a default.
472
+ for (const [harnessId, harnessPath] of Object.entries(HARNESS_SOURCE_PATHS)) {
473
+ if (isUnderPath(resolvedPath, harnessPath)) return `harness:${harnessId}`;
474
+ }
453
475
  return "path";
454
476
  };
455
477
 
@@ -404,14 +404,18 @@ export {
404
404
  } from "./modes/interactive/components/index.js";
405
405
  // Theme utilities for custom tools and extensions
406
406
  export {
407
+ getAvailableThemes,
408
+ getAvailableThemesWithPaths,
407
409
  getLanguageFromPath,
408
410
  getMarkdownTheme,
409
411
  getSelectListTheme,
410
412
  getSettingsListTheme,
411
413
  highlightCode,
412
414
  initTheme,
415
+ setTheme,
413
416
  Theme,
414
417
  type ThemeColor,
418
+ type ThemeInfo,
415
419
  } from "./modes/interactive/theme/theme.js";
416
420
  // Clipboard utilities
417
421
  export { copyToClipboard } from "./utils/clipboard.js";
@@ -565,14 +565,25 @@ export class InteractiveMode {
565
565
  getArgumentCompletions: cmd.getArgumentCompletions,
566
566
  }));
567
567
 
568
- // Build skill commands from session.skills (if enabled)
568
+ // Build skill commands from session.skills (if enabled).
569
+ // When a skill's source starts with "harness:" (set by skills.ts:getSource
570
+ // for skills loaded from ~/.claude/skills, ~/.codex/skills, ~/.kiro/skills
571
+ // and similar), surface the origin in the autocomplete dropdown — as a
572
+ // suffix on the description AND as a `tag` for the colored chip
573
+ // rendered by SelectList (see pi-tui/src/components/select-list.ts).
569
574
  this.skillCommands.clear();
570
575
  const skillCommandList: SlashCommand[] = [];
571
576
  if (this.settingsManager.getEnableSkillCommands()) {
572
577
  for (const skill of this.session.resourceLoader.getSkills().skills) {
573
578
  const commandName = `skill:${skill.name}`;
574
579
  this.skillCommands.set(commandName, skill.filePath);
575
- skillCommandList.push({ name: commandName, description: skill.description });
580
+ const harnessId = skill.source.startsWith("harness:")
581
+ ? skill.source.slice("harness:".length)
582
+ : undefined;
583
+ const description = harnessId
584
+ ? `${skill.description} (${harnessId})`
585
+ : skill.description;
586
+ skillCommandList.push({ name: commandName, description, tag: harnessId });
576
587
  }
577
588
  }
578
589
 
@@ -1106,6 +1106,10 @@ export function getSelectListTheme(): SelectListTheme {
1106
1106
  description: (text: string) => theme.fg("muted", text),
1107
1107
  scrollInfo: (text: string) => theme.fg("muted", text),
1108
1108
  noMatch: (text: string) => theme.fg("muted", text),
1109
+ // Origin chip color (e.g. "[claude] " before a skill name). Uses the
1110
+ // accent palette so the chip is glanceable but doesn't compete with
1111
+ // selectedText (the chip is rendered uncolored when its row is selected).
1112
+ tag: (text: string) => theme.fg("accent", text),
1109
1113
  };
1110
1114
  }
1111
1115