@oh-my-pi/pi-coding-agent 16.3.8 → 16.3.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.
- package/CHANGELOG.md +18 -0
- package/dist/cli.js +2563 -2561
- package/dist/types/commands/say.d.ts +6 -1
- package/dist/types/commit/agentic/lock-files.d.ts +36 -0
- package/dist/types/config/model-discovery.d.ts +3 -2
- package/package.json +12 -12
- package/src/commands/say.ts +73 -30
- package/src/commit/agentic/index.ts +3 -1
- package/src/commit/agentic/lock-files.ts +107 -0
- package/src/commit/agentic/tools/git-overview.ts +1 -20
- package/src/config/model-discovery.ts +6 -3
- package/src/config/model-registry.ts +18 -7
- package/src/discovery/claude.ts +12 -8
- package/src/extensibility/skills.ts +8 -5
- package/src/tts/speakable.ts +19 -9
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import { Command } from "@oh-my-pi/pi-utils/cli";
|
|
2
2
|
export default class Say extends Command {
|
|
3
|
+
#private;
|
|
3
4
|
static description: string;
|
|
4
5
|
static args: {
|
|
5
6
|
text: import("@oh-my-pi/pi-utils/cli").ArgDescriptor & {
|
|
6
|
-
required: true;
|
|
7
7
|
description: string;
|
|
8
8
|
};
|
|
9
9
|
};
|
|
10
10
|
static flags: {
|
|
11
11
|
voice: import("@oh-my-pi/pi-utils/cli").FlagDescriptor<"string"> & {
|
|
12
12
|
description: string;
|
|
13
|
+
options: readonly string[];
|
|
13
14
|
};
|
|
14
15
|
model: import("@oh-my-pi/pi-utils/cli").FlagDescriptor<"string"> & {
|
|
15
16
|
description: string;
|
|
16
17
|
};
|
|
18
|
+
file: import("@oh-my-pi/pi-utils/cli").FlagDescriptor<"string"> & {
|
|
19
|
+
char: string;
|
|
20
|
+
description: string;
|
|
21
|
+
};
|
|
17
22
|
out: import("@oh-my-pi/pi-utils/cli").FlagDescriptor<"string"> & {
|
|
18
23
|
char: string;
|
|
19
24
|
description: string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock-file handling for the split-commit workflow.
|
|
3
|
+
*
|
|
4
|
+
* The commit agent hides these machine-generated files from analysis so the
|
|
5
|
+
* model does not waste tokens on them and does not treat them as evidence for
|
|
6
|
+
* commit boundaries. That leaves them staged but unseen: without deterministic
|
|
7
|
+
* post-plan placement the split validator rejects the plan with
|
|
8
|
+
* `Split commit plan missing staged files: <lockfile>`, and the executor
|
|
9
|
+
* (`git stage.reset` -> per-group `stage.hunks`) would silently drop the file
|
|
10
|
+
* if the validator were skipped. See issue #4632.
|
|
11
|
+
*/
|
|
12
|
+
import type { SplitCommitPlan } from "./state.js";
|
|
13
|
+
/**
|
|
14
|
+
* Lock file basename -> ordered sibling manifests. Order matters: the first
|
|
15
|
+
* manifest present in a commit group's changes wins.
|
|
16
|
+
*/
|
|
17
|
+
export declare const LOCK_FILE_MANIFESTS: Readonly<Record<string, readonly string[]>>;
|
|
18
|
+
/**
|
|
19
|
+
* Lock-file basenames the commit agent excludes from `git_overview` output and
|
|
20
|
+
* from split-commit validation. Derived from {@link LOCK_FILE_MANIFESTS} so a
|
|
21
|
+
* single edit keeps both the analysis filter and the post-plan pairing in sync.
|
|
22
|
+
*/
|
|
23
|
+
export declare const EXCLUDED_LOCK_FILES: ReadonlySet<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Attach staged lock files the model never saw to the split plan.
|
|
26
|
+
*
|
|
27
|
+
* Placement precedence per lock file:
|
|
28
|
+
* 1. commit group that touches a sibling manifest (same directory)
|
|
29
|
+
* 2. commit group that touches a manifest in any directory
|
|
30
|
+
* 3. last commit group (fallback)
|
|
31
|
+
*
|
|
32
|
+
* Mutates {@link plan} in place. No-ops on an empty plan, on lock files
|
|
33
|
+
* already present in some commit group, and on staged files that are not
|
|
34
|
+
* recognized lock files.
|
|
35
|
+
*/
|
|
36
|
+
export declare function assignLockFilesToPlan(plan: SplitCommitPlan, stagedFiles: readonly string[]): void;
|
|
@@ -38,8 +38,9 @@ export interface DiscoveryContext {
|
|
|
38
38
|
getBearerApiKeyResolver(provider: string): Promise<ApiKey | undefined>;
|
|
39
39
|
}
|
|
40
40
|
type LlamaCppDiscoveredModelRuntimeMetadata = {
|
|
41
|
-
contextWindow
|
|
42
|
-
maxTokens
|
|
41
|
+
contextWindow?: number;
|
|
42
|
+
maxTokens?: number;
|
|
43
|
+
input?: ("text" | "image")[];
|
|
43
44
|
};
|
|
44
45
|
export declare function discoverModelsByProviderType(providerConfig: DiscoveryProviderConfig, ctx: DiscoveryContext): Promise<Model<Api>[]>;
|
|
45
46
|
export declare function discoverOllamaModels(providerConfig: DiscoveryProviderConfig, ctx: DiscoveryContext): Promise<Model<Api>[]>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "16.3.
|
|
4
|
+
"version": "16.3.9",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -56,17 +56,17 @@
|
|
|
56
56
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
57
57
|
"@babel/parser": "^7.29.7",
|
|
58
58
|
"@mozilla/readability": "^0.6.0",
|
|
59
|
-
"@oh-my-pi/hashline": "16.3.
|
|
60
|
-
"@oh-my-pi/omp-stats": "16.3.
|
|
61
|
-
"@oh-my-pi/pi-agent-core": "16.3.
|
|
62
|
-
"@oh-my-pi/pi-ai": "16.3.
|
|
63
|
-
"@oh-my-pi/pi-catalog": "16.3.
|
|
64
|
-
"@oh-my-pi/pi-mnemopi": "16.3.
|
|
65
|
-
"@oh-my-pi/pi-natives": "16.3.
|
|
66
|
-
"@oh-my-pi/pi-tui": "16.3.
|
|
67
|
-
"@oh-my-pi/pi-utils": "16.3.
|
|
68
|
-
"@oh-my-pi/pi-wire": "16.3.
|
|
69
|
-
"@oh-my-pi/snapcompact": "16.3.
|
|
59
|
+
"@oh-my-pi/hashline": "16.3.9",
|
|
60
|
+
"@oh-my-pi/omp-stats": "16.3.9",
|
|
61
|
+
"@oh-my-pi/pi-agent-core": "16.3.9",
|
|
62
|
+
"@oh-my-pi/pi-ai": "16.3.9",
|
|
63
|
+
"@oh-my-pi/pi-catalog": "16.3.9",
|
|
64
|
+
"@oh-my-pi/pi-mnemopi": "16.3.9",
|
|
65
|
+
"@oh-my-pi/pi-natives": "16.3.9",
|
|
66
|
+
"@oh-my-pi/pi-tui": "16.3.9",
|
|
67
|
+
"@oh-my-pi/pi-utils": "16.3.9",
|
|
68
|
+
"@oh-my-pi/pi-wire": "16.3.9",
|
|
69
|
+
"@oh-my-pi/snapcompact": "16.3.9",
|
|
70
70
|
"@opentelemetry/api": "^1.9.1",
|
|
71
71
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
72
72
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
package/src/commands/say.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Synthesize text with the local TTS engine and play it (or save it with --out).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Text comes from the argument or --file. Input is segmented into
|
|
5
|
+
* sentence-sized chunks ({@link SpeakableStream}) and synthesized through the
|
|
6
|
+
* streaming TTS worker, so arbitrarily long text plays gaplessly instead of
|
|
7
|
+
* hitting Kokoro's single-call ~510-phoneme truncation. --out concatenates the
|
|
8
|
+
* streamed segments into one WAV. The first run downloads the configured local
|
|
9
|
+
* model into the worker's cache.
|
|
7
10
|
*/
|
|
8
|
-
import
|
|
9
|
-
import * as path from "node:path";
|
|
10
|
-
import { getProjectDir, Snowflake } from "@oh-my-pi/pi-utils";
|
|
11
|
+
import { getProjectDir } from "@oh-my-pi/pi-utils";
|
|
11
12
|
import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
|
12
13
|
import chalk from "chalk";
|
|
13
14
|
import { Settings, settings } from "../config/settings";
|
|
14
|
-
import {
|
|
15
|
+
import { TTS_LOCAL_VOICE_VALUES } from "../tts/models";
|
|
16
|
+
import { SpeakableStream } from "../tts/speakable";
|
|
17
|
+
import { StreamingAudioPlayer } from "../tts/streaming-player";
|
|
15
18
|
import { shutdownTtsClient, ttsClient } from "../tts/tts-client";
|
|
16
19
|
import { encodeWav } from "../tts/wav";
|
|
17
20
|
|
|
@@ -19,24 +22,28 @@ export default class Say extends Command {
|
|
|
19
22
|
static description = "Synthesize text with the local TTS engine and play it through the speakers";
|
|
20
23
|
|
|
21
24
|
static args = {
|
|
22
|
-
text: Args.string({
|
|
25
|
+
text: Args.string({ description: "Text to speak (or use --file)" }),
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
static flags = {
|
|
26
|
-
voice: Flags.string({ description: "Voice id" }),
|
|
29
|
+
voice: Flags.string({ description: "Voice id", options: TTS_LOCAL_VOICE_VALUES }),
|
|
27
30
|
model: Flags.string({ description: "Local TTS model key" }),
|
|
31
|
+
file: Flags.string({ char: "f", description: "Read the text to speak from this file" }),
|
|
28
32
|
out: Flags.string({ char: "o", description: "Write WAV to this path instead of playing" }),
|
|
29
33
|
};
|
|
30
34
|
|
|
31
35
|
static examples = [
|
|
32
36
|
'omp say "hello world"',
|
|
37
|
+
"omp say --file notes.md --voice bm_fable",
|
|
33
38
|
'omp say "hello world" --out /tmp/hello.wav',
|
|
34
|
-
'omp say "bonjour" --voice af_heart --model kokoro',
|
|
35
39
|
];
|
|
36
40
|
|
|
37
41
|
async run(): Promise<void> {
|
|
38
42
|
const { args, flags } = await this.parse(Say);
|
|
39
|
-
|
|
43
|
+
if (args.text && flags.file) {
|
|
44
|
+
process.stderr.write(chalk.red("error: pass either text or --file, not both\n"));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
40
47
|
|
|
41
48
|
await Settings.init({ cwd: getProjectDir() });
|
|
42
49
|
const model = flags.model ?? settings.get("tts.localModel");
|
|
@@ -55,23 +62,42 @@ export default class Say extends Command {
|
|
|
55
62
|
});
|
|
56
63
|
|
|
57
64
|
try {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"Run `omp setup speech` to install it.\n",
|
|
64
|
-
),
|
|
65
|
-
);
|
|
65
|
+
const text = flags.file ? await Bun.file(flags.file).text() : (args.text ?? "");
|
|
66
|
+
const splitter = new SpeakableStream();
|
|
67
|
+
const segments = [...splitter.push(text), ...splitter.flush()];
|
|
68
|
+
if (segments.length === 0) {
|
|
69
|
+
process.stderr.write(chalk.red("error: nothing speakable in the input\n"));
|
|
66
70
|
exitCode = 1;
|
|
67
71
|
return;
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
const
|
|
71
|
-
const
|
|
74
|
+
const stream = ttsClient.synthesizeStream(model, { voice });
|
|
75
|
+
for (const segment of segments) stream.push(segment);
|
|
76
|
+
stream.end();
|
|
72
77
|
|
|
73
78
|
if (flags.out) {
|
|
79
|
+
const pcms: Float32Array[] = [];
|
|
80
|
+
let total = 0;
|
|
81
|
+
let sampleRate = 0;
|
|
82
|
+
for await (const chunk of stream.chunks) {
|
|
83
|
+
pcms.push(chunk.pcm);
|
|
84
|
+
total += chunk.pcm.length;
|
|
85
|
+
sampleRate = chunk.sampleRate;
|
|
86
|
+
}
|
|
87
|
+
if (total === 0) {
|
|
88
|
+
this.#synthesisFailed(model);
|
|
89
|
+
exitCode = 1;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const pcm = new Float32Array(total);
|
|
93
|
+
let offset = 0;
|
|
94
|
+
for (const part of pcms) {
|
|
95
|
+
pcm.set(part, offset);
|
|
96
|
+
offset += part.length;
|
|
97
|
+
}
|
|
98
|
+
const wav = encodeWav(pcm, sampleRate);
|
|
74
99
|
await Bun.write(flags.out, wav);
|
|
100
|
+
const durationSec = total / sampleRate;
|
|
75
101
|
process.stdout.write(
|
|
76
102
|
`${chalk.green("saved")} ${flags.out} ` +
|
|
77
103
|
`${chalk.dim(`(${voice}, ${model}, ${durationSec.toFixed(1)}s, ${wav.byteLength} bytes)`)}\n`,
|
|
@@ -79,16 +105,25 @@ export default class Say extends Command {
|
|
|
79
105
|
return;
|
|
80
106
|
}
|
|
81
107
|
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
await removeTempFile(tmp);
|
|
108
|
+
const player = new StreamingAudioPlayer();
|
|
109
|
+
let spoken = 0;
|
|
110
|
+
let seconds = 0;
|
|
111
|
+
for await (const chunk of stream.chunks) {
|
|
112
|
+
player.start(chunk.sampleRate);
|
|
113
|
+
player.write(chunk.pcm);
|
|
114
|
+
spoken++;
|
|
115
|
+
seconds += chunk.pcm.length / chunk.sampleRate;
|
|
91
116
|
}
|
|
117
|
+
if (spoken === 0) {
|
|
118
|
+
player.stop();
|
|
119
|
+
this.#synthesisFailed(model);
|
|
120
|
+
exitCode = 1;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
await player.end();
|
|
124
|
+
process.stdout.write(
|
|
125
|
+
`${chalk.green("spoke")} ${chalk.dim(`(${voice}, ${model}, ${seconds.toFixed(1)}s, ${spoken} segments)`)}\n`,
|
|
126
|
+
);
|
|
92
127
|
} catch (err) {
|
|
93
128
|
process.stderr.write(chalk.red(`error: ${err instanceof Error ? err.message : String(err)}\n`));
|
|
94
129
|
exitCode = 1;
|
|
@@ -99,4 +134,12 @@ export default class Say extends Command {
|
|
|
99
134
|
|
|
100
135
|
if (exitCode !== 0) process.exit(exitCode);
|
|
101
136
|
}
|
|
137
|
+
|
|
138
|
+
#synthesisFailed(model: string): void {
|
|
139
|
+
process.stderr.write(
|
|
140
|
+
chalk.red(
|
|
141
|
+
`error: could not synthesize with local TTS model "${model}". Run \`omp setup speech\` to install it.\n`,
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
102
145
|
}
|
|
@@ -13,6 +13,7 @@ import { discoverAuthStorage, discoverContextFiles } from "../../sdk";
|
|
|
13
13
|
import * as git from "../../utils/git";
|
|
14
14
|
import { type ExistingChangelogEntries, runCommitAgentSession } from "./agent";
|
|
15
15
|
import { generateFallbackProposal } from "./fallback";
|
|
16
|
+
import { assignLockFilesToPlan } from "./lock-files";
|
|
16
17
|
import splitConfirmPrompt from "./prompts/split-confirm.md" with { type: "text" };
|
|
17
18
|
import type { CommitAgentState, CommitProposal, HunkSelector, SplitCommitPlan } from "./state";
|
|
18
19
|
import { computeDependencyOrder } from "./topo-sort";
|
|
@@ -230,6 +231,7 @@ async function runSplitCommit(
|
|
|
230
231
|
appendFilesToLastCommit(plan, ctx.additionalFiles);
|
|
231
232
|
}
|
|
232
233
|
const stagedFiles = await git.diff.changedFiles(ctx.cwd, { cached: true });
|
|
234
|
+
assignLockFilesToPlan(plan, stagedFiles);
|
|
233
235
|
const plannedFiles = new Set(plan.commits.flatMap(commit => commit.changes.map(change => change.path)));
|
|
234
236
|
const missingFiles = stagedFiles.filter(file => !plannedFiles.has(file));
|
|
235
237
|
if (missingFiles.length > 0) {
|
|
@@ -266,7 +268,7 @@ async function runSplitCommit(
|
|
|
266
268
|
throw new Error(order.error);
|
|
267
269
|
}
|
|
268
270
|
|
|
269
|
-
const stagedDiff = await git.diff(ctx.cwd, { cached: true });
|
|
271
|
+
const stagedDiff = await git.diff(ctx.cwd, { cached: true, binary: true });
|
|
270
272
|
await git.stage.reset(ctx.cwd);
|
|
271
273
|
for (const commitIndex of order) {
|
|
272
274
|
const commit = plan.commits[commitIndex];
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock-file handling for the split-commit workflow.
|
|
3
|
+
*
|
|
4
|
+
* The commit agent hides these machine-generated files from analysis so the
|
|
5
|
+
* model does not waste tokens on them and does not treat them as evidence for
|
|
6
|
+
* commit boundaries. That leaves them staged but unseen: without deterministic
|
|
7
|
+
* post-plan placement the split validator rejects the plan with
|
|
8
|
+
* `Split commit plan missing staged files: <lockfile>`, and the executor
|
|
9
|
+
* (`git stage.reset` -> per-group `stage.hunks`) would silently drop the file
|
|
10
|
+
* if the validator were skipped. See issue #4632.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { SplitCommitPlan } from "./state";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Lock file basename -> ordered sibling manifests. Order matters: the first
|
|
17
|
+
* manifest present in a commit group's changes wins.
|
|
18
|
+
*/
|
|
19
|
+
export const LOCK_FILE_MANIFESTS: Readonly<Record<string, readonly string[]>> = {
|
|
20
|
+
"Cargo.lock": ["Cargo.toml"],
|
|
21
|
+
"package-lock.json": ["package.json"],
|
|
22
|
+
"yarn.lock": ["package.json"],
|
|
23
|
+
"pnpm-lock.yaml": ["package.json"],
|
|
24
|
+
"bun.lock": ["package.json"],
|
|
25
|
+
"bun.lockb": ["package.json"],
|
|
26
|
+
"go.sum": ["go.mod"],
|
|
27
|
+
"poetry.lock": ["pyproject.toml"],
|
|
28
|
+
"Pipfile.lock": ["Pipfile"],
|
|
29
|
+
"uv.lock": ["pyproject.toml"],
|
|
30
|
+
"composer.lock": ["composer.json"],
|
|
31
|
+
"Gemfile.lock": ["Gemfile"],
|
|
32
|
+
"flake.lock": ["flake.nix"],
|
|
33
|
+
"pubspec.lock": ["pubspec.yaml"],
|
|
34
|
+
"Podfile.lock": ["Podfile"],
|
|
35
|
+
"mix.lock": ["mix.exs"],
|
|
36
|
+
"gradle.lockfile": ["build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Lock-file basenames the commit agent excludes from `git_overview` output and
|
|
41
|
+
* from split-commit validation. Derived from {@link LOCK_FILE_MANIFESTS} so a
|
|
42
|
+
* single edit keeps both the analysis filter and the post-plan pairing in sync.
|
|
43
|
+
*/
|
|
44
|
+
export const EXCLUDED_LOCK_FILES: ReadonlySet<string> = new Set(Object.keys(LOCK_FILE_MANIFESTS));
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Attach staged lock files the model never saw to the split plan.
|
|
48
|
+
*
|
|
49
|
+
* Placement precedence per lock file:
|
|
50
|
+
* 1. commit group that touches a sibling manifest (same directory)
|
|
51
|
+
* 2. commit group that touches a manifest in any directory
|
|
52
|
+
* 3. last commit group (fallback)
|
|
53
|
+
*
|
|
54
|
+
* Mutates {@link plan} in place. No-ops on an empty plan, on lock files
|
|
55
|
+
* already present in some commit group, and on staged files that are not
|
|
56
|
+
* recognized lock files.
|
|
57
|
+
*/
|
|
58
|
+
export function assignLockFilesToPlan(plan: SplitCommitPlan, stagedFiles: readonly string[]): void {
|
|
59
|
+
if (plan.commits.length === 0) return;
|
|
60
|
+
|
|
61
|
+
const planned = new Set(plan.commits.flatMap(commit => commit.changes.map(change => change.path)));
|
|
62
|
+
const orphanedLockFiles: string[] = [];
|
|
63
|
+
for (const file of stagedFiles) {
|
|
64
|
+
if (planned.has(file)) continue;
|
|
65
|
+
const parts = file.split("/");
|
|
66
|
+
const basename = parts[parts.length - 1];
|
|
67
|
+
if (EXCLUDED_LOCK_FILES.has(basename)) orphanedLockFiles.push(file);
|
|
68
|
+
}
|
|
69
|
+
if (orphanedLockFiles.length === 0) return;
|
|
70
|
+
|
|
71
|
+
for (const lockFile of orphanedLockFiles) {
|
|
72
|
+
const parts = lockFile.split("/");
|
|
73
|
+
const basename = parts[parts.length - 1];
|
|
74
|
+
const dir = parts.slice(0, -1).join("/");
|
|
75
|
+
const manifests = LOCK_FILE_MANIFESTS[basename] ?? [];
|
|
76
|
+
const targetIndex = findManifestCommitIndex(plan, dir, manifests);
|
|
77
|
+
plan.commits[targetIndex].changes.push({ path: lockFile, hunks: { type: "all" } });
|
|
78
|
+
planned.add(lockFile);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function findManifestCommitIndex(plan: SplitCommitPlan, lockDir: string, manifests: readonly string[]): number {
|
|
83
|
+
// Prefer a manifest in the same directory as the lock file — the strongest
|
|
84
|
+
// semantic signal (e.g. workspace-crate `Cargo.toml` next to `Cargo.lock`).
|
|
85
|
+
for (const manifestName of manifests) {
|
|
86
|
+
for (let i = 0; i < plan.commits.length; i++) {
|
|
87
|
+
for (const change of plan.commits[i].changes) {
|
|
88
|
+
const parts = change.path.split("/");
|
|
89
|
+
const basename = parts[parts.length - 1];
|
|
90
|
+
const dir = parts.slice(0, -1).join("/");
|
|
91
|
+
if (basename === manifestName && dir === lockDir) return i;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Fall back to any matching manifest — a monorepo may lock at repo root
|
|
96
|
+
// while the manifest sits under a subpath.
|
|
97
|
+
for (const manifestName of manifests) {
|
|
98
|
+
for (let i = 0; i < plan.commits.length; i++) {
|
|
99
|
+
for (const change of plan.commits[i].changes) {
|
|
100
|
+
const parts = change.path.split("/");
|
|
101
|
+
if (parts[parts.length - 1] === manifestName) return i;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Nothing matched: attach to the last commit so the file still ships.
|
|
106
|
+
return plan.commits.length - 1;
|
|
107
|
+
}
|
|
@@ -3,26 +3,7 @@ import type { CommitAgentState, GitOverviewSnapshot } from "../../../commit/agen
|
|
|
3
3
|
import { extractScopeCandidates } from "../../../commit/analysis/scope";
|
|
4
4
|
import type { CustomTool } from "../../../extensibility/custom-tools/types";
|
|
5
5
|
import * as git from "../../../utils/git";
|
|
6
|
-
|
|
7
|
-
const EXCLUDED_LOCK_FILES = new Set([
|
|
8
|
-
"Cargo.lock",
|
|
9
|
-
"package-lock.json",
|
|
10
|
-
"yarn.lock",
|
|
11
|
-
"pnpm-lock.yaml",
|
|
12
|
-
"bun.lock",
|
|
13
|
-
"bun.lockb",
|
|
14
|
-
"go.sum",
|
|
15
|
-
"poetry.lock",
|
|
16
|
-
"Pipfile.lock",
|
|
17
|
-
"uv.lock",
|
|
18
|
-
"composer.lock",
|
|
19
|
-
"Gemfile.lock",
|
|
20
|
-
"flake.lock",
|
|
21
|
-
"pubspec.lock",
|
|
22
|
-
"Podfile.lock",
|
|
23
|
-
"mix.lock",
|
|
24
|
-
"gradle.lockfile",
|
|
25
|
-
]);
|
|
6
|
+
import { EXCLUDED_LOCK_FILES } from "../lock-files";
|
|
26
7
|
|
|
27
8
|
function isExcludedFile(path: string): boolean {
|
|
28
9
|
const basename = path.split("/").pop() ?? path;
|
|
@@ -151,8 +151,9 @@ type LlamaCppDiscoveredServerMetadata = {
|
|
|
151
151
|
};
|
|
152
152
|
|
|
153
153
|
type LlamaCppDiscoveredModelRuntimeMetadata = {
|
|
154
|
-
contextWindow
|
|
155
|
-
maxTokens
|
|
154
|
+
contextWindow?: number;
|
|
155
|
+
maxTokens?: number;
|
|
156
|
+
input?: ("text" | "image")[];
|
|
156
157
|
};
|
|
157
158
|
|
|
158
159
|
type LlamaCppModelListEntry = {
|
|
@@ -594,12 +595,14 @@ export async function discoverLlamaCppModelRuntimeMetadata(
|
|
|
594
595
|
entry.configuredContextWindow ??
|
|
595
596
|
serverMetadata?.contextWindow ??
|
|
596
597
|
entry.trainingContextWindow;
|
|
598
|
+
const input = serverMetadata?.input;
|
|
597
599
|
if (contextWindow === undefined) {
|
|
598
|
-
return undefined;
|
|
600
|
+
return input === undefined ? undefined : { input };
|
|
599
601
|
}
|
|
600
602
|
return {
|
|
601
603
|
contextWindow,
|
|
602
604
|
maxTokens: resolveLlamaCppMaxTokens(contextWindow, serverMetadata?.maxTokens),
|
|
605
|
+
...(input !== undefined ? { input } : {}),
|
|
603
606
|
};
|
|
604
607
|
};
|
|
605
608
|
try {
|
|
@@ -866,12 +866,13 @@ export class ModelRegistry {
|
|
|
866
866
|
if (runtimeMetadata === undefined) {
|
|
867
867
|
return this.find(model.provider, model.id) ?? model;
|
|
868
868
|
}
|
|
869
|
-
const { contextWindow, maxTokens } = runtimeMetadata;
|
|
869
|
+
const { contextWindow, maxTokens, input } = runtimeMetadata;
|
|
870
870
|
const current = this.find(model.provider, model.id) ?? model;
|
|
871
871
|
const override = this.#resolveLiveModelOverride(current);
|
|
872
872
|
const customModel = this.#resolveLiveCustomModelOverlay(current);
|
|
873
873
|
const patch: ModelPatch = {};
|
|
874
874
|
if (
|
|
875
|
+
contextWindow !== undefined &&
|
|
875
876
|
override?.contextWindow === undefined &&
|
|
876
877
|
customModel?.contextWindow === undefined &&
|
|
877
878
|
current.contextWindow !== contextWindow
|
|
@@ -884,15 +885,25 @@ export class ModelRegistry {
|
|
|
884
885
|
patch.contextWindow ??
|
|
885
886
|
current.contextWindow ??
|
|
886
887
|
contextWindow;
|
|
887
|
-
|
|
888
|
+
if (maxTokens !== undefined && effectiveContextWindow !== undefined) {
|
|
889
|
+
const effectiveMaxTokens = Math.min(maxTokens, effectiveContextWindow);
|
|
890
|
+
if (
|
|
891
|
+
override?.maxTokens === undefined &&
|
|
892
|
+
customModel?.maxTokens === undefined &&
|
|
893
|
+
current.maxTokens !== effectiveMaxTokens
|
|
894
|
+
) {
|
|
895
|
+
patch.maxTokens = effectiveMaxTokens;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
888
898
|
if (
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
899
|
+
input !== undefined &&
|
|
900
|
+
override?.input === undefined &&
|
|
901
|
+
customModel?.input === undefined &&
|
|
902
|
+
(current.input.length !== input.length || current.input.some((value, index) => value !== input[index]))
|
|
892
903
|
) {
|
|
893
|
-
patch.
|
|
904
|
+
patch.input = input;
|
|
894
905
|
}
|
|
895
|
-
if (patch.contextWindow === undefined && patch.maxTokens === undefined) {
|
|
906
|
+
if (patch.contextWindow === undefined && patch.maxTokens === undefined && patch.input === undefined) {
|
|
896
907
|
return current;
|
|
897
908
|
}
|
|
898
909
|
const patched = applyModelPatch(current, patch, "merge");
|
package/src/discovery/claude.ts
CHANGED
|
@@ -167,17 +167,21 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
|
|
|
167
167
|
async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
168
168
|
const userSkillsDir = path.join(getUserClaude(ctx), "skills");
|
|
169
169
|
|
|
170
|
-
// Walk up from cwd finding .claude/skills/ in ancestors
|
|
170
|
+
// Walk up from cwd finding .claude/skills/ in ancestors. Skip $HOME:
|
|
171
|
+
// that path is already scanned as the Claude user source below, and scanning
|
|
172
|
+
// it again as project would bypass enableClaudeUser when project skills stay enabled.
|
|
171
173
|
const projectScans: Promise<LoadResult<Skill>>[] = [];
|
|
172
174
|
let current = ctx.cwd;
|
|
173
175
|
while (true) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
176
|
+
if (current !== ctx.home) {
|
|
177
|
+
projectScans.push(
|
|
178
|
+
scanSkillsFromDir(ctx, {
|
|
179
|
+
dir: path.join(current, CONFIG_DIR, "skills"),
|
|
180
|
+
providerId: PROVIDER_ID,
|
|
181
|
+
level: "project",
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
181
185
|
if (current === (ctx.repoRoot ?? ctx.home)) break;
|
|
182
186
|
const parent = path.dirname(current);
|
|
183
187
|
if (parent === current) break; // filesystem root
|
|
@@ -190,12 +190,18 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
|
|
|
190
190
|
const disabledSkillNames = new Set(
|
|
191
191
|
(disabledExtensions ?? []).filter(id => id.startsWith("skill:")).map(id => id.slice(6)),
|
|
192
192
|
);
|
|
193
|
-
//
|
|
194
|
-
|
|
193
|
+
// Select authored skills from the pre-dedup superset. `loadCapability`
|
|
194
|
+
// dedupes before source toggles, so a disabled high-priority provider must
|
|
195
|
+
// not hide an enabled lower-priority provider with the same skill name.
|
|
196
|
+
const seenAuthoredSkillNames = new Set<string>();
|
|
197
|
+
const filteredSkills = result.all.filter(capSkill => {
|
|
198
|
+
if (capSkill._source.provider === MANAGED_SKILLS_PROVIDER_ID) return false;
|
|
195
199
|
if (disabledSkillNames.has(capSkill.name)) return false;
|
|
196
200
|
if (!isSourceEnabled(capSkill._source)) return false;
|
|
197
201
|
if (matchesIgnorePatterns(capSkill.name)) return false;
|
|
198
202
|
if (!matchesIncludePatterns(capSkill.name)) return false;
|
|
203
|
+
if (seenAuthoredSkillNames.has(capSkill.name)) return false;
|
|
204
|
+
seenAuthoredSkillNames.add(capSkill.name);
|
|
199
205
|
return true;
|
|
200
206
|
});
|
|
201
207
|
|
|
@@ -213,9 +219,6 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
|
|
|
213
219
|
// Process skills with resolved paths
|
|
214
220
|
for (let i = 0; i < filteredSkills.length; i++) {
|
|
215
221
|
const capSkill = filteredSkills[i];
|
|
216
|
-
// Managed (auto-learn) skills are resolved dead-last (below) so any
|
|
217
|
-
// authored skill of the same name — from ANY provider or custom dir — wins.
|
|
218
|
-
if (capSkill._source.provider === MANAGED_SKILLS_PROVIDER_ID) continue;
|
|
219
222
|
const resolvedPath = realPaths[i];
|
|
220
223
|
|
|
221
224
|
// Skip silently if we've already loaded this exact file (via symlink)
|
package/src/tts/speakable.ts
CHANGED
|
@@ -318,15 +318,20 @@ export class SpeakableStream {
|
|
|
318
318
|
this.#drain(out);
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
/**
|
|
321
|
+
/**
|
|
322
|
+
* Emit every buffered character. Runs the bounded streaming segmenter first
|
|
323
|
+
* so a large buffer (paste-sized delta, one-shot push) prefers sentence and
|
|
324
|
+
* clause cuts within {@link MAX_SEGMENT}, instead of word-splitting whole
|
|
325
|
+
* paragraphs at the cap ("…a big jump is" / "coming"). Not byte-identical to
|
|
326
|
+
* char-by-char streaming — the soft-clause latency cut can fire earlier
|
|
327
|
+
* there — but every segment obeys the same cap and boundary preferences.
|
|
328
|
+
* {@link #extract} leaves at most MAX_SEGMENT behind, emitted as the
|
|
329
|
+
* trailing segment.
|
|
330
|
+
*/
|
|
322
331
|
#drain(out: string[]): void {
|
|
323
|
-
|
|
332
|
+
this.#extract(out);
|
|
333
|
+
const text = this.#buf;
|
|
324
334
|
this.#buf = "";
|
|
325
|
-
while (text.length > MAX_SEGMENT) {
|
|
326
|
-
const cut = findForcedCut(text, MAX_SEGMENT);
|
|
327
|
-
this.#emit(text.slice(0, cut), out);
|
|
328
|
-
text = text.slice(cut);
|
|
329
|
-
}
|
|
330
335
|
this.#emit(text, out);
|
|
331
336
|
}
|
|
332
337
|
|
|
@@ -335,14 +340,19 @@ export class SpeakableStream {
|
|
|
335
340
|
for (;;) {
|
|
336
341
|
const buf = this.#buf;
|
|
337
342
|
const min = this.#spoke ? MIN_SEGMENT : FIRST_SEGMENT_MIN;
|
|
343
|
+
// Bounded: a sentence past MAX_SEGMENT risks Kokoro's ~510-phoneme
|
|
344
|
+
// truncation — fall through to clause/word cuts instead.
|
|
338
345
|
const sentence = findSentenceCut(buf, min);
|
|
339
|
-
if (sentence !== -1) {
|
|
346
|
+
if (sentence !== -1 && sentence <= MAX_SEGMENT) {
|
|
340
347
|
this.#cut(sentence, out);
|
|
341
348
|
continue;
|
|
342
349
|
}
|
|
343
350
|
if (!this.#spoke && buf.length >= FIRST_CLAUSE_MIN) {
|
|
351
|
+
// Bounded like the sentence branch: in a one-shot buffer the earliest
|
|
352
|
+
// clause can lie far past the cap; per-char streaming would have
|
|
353
|
+
// force-cut at FIRST_FORCED_MAX long before seeing it.
|
|
344
354
|
const clause = findClauseCut(buf, FIRST_SEGMENT_MIN);
|
|
345
|
-
if (clause !== -1) {
|
|
355
|
+
if (clause !== -1 && clause <= FIRST_FORCED_MAX) {
|
|
346
356
|
this.#cut(clause, out);
|
|
347
357
|
continue;
|
|
348
358
|
}
|