@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.
- package/dist/cli-args.d.ts +2 -0
- package/dist/cli-args.js +6 -0
- package/dist/cli.js +27 -0
- package/dist/help-text.js +15 -0
- package/dist/onboarding.d.ts +19 -0
- package/dist/onboarding.js +133 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/otto/commands/release-notes/_data.js +171 -0
- package/dist/resources/extensions/otto/commands/release-notes/command.js +114 -0
- package/dist/resources/extensions/otto/commands/theme/command.js +75 -0
- package/dist/resources/extensions/otto/extension-manifest.json +1 -1
- package/dist/resources/extensions/otto/index.js +6 -0
- package/dist/resources/extensions/subagent/agents.js +160 -8
- package/dist/resources/extensions/subagent/index.js +45 -4
- package/dist/resources/extensions/subagent/skill-tool-stub.js +23 -0
- package/dist/seed-defaults.d.ts +66 -0
- package/dist/seed-defaults.js +294 -0
- package/package.json +8 -7
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +3 -3
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +3 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +17 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +89 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.js +105 -67
- package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resolve-config-value.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resolve-config-value.js +20 -2
- package/packages/pi-coding-agent/dist/core/resolve-config-value.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +28 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +47 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.d.ts +11 -0
- package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.js +22 -0
- package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
- package/packages/pi-coding-agent/package.json +2 -2
- package/packages/pi-coding-agent/src/core/agent-session.ts +3 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +90 -1
- package/packages/pi-coding-agent/src/core/package-manager.ts +131 -64
- package/packages/pi-coding-agent/src/core/resolve-config-value.ts +20 -2
- package/packages/pi-coding-agent/src/core/settings-manager.ts +65 -0
- package/packages/pi-coding-agent/src/core/skills.ts +23 -1
- package/packages/pi-coding-agent/src/index.ts +4 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +13 -2
- package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +4 -0
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/dist/autocomplete.d.ts +9 -0
- package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
- package/packages/pi-tui/dist/autocomplete.js +2 -0
- package/packages/pi-tui/dist/autocomplete.js.map +1 -1
- package/packages/pi-tui/dist/components/select-list.d.ts +10 -0
- package/packages/pi-tui/dist/components/select-list.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/select-list.js +30 -17
- package/packages/pi-tui/dist/components/select-list.js.map +1 -1
- package/packages/pi-tui/package.json +1 -1
- package/packages/pi-tui/src/autocomplete.ts +11 -0
- package/packages/pi-tui/src/components/select-list.ts +41 -17
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/packages/rpc-client/package.json +2 -2
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/pkg/dist/modes/interactive/theme/theme.js +4 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/otto/commands/release-notes/_data.ts +187 -0
- package/src/resources/extensions/otto/commands/release-notes/command.ts +141 -0
- package/src/resources/extensions/otto/commands/theme/command.ts +89 -0
- package/src/resources/extensions/otto/extension-manifest.json +1 -1
- package/src/resources/extensions/otto/index.ts +8 -0
- package/src/resources/extensions/subagent/agents.ts +166 -8
- package/src/resources/extensions/subagent/index.ts +46 -6
- package/src/resources/extensions/subagent/skill-tool-stub.ts +28 -0
- package/src/resources/extensions/subagent/tests/parse-agent-tools.test.ts +52 -0
- 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
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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):
|
|
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
|
-
|
|
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
|
|