@f5xc-salesdemos/xcsh 17.5.2 → 18.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/CHANGELOG.md +16 -2
- package/package.json +13 -12
- package/scripts/build-info/resolvers.ts +51 -0
- package/scripts/generate-build-info.ts +106 -0
- package/src/config/settings-schema.ts +8 -8
- package/src/internal-urls/build-info-runtime.ts +182 -0
- package/src/internal-urls/build-info.generated.ts +33 -0
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/internal-urls/index.ts +1 -1
- package/src/internal-urls/router.ts +2 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/internal-urls/xcsh-protocol.ts +113 -0
- package/src/modes/components/custom-message.ts +1 -1
- package/src/modes/components/gutter-block.ts +10 -3
- package/src/modes/components/hook-message.ts +1 -1
- package/src/modes/components/skill-message.ts +1 -1
- package/src/modes/components/todo-reminder.ts +1 -1
- package/src/modes/components/tool-execution.ts +4 -4
- package/src/modes/components/ttsr-notification.ts +1 -1
- package/src/modes/components/user-message.ts +16 -8
- package/src/modes/theme/dark.json +1 -1
- package/src/modes/theme/defaults/xcsh-dark.json +1 -1
- package/src/prompts/system/system-prompt.md +2 -1
- package/src/sdk.ts +3 -3
- package/src/tools/todo-write.ts +4 -3
- package/src/internal-urls/pi-protocol.ts +0 -84
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Flipped eight user-facing factory defaults for new users:
|
|
8
|
+
- `defaultThinkingLevel`: `"high"` → `"xhigh"`
|
|
9
|
+
- `hideThinkingBlock`: `false` → `true`
|
|
10
|
+
- `interruptMode`: `"immediate"` → `"wait"`
|
|
11
|
+
- `todo.eager`: `false` → `true`
|
|
12
|
+
- `grep.contextBefore`: `0` → `3`
|
|
13
|
+
- `grep.contextAfter`: `0` → `3`
|
|
14
|
+
- `task.eager`: `false` → `true`
|
|
15
|
+
- `secrets.enabled`: `false` → `true`
|
|
16
|
+
- Existing users keep any value they have already saved in `config.yml`; no force-migration runs.
|
|
17
|
+
|
|
5
18
|
## [15.0.0] - 2026-04-10
|
|
6
19
|
|
|
7
20
|
### Fixed
|
|
@@ -9,6 +22,7 @@
|
|
|
9
22
|
- Fixed cached Ollama discovery rows so upgraded installs switch to the OpenAI Responses transport instead of staying on the old completions transport
|
|
10
23
|
|
|
11
24
|
## [14.0.2] - 2026-04-09
|
|
25
|
+
|
|
12
26
|
### Added
|
|
13
27
|
|
|
14
28
|
- Added `/force` slash command to force the next agent turn to use a specific tool
|
|
@@ -1562,7 +1576,7 @@
|
|
|
1562
1576
|
### Fixed
|
|
1563
1577
|
|
|
1564
1578
|
- Restored inline rendering for `read` tool image results in assistant transcript components, including streaming and rebuilt session history paths.
|
|
1565
|
-
- Fixed shell-escaped read paths (for example, pasted
|
|
1579
|
+
- Fixed shell-escaped read paths (for example, pasted backslash-escaped screenshot filenames) by resolving unescaped fallback candidates before macOS filename normalization variants.
|
|
1566
1580
|
|
|
1567
1581
|
## [13.3.8] - 2026-02-28
|
|
1568
1582
|
|
|
@@ -4458,7 +4472,7 @@
|
|
|
4458
4472
|
- Enhanced Python kernel availability checking with faster validation
|
|
4459
4473
|
- Optimized Python environment warming to avoid blocking during tool initialization
|
|
4460
4474
|
- Reorganized settings interface into behavior, tools, display, voice, status, lsp, and exa tabs
|
|
4461
|
-
- Migrated environment variables from
|
|
4475
|
+
- Migrated environment variables from `PI_*` to `OMP_*` prefix with automatic migration
|
|
4462
4476
|
- Updated model selector to use TabBar component for provider navigation
|
|
4463
4477
|
- Changed role badges to inverted style with colored backgrounds
|
|
4464
4478
|
- Added support for /models command alias in addition to /model
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "18.1.0",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5xc-salesdemos/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -31,27 +31,28 @@
|
|
|
31
31
|
"xcsh": "src/cli.ts"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
|
-
"build": "bun --cwd=../stats scripts/generate-client-bundle.ts --generate && bun --cwd=../natives run embed:native && bun build --compile --define PI_COMPILED=true --external mupdf --root ../.. ./src/cli.ts --outfile dist/xcsh && bun --cwd=../natives run embed:native --reset && bun --cwd=../stats scripts/generate-client-bundle.ts --reset",
|
|
34
|
+
"build": "bun run generate-build-info && bun --cwd=../stats scripts/generate-client-bundle.ts --generate && bun --cwd=../natives run embed:native && bun build --compile --define PI_COMPILED=true --external mupdf --root ../.. ./src/cli.ts --outfile dist/xcsh && bun --cwd=../natives run embed:native --reset && bun --cwd=../stats scripts/generate-client-bundle.ts --reset",
|
|
35
35
|
"check": "biome check . && bun run check:types",
|
|
36
|
-
"check:types": "tsgo -p tsconfig.json --noEmit",
|
|
36
|
+
"check:types": "bun run generate-build-info && tsgo -p tsconfig.json --noEmit",
|
|
37
37
|
"lint": "biome lint .",
|
|
38
|
-
"test": "bun test --max-concurrency 4",
|
|
39
|
-
"fix": "biome check --write --unsafe . && bun run format-prompts && bun run generate-docs-index",
|
|
38
|
+
"test": "bun run generate-build-info && bun test --max-concurrency 4",
|
|
39
|
+
"fix": "biome check --write --unsafe . && bun run format-prompts && bun run generate-docs-index && bun run generate-build-info",
|
|
40
40
|
"fmt": "biome format --write . && bun run format-prompts",
|
|
41
41
|
"format-prompts": "bun scripts/format-prompts.ts",
|
|
42
42
|
"generate-docs-index": "bun scripts/generate-docs-index.ts",
|
|
43
|
-
"
|
|
43
|
+
"generate-build-info": "bun scripts/generate-build-info.ts",
|
|
44
|
+
"prepack": "bun scripts/generate-docs-index.ts && bun scripts/generate-build-info.ts",
|
|
44
45
|
"generate-template": "bun scripts/generate-template.ts"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
48
49
|
"@mozilla/readability": "^0.6",
|
|
49
|
-
"@f5xc-salesdemos/xcsh-stats": "
|
|
50
|
-
"@f5xc-salesdemos/pi-agent-core": "
|
|
51
|
-
"@f5xc-salesdemos/pi-ai": "
|
|
52
|
-
"@f5xc-salesdemos/pi-natives": "
|
|
53
|
-
"@f5xc-salesdemos/pi-tui": "
|
|
54
|
-
"@f5xc-salesdemos/pi-utils": "
|
|
50
|
+
"@f5xc-salesdemos/xcsh-stats": "18.1.0",
|
|
51
|
+
"@f5xc-salesdemos/pi-agent-core": "18.1.0",
|
|
52
|
+
"@f5xc-salesdemos/pi-ai": "18.1.0",
|
|
53
|
+
"@f5xc-salesdemos/pi-natives": "18.1.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-tui": "18.1.0",
|
|
55
|
+
"@f5xc-salesdemos/pi-utils": "18.1.0",
|
|
55
56
|
"@sinclair/typebox": "^0.34",
|
|
56
57
|
"@xterm/headless": "^6.0",
|
|
57
58
|
"ajv": "^8.18",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface EnvLike {
|
|
2
|
+
readonly XCSH_BUILD_COMMIT?: string;
|
|
3
|
+
readonly XCSH_BUILD_BRANCH?: string;
|
|
4
|
+
readonly XCSH_BUILD_TAG?: string;
|
|
5
|
+
readonly XCSH_BUILD_PR?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type GitFn = (args: string[]) => Promise<string>;
|
|
9
|
+
export type GhFn = (sha: string) => Promise<string>;
|
|
10
|
+
|
|
11
|
+
function pick(value: string | undefined): string {
|
|
12
|
+
return value?.trim() ?? "";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function resolveCommit(env: EnvLike, git: GitFn): Promise<string> {
|
|
16
|
+
return pick(env.XCSH_BUILD_COMMIT) || (await git(["rev-parse", "HEAD"]));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function resolveBranch(env: EnvLike, git: GitFn): Promise<string> {
|
|
20
|
+
const override = pick(env.XCSH_BUILD_BRANCH);
|
|
21
|
+
if (override) return override;
|
|
22
|
+
|
|
23
|
+
const local = await git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
24
|
+
if (local && local !== "HEAD") return local;
|
|
25
|
+
|
|
26
|
+
const remote = await git(["branch", "-r", "--contains", "HEAD"]);
|
|
27
|
+
for (const raw of remote.split("\n")) {
|
|
28
|
+
const line = raw.trim();
|
|
29
|
+
if (!line || line.includes("->") || line === "HEAD") continue;
|
|
30
|
+
const stripped = line.replace(/^origin\//, "");
|
|
31
|
+
if (stripped && stripped !== "HEAD") return stripped;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return "unknown";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function resolveTag(env: EnvLike, git: GitFn): Promise<string> {
|
|
38
|
+
return pick(env.XCSH_BUILD_TAG) || (await git(["describe", "--exact-match", "--tags", "HEAD"]));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function resolveDirty(_env: EnvLike, git: GitFn): Promise<boolean> {
|
|
42
|
+
const status = await git(["status", "--porcelain"]);
|
|
43
|
+
return status.length > 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function resolvePrNumber(sha: string, env: EnvLike, gh: GhFn): Promise<string> {
|
|
47
|
+
const override = pick(env.XCSH_BUILD_PR);
|
|
48
|
+
if (override) return override;
|
|
49
|
+
if (!sha) return "";
|
|
50
|
+
return await gh(sha);
|
|
51
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { $ } from "bun";
|
|
5
|
+
import {
|
|
6
|
+
type EnvLike,
|
|
7
|
+
resolveBranch,
|
|
8
|
+
resolveCommit,
|
|
9
|
+
resolveDirty,
|
|
10
|
+
resolvePrNumber,
|
|
11
|
+
resolveTag,
|
|
12
|
+
} from "./build-info/resolvers";
|
|
13
|
+
|
|
14
|
+
const repoRoot = path.resolve(import.meta.dir, "../../..");
|
|
15
|
+
const outputPath = path.resolve(import.meta.dir, "../src/internal-urls/build-info.generated.ts");
|
|
16
|
+
const utilsPackageJsonPath = path.resolve(repoRoot, "packages/utils/package.json");
|
|
17
|
+
|
|
18
|
+
const REPO_SLUG = "f5xc-salesdemos/xcsh";
|
|
19
|
+
const REPO_URL = `https://github.com/${REPO_SLUG}`;
|
|
20
|
+
|
|
21
|
+
async function git(args: string[]): Promise<string> {
|
|
22
|
+
try {
|
|
23
|
+
const result = await $`git ${args}`.cwd(repoRoot).quiet();
|
|
24
|
+
return result.stdout.toString().trim();
|
|
25
|
+
} catch {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function ghPrForSha(sha: string): Promise<string> {
|
|
31
|
+
try {
|
|
32
|
+
const result =
|
|
33
|
+
await $`gh pr list --search ${sha} --state merged --json number --limit 1 --jq ${".[0].number // empty"}`
|
|
34
|
+
.cwd(repoRoot)
|
|
35
|
+
.quiet();
|
|
36
|
+
return result.stdout.toString().trim();
|
|
37
|
+
} catch {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function resolveCommitDate(sha: string): Promise<string> {
|
|
43
|
+
if (!sha) return "";
|
|
44
|
+
return await git(["log", "-1", "--format=%cI", sha]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const env = process.env as EnvLike;
|
|
48
|
+
|
|
49
|
+
const utilsPackageJson = (await Bun.file(utilsPackageJsonPath).json()) as { version: string };
|
|
50
|
+
const version = utilsPackageJson.version;
|
|
51
|
+
const commit = await resolveCommit(env, git);
|
|
52
|
+
const shortCommit = commit ? commit.slice(0, 7) : "";
|
|
53
|
+
const branch = await resolveBranch(env, git);
|
|
54
|
+
const tag = await resolveTag(env, git);
|
|
55
|
+
const commitDate = await resolveCommitDate(commit);
|
|
56
|
+
const dirty = await resolveDirty(env, git);
|
|
57
|
+
const prNumber = await resolvePrNumber(commit, env, ghPrForSha);
|
|
58
|
+
const buildDate = new Date().toISOString();
|
|
59
|
+
const releaseUrl = `${REPO_URL}/releases/tag/v${version}`;
|
|
60
|
+
const commitUrl = commit ? `${REPO_URL}/commit/${commit}` : REPO_URL;
|
|
61
|
+
|
|
62
|
+
const output = [
|
|
63
|
+
"// Auto-generated by scripts/generate-build-info.ts - DO NOT EDIT",
|
|
64
|
+
"",
|
|
65
|
+
"export interface BuildInfo {",
|
|
66
|
+
"\treadonly version: string;",
|
|
67
|
+
"\treadonly commit: string;",
|
|
68
|
+
"\treadonly shortCommit: string;",
|
|
69
|
+
"\treadonly branch: string;",
|
|
70
|
+
"\treadonly tag: string;",
|
|
71
|
+
"\treadonly commitDate: string;",
|
|
72
|
+
"\treadonly buildDate: string;",
|
|
73
|
+
"\treadonly dirty: boolean;",
|
|
74
|
+
"\treadonly prNumber: string;",
|
|
75
|
+
"\treadonly repoUrl: string;",
|
|
76
|
+
"\treadonly repoSlug: string;",
|
|
77
|
+
"\treadonly commitUrl: string;",
|
|
78
|
+
"\treadonly releaseUrl: string;",
|
|
79
|
+
"}",
|
|
80
|
+
"",
|
|
81
|
+
`export const BUILD_INFO: BuildInfo = ${JSON.stringify(
|
|
82
|
+
{
|
|
83
|
+
version,
|
|
84
|
+
commit,
|
|
85
|
+
shortCommit,
|
|
86
|
+
branch,
|
|
87
|
+
tag,
|
|
88
|
+
commitDate,
|
|
89
|
+
buildDate,
|
|
90
|
+
dirty,
|
|
91
|
+
prNumber,
|
|
92
|
+
repoUrl: REPO_URL,
|
|
93
|
+
repoSlug: REPO_SLUG,
|
|
94
|
+
commitUrl,
|
|
95
|
+
releaseUrl,
|
|
96
|
+
},
|
|
97
|
+
null,
|
|
98
|
+
"\t",
|
|
99
|
+
)};`,
|
|
100
|
+
"",
|
|
101
|
+
].join("\n");
|
|
102
|
+
|
|
103
|
+
await Bun.write(outputPath, output);
|
|
104
|
+
console.log(
|
|
105
|
+
`Generated ${path.relative(process.cwd(), outputPath)} (v${version}, ${shortCommit || "no-commit"}, branch=${branch}${tag ? `, tag=${tag}` : ""}${dirty ? ", dirty" : ""})`,
|
|
106
|
+
);
|
|
@@ -438,7 +438,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
438
438
|
defaultThinkingLevel: {
|
|
439
439
|
type: "enum",
|
|
440
440
|
values: THINKING_EFFORTS,
|
|
441
|
-
default: "
|
|
441
|
+
default: "xhigh",
|
|
442
442
|
ui: {
|
|
443
443
|
tab: "model",
|
|
444
444
|
label: "Thinking Level",
|
|
@@ -449,7 +449,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
449
449
|
|
|
450
450
|
hideThinkingBlock: {
|
|
451
451
|
type: "boolean",
|
|
452
|
-
default:
|
|
452
|
+
default: true,
|
|
453
453
|
ui: { tab: "model", label: "Hide Thinking Blocks", description: "Hide thinking blocks in assistant responses" },
|
|
454
454
|
},
|
|
455
455
|
|
|
@@ -600,7 +600,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
600
600
|
interruptMode: {
|
|
601
601
|
type: "enum",
|
|
602
602
|
values: ["immediate", "wait"] as const,
|
|
603
|
-
default: "
|
|
603
|
+
default: "wait",
|
|
604
604
|
ui: {
|
|
605
605
|
tab: "interaction",
|
|
606
606
|
label: "Interrupt Mode",
|
|
@@ -1165,7 +1165,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1165
1165
|
|
|
1166
1166
|
"todo.eager": {
|
|
1167
1167
|
type: "boolean",
|
|
1168
|
-
default:
|
|
1168
|
+
default: true,
|
|
1169
1169
|
ui: {
|
|
1170
1170
|
tab: "tools",
|
|
1171
1171
|
label: "Create Todos Automatically",
|
|
@@ -1188,7 +1188,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1188
1188
|
|
|
1189
1189
|
"grep.contextBefore": {
|
|
1190
1190
|
type: "number",
|
|
1191
|
-
default:
|
|
1191
|
+
default: 3,
|
|
1192
1192
|
ui: {
|
|
1193
1193
|
tab: "tools",
|
|
1194
1194
|
label: "Grep Context Before",
|
|
@@ -1199,7 +1199,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1199
1199
|
|
|
1200
1200
|
"grep.contextAfter": {
|
|
1201
1201
|
type: "number",
|
|
1202
|
-
default:
|
|
1202
|
+
default: 3,
|
|
1203
1203
|
ui: {
|
|
1204
1204
|
tab: "tools",
|
|
1205
1205
|
label: "Grep Context After",
|
|
@@ -1503,7 +1503,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1503
1503
|
|
|
1504
1504
|
"task.eager": {
|
|
1505
1505
|
type: "boolean",
|
|
1506
|
-
default:
|
|
1506
|
+
default: true,
|
|
1507
1507
|
ui: {
|
|
1508
1508
|
tab: "tasks",
|
|
1509
1509
|
label: "Prefer Task Delegation",
|
|
@@ -1599,7 +1599,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1599
1599
|
// Secret handling
|
|
1600
1600
|
"secrets.enabled": {
|
|
1601
1601
|
type: "boolean",
|
|
1602
|
-
default:
|
|
1602
|
+
default: true,
|
|
1603
1603
|
ui: { tab: "providers", label: "Hide Secrets", description: "Obfuscate secrets before sending to AI providers" },
|
|
1604
1604
|
},
|
|
1605
1605
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { $ } from "bun";
|
|
4
|
+
import { BUILD_INFO, type BuildInfo } from "./build-info.generated";
|
|
5
|
+
|
|
6
|
+
export type BuildInfoSource = "compiled" | "live-git" | "embedded-fallback";
|
|
7
|
+
|
|
8
|
+
export interface RuntimeBuildInfo extends BuildInfo {
|
|
9
|
+
readonly source: BuildInfoSource;
|
|
10
|
+
readonly resolvedAt: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RuntimeBuildInfoDeps {
|
|
14
|
+
readonly isCompiled: boolean;
|
|
15
|
+
readonly gitAvailable: () => boolean;
|
|
16
|
+
readonly git: (args: string[]) => Promise<string>;
|
|
17
|
+
readonly now: () => Date;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function shortOf(sha: string): string {
|
|
21
|
+
return sha ? sha.slice(0, 7) : "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function commitUrl(repoUrl: string, commit: string): string {
|
|
25
|
+
return commit ? `${repoUrl}/commit/${commit}` : repoUrl;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function firstRemoteBranch(output: string): string {
|
|
29
|
+
for (const raw of output.split("\n")) {
|
|
30
|
+
const line = raw.trim();
|
|
31
|
+
if (!line || line.includes("->") || line === "HEAD") continue;
|
|
32
|
+
const stripped = line.replace(/^origin\//, "");
|
|
33
|
+
if (stripped && stripped !== "HEAD") return stripped;
|
|
34
|
+
}
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function liveBranch(git: RuntimeBuildInfoDeps["git"]): Promise<string> {
|
|
39
|
+
const abbrev = await git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
40
|
+
if (abbrev && abbrev !== "HEAD") return abbrev;
|
|
41
|
+
const remote = await git(["branch", "-r", "--contains", "HEAD"]);
|
|
42
|
+
return firstRemoteBranch(remote);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function resolveRuntimeBuildInfo(
|
|
46
|
+
embedded: BuildInfo,
|
|
47
|
+
deps: RuntimeBuildInfoDeps,
|
|
48
|
+
): Promise<RuntimeBuildInfo> {
|
|
49
|
+
const resolvedAt = deps.now().toISOString();
|
|
50
|
+
|
|
51
|
+
if (deps.isCompiled) {
|
|
52
|
+
return { ...embedded, source: "compiled", resolvedAt };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!deps.gitAvailable()) {
|
|
56
|
+
return { ...embedded, source: "embedded-fallback", resolvedAt };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const commit = (await deps.git(["rev-parse", "HEAD"])) || embedded.commit;
|
|
60
|
+
const branch = (await liveBranch(deps.git)) || embedded.branch;
|
|
61
|
+
const tag = await deps.git(["describe", "--exact-match", "--tags", "HEAD"]);
|
|
62
|
+
const status = await deps.git(["status", "--porcelain"]);
|
|
63
|
+
const dirty = status.length > 0;
|
|
64
|
+
const commitDate = (await deps.git(["log", "-1", "--format=%cI", "HEAD"])) || embedded.commitDate;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
version: embedded.version,
|
|
68
|
+
commit,
|
|
69
|
+
shortCommit: shortOf(commit),
|
|
70
|
+
branch,
|
|
71
|
+
tag,
|
|
72
|
+
commitDate,
|
|
73
|
+
buildDate: embedded.buildDate,
|
|
74
|
+
dirty,
|
|
75
|
+
prNumber: embedded.prNumber,
|
|
76
|
+
repoUrl: embedded.repoUrl,
|
|
77
|
+
repoSlug: embedded.repoSlug,
|
|
78
|
+
commitUrl: commitUrl(embedded.repoUrl, commit),
|
|
79
|
+
releaseUrl: embedded.releaseUrl,
|
|
80
|
+
source: "live-git",
|
|
81
|
+
resolvedAt,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function renderAboutDoc(info: RuntimeBuildInfo): string {
|
|
86
|
+
return [
|
|
87
|
+
"# xcsh — identity and build fingerprint",
|
|
88
|
+
"",
|
|
89
|
+
"You are running inside xcsh, a coworker-style CLI for F5 sales engineers:",
|
|
90
|
+
"demos, docs, research, MEDDPICC, customer meeting prep, and day-to-day SE tasks.",
|
|
91
|
+
"This document is the authoritative answer when the user asks about xcsh itself.",
|
|
92
|
+
"",
|
|
93
|
+
"## Build fingerprint",
|
|
94
|
+
"",
|
|
95
|
+
`- Version: \`${info.version}\``,
|
|
96
|
+
`- Commit: \`${info.shortCommit || "unknown"}\` (full: \`${info.commit || "unknown"}\`)`,
|
|
97
|
+
`- Branch: \`${info.branch || "unknown"}\``,
|
|
98
|
+
`- Tag: ${info.tag ? `\`${info.tag}\`` : "(not a tagged build)"}`,
|
|
99
|
+
`- Commit date: ${info.commitDate || "unknown"}`,
|
|
100
|
+
`- Build date: ${info.buildDate || "unknown"}`,
|
|
101
|
+
`- Built from dirty tree: ${info.dirty ? "yes" : "no"}`,
|
|
102
|
+
`- PR that shipped this version: ${info.prNumber ? `#${info.prNumber}` : "unknown (resolve via gh if needed)"}`,
|
|
103
|
+
`- Provenance source: \`${info.source}\` (resolved at ${info.resolvedAt})`,
|
|
104
|
+
"",
|
|
105
|
+
"## Source of truth",
|
|
106
|
+
"",
|
|
107
|
+
`- Repository: ${info.repoUrl}`,
|
|
108
|
+
`- Issues: ${info.repoUrl}/issues`,
|
|
109
|
+
`- Pull requests: ${info.repoUrl}/pulls`,
|
|
110
|
+
`- This commit on GitHub: ${info.commitUrl}`,
|
|
111
|
+
`- Release for this version: ${info.releaseUrl}`,
|
|
112
|
+
"",
|
|
113
|
+
"## What to do when asked about xcsh itself",
|
|
114
|
+
"",
|
|
115
|
+
"1. Confirm the user is running the version above. If unsure, ask them to run `xcsh --version`.",
|
|
116
|
+
"2. Check recent changes with `gh pr list --repo f5xc-salesdemos/xcsh --base main --state merged --limit 20`",
|
|
117
|
+
" or `git log --oneline -n 20` if you have a local clone. A fix may already be on `main`.",
|
|
118
|
+
"3. If behavior contradicts `xcsh://…` docs, read the actual source under the repo above to determine",
|
|
119
|
+
" whether the binary is wrong or the doc is stale.",
|
|
120
|
+
"4. Classify the report as one of: **bug**, **feature**, **docs-drift**, or **config/usage**.",
|
|
121
|
+
"5. Offer to file it with",
|
|
122
|
+
" `gh issue create --repo f5xc-salesdemos/xcsh --title ... --body ...`, referencing the commit above.",
|
|
123
|
+
"",
|
|
124
|
+
"## What NOT to assume",
|
|
125
|
+
"",
|
|
126
|
+
"- Do not guess the repo URL, version, or commit — use the values above.",
|
|
127
|
+
"- Do not invent recent changes; fetch them at runtime via `gh` or `git`.",
|
|
128
|
+
"- Do not read this document unless the user asked about xcsh itself.",
|
|
129
|
+
"",
|
|
130
|
+
].join("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Bun-embedded module URL markers. Mirrors the native addon loader
|
|
134
|
+
// (see xcsh://natives-addon-loader-runtime.md) so compiled-mode detection stays
|
|
135
|
+
// consistent across the codebase. Update all three in lockstep if Bun changes them.
|
|
136
|
+
const COMPILED_URL_MARKERS = ["$bunfs", "~BUN", "%7EBUN"] as const;
|
|
137
|
+
|
|
138
|
+
export function detectCompiledRuntime(
|
|
139
|
+
metaUrl: string,
|
|
140
|
+
env: Readonly<Record<string, string | undefined>> = {},
|
|
141
|
+
): boolean {
|
|
142
|
+
if (env.PI_COMPILED) return true;
|
|
143
|
+
return COMPILED_URL_MARKERS.some(marker => metaUrl.includes(marker));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function findGitRoot(startDir: string, fsExists: (p: string) => boolean = fs.existsSync): string | null {
|
|
147
|
+
let current = path.resolve(startDir);
|
|
148
|
+
while (true) {
|
|
149
|
+
if (fsExists(path.join(current, ".git"))) return current;
|
|
150
|
+
const parent = path.dirname(current);
|
|
151
|
+
if (parent === current) return null;
|
|
152
|
+
current = parent;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function defaultRuntimeDeps(): RuntimeBuildInfoDeps {
|
|
157
|
+
const isCompiled = detectCompiledRuntime(import.meta.url, Bun.env);
|
|
158
|
+
const gitRoot = isCompiled ? null : findGitRoot(import.meta.dir);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
isCompiled,
|
|
162
|
+
gitAvailable: () => gitRoot !== null,
|
|
163
|
+
git: async (args: string[]): Promise<string> => {
|
|
164
|
+
if (!gitRoot) return "";
|
|
165
|
+
try {
|
|
166
|
+
const result = await $`git ${args}`.cwd(gitRoot).quiet();
|
|
167
|
+
return result.stdout.toString().trim();
|
|
168
|
+
} catch {
|
|
169
|
+
return "";
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
now: () => new Date(),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Intentionally no cache. `xcsh://about` is invoked once per xcsh-related question
|
|
177
|
+
// at agent-tool-call granularity; stale fingerprints after branch-switch / dirty-tree
|
|
178
|
+
// changes would silently lie under source-mode. Re-resolving costs ~30ms of git subprocess
|
|
179
|
+
// time in source mode and ~0ms in compiled mode (where we return embedded BUILD_INFO).
|
|
180
|
+
export function getRuntimeBuildInfo(): Promise<RuntimeBuildInfo> {
|
|
181
|
+
return resolveRuntimeBuildInfo(BUILD_INFO, defaultRuntimeDeps());
|
|
182
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Auto-generated by scripts/generate-build-info.ts - DO NOT EDIT
|
|
2
|
+
|
|
3
|
+
export interface BuildInfo {
|
|
4
|
+
readonly version: string;
|
|
5
|
+
readonly commit: string;
|
|
6
|
+
readonly shortCommit: string;
|
|
7
|
+
readonly branch: string;
|
|
8
|
+
readonly tag: string;
|
|
9
|
+
readonly commitDate: string;
|
|
10
|
+
readonly buildDate: string;
|
|
11
|
+
readonly dirty: boolean;
|
|
12
|
+
readonly prNumber: string;
|
|
13
|
+
readonly repoUrl: string;
|
|
14
|
+
readonly repoSlug: string;
|
|
15
|
+
readonly commitUrl: string;
|
|
16
|
+
readonly releaseUrl: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const BUILD_INFO: BuildInfo = {
|
|
20
|
+
"version": "18.1.0",
|
|
21
|
+
"commit": "caf6a7fa027b863b9f421a8d8cc7cc4df2ec8136",
|
|
22
|
+
"shortCommit": "caf6a7f",
|
|
23
|
+
"branch": "main",
|
|
24
|
+
"tag": "v18.1.0",
|
|
25
|
+
"commitDate": "2026-04-20T07:08:46Z",
|
|
26
|
+
"buildDate": "2026-04-20T07:32:00.751Z",
|
|
27
|
+
"dirty": false,
|
|
28
|
+
"prNumber": "",
|
|
29
|
+
"repoUrl": "https://github.com/f5xc-salesdemos/xcsh",
|
|
30
|
+
"repoSlug": "f5xc-salesdemos/xcsh",
|
|
31
|
+
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/caf6a7fa027b863b9f421a8d8cc7cc4df2ec8136",
|
|
32
|
+
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.1.0"
|
|
33
|
+
};
|