@oh-my-pi/pi-coding-agent 16.2.9 → 16.2.11
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 +29 -0
- package/dist/cli.js +3178 -3177
- package/dist/types/extensibility/skills.d.ts +29 -0
- package/dist/types/modes/components/todo-reminder.d.ts +3 -1
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +0 -1
- package/dist/types/modes/skill-command.d.ts +1 -1
- package/dist/types/modes/types.d.ts +0 -1
- package/dist/types/stt/asr-client.d.ts +7 -3
- package/package.json +14 -13
- package/scripts/bundle-dist.ts +23 -4
- package/scripts/generate-docs-index.ts +116 -24
- package/src/async/job-manager.ts +27 -3
- package/src/cli/grep-cli.ts +1 -1
- package/src/config/model-discovery.ts +118 -76
- package/src/extensibility/skills.ts +77 -0
- package/src/internal-urls/docs-index.generated.txt +2 -2
- package/src/lsp/config.ts +17 -3
- package/src/modes/acp/acp-agent.ts +6 -9
- package/src/modes/components/todo-reminder.ts +5 -1
- package/src/modes/controllers/event-controller.ts +40 -24
- package/src/modes/controllers/selector-controller.ts +57 -35
- package/src/modes/controllers/tool-args-reveal.ts +12 -0
- package/src/modes/interactive-mode.ts +0 -6
- package/src/modes/rpc/rpc-mode.ts +5 -8
- package/src/modes/skill-command.ts +8 -20
- package/src/modes/types.ts +0 -1
- package/src/stt/asr-client.ts +87 -27
- package/src/stt/downloader.ts +8 -2
- package/src/task/executor.ts +1 -3
- package/src/task/index.ts +14 -5
- package/src/tools/ast-grep.ts +34 -12
- package/src/tools/grep.ts +11 -8
- package/src/utils/git.ts +22 -1
|
@@ -35,6 +35,31 @@ import type { ProviderDiscovery } from "./models-config-schema";
|
|
|
35
35
|
export const DISCOVERY_DEFAULT_CONTEXT_WINDOW = OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW;
|
|
36
36
|
export const DISCOVERY_DEFAULT_MAX_TOKENS = OPENAI_COMPAT_DISCOVERY_DEFAULT_MAX_TOKENS;
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Run `fn` with an abort signal that fires after `timeoutMs`, clearing the
|
|
40
|
+
* backing timer the instant the operation settles.
|
|
41
|
+
*
|
|
42
|
+
* Unlike the built-in abort-signal timeout API, the timer never outlives the
|
|
43
|
+
* request: on the success path it is cancelled before `fn` resolves, so the
|
|
44
|
+
* signal is never aborted and no pending callback lingers on the heap. A leaked
|
|
45
|
+
* abort-signal timeout (e.g. discovery against a mocked fetch that resolves
|
|
46
|
+
* instantly) fires seconds later and sets its abort `reason` — which crashed
|
|
47
|
+
* Bun's concurrent GC while it marked the signal's wrapped reason during an
|
|
48
|
+
* unrelated allocation (`JSAbortSignal::visitAdditionalChildren`).
|
|
49
|
+
*/
|
|
50
|
+
async function withTimeoutSignal<T>(timeoutMs: number, fn: (signal: AbortSignal) => Promise<T>): Promise<T> {
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
const timer = setTimeout(
|
|
53
|
+
() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")),
|
|
54
|
+
timeoutMs,
|
|
55
|
+
);
|
|
56
|
+
try {
|
|
57
|
+
return await fn(controller.signal);
|
|
58
|
+
} finally {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
38
63
|
const DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434";
|
|
39
64
|
const OLLAMA_HOST_DEFAULT_PORT = "11434";
|
|
40
65
|
|
|
@@ -288,16 +313,18 @@ async function discoverOllamaModelMetadata(
|
|
|
288
313
|
): Promise<OllamaDiscoveredModelMetadata | null> {
|
|
289
314
|
const showUrl = `${endpoint}/api/show`;
|
|
290
315
|
try {
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
316
|
+
const payload = await withTimeoutSignal(150, async signal => {
|
|
317
|
+
const response = await ctx.fetch(showUrl, {
|
|
318
|
+
method: "POST",
|
|
319
|
+
headers: { ...(headers ?? {}), "Content-Type": "application/json" },
|
|
320
|
+
body: JSON.stringify({ model: modelId }),
|
|
321
|
+
signal,
|
|
322
|
+
});
|
|
323
|
+
if (!response.ok) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
return (await response.json()) as unknown;
|
|
296
327
|
});
|
|
297
|
-
if (!response.ok) {
|
|
298
|
-
return null;
|
|
299
|
-
}
|
|
300
|
-
const payload = (await response.json()) as unknown;
|
|
301
328
|
if (!isRecord(payload)) {
|
|
302
329
|
return null;
|
|
303
330
|
}
|
|
@@ -339,14 +366,16 @@ export async function discoverOllamaModels(
|
|
|
339
366
|
const endpoint = normalizeOllamaBaseUrl(providerConfig.baseUrl);
|
|
340
367
|
const tagsUrl = `${endpoint}/api/tags`;
|
|
341
368
|
const headers = { ...(providerConfig.headers ?? {}) };
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
369
|
+
const payload = await withTimeoutSignal(250, async signal => {
|
|
370
|
+
const response = await ctx.fetch(tagsUrl, {
|
|
371
|
+
headers,
|
|
372
|
+
signal,
|
|
373
|
+
});
|
|
374
|
+
if (!response.ok) {
|
|
375
|
+
throw new Error(`HTTP ${response.status} from ${tagsUrl}`);
|
|
376
|
+
}
|
|
377
|
+
return (await response.json()) as { models?: Array<{ name?: string; model?: string }> };
|
|
345
378
|
});
|
|
346
|
-
if (!response.ok) {
|
|
347
|
-
throw new Error(`HTTP ${response.status} from ${tagsUrl}`);
|
|
348
|
-
}
|
|
349
|
-
const payload = (await response.json()) as { models?: Array<{ name?: string; model?: string }> };
|
|
350
379
|
const entries = (payload.models ?? []).flatMap(item => {
|
|
351
380
|
const id = item.model || item.name;
|
|
352
381
|
return id ? [{ id, name: item.name || id }] : [];
|
|
@@ -384,14 +413,16 @@ async function discoverLlamaCppServerMetadata(
|
|
|
384
413
|
): Promise<LlamaCppDiscoveredServerMetadata | null> {
|
|
385
414
|
const propsUrl = `${toLlamaCppNativeBaseUrl(baseUrl)}/props`;
|
|
386
415
|
try {
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
416
|
+
const payload = await withTimeoutSignal(150, async signal => {
|
|
417
|
+
const response = await ctx.fetch(propsUrl, {
|
|
418
|
+
headers,
|
|
419
|
+
signal,
|
|
420
|
+
});
|
|
421
|
+
if (!response.ok) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
return (await response.json()) as unknown;
|
|
390
425
|
});
|
|
391
|
-
if (!response.ok) {
|
|
392
|
-
return null;
|
|
393
|
-
}
|
|
394
|
-
const payload = (await response.json()) as unknown;
|
|
395
426
|
if (!isRecord(payload)) {
|
|
396
427
|
return null;
|
|
397
428
|
}
|
|
@@ -415,24 +446,26 @@ export async function discoverLlamaCppModels(
|
|
|
415
446
|
const baseHeaders: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
416
447
|
let headers = baseHeaders;
|
|
417
448
|
const attempt = async (h: Record<string, string>) => {
|
|
418
|
-
const [
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
449
|
+
const [payload, metadata] = await Promise.all([
|
|
450
|
+
withTimeoutSignal(250, async signal => {
|
|
451
|
+
const response = await ctx.fetch(modelsUrl, {
|
|
452
|
+
headers: h,
|
|
453
|
+
signal,
|
|
454
|
+
});
|
|
455
|
+
if (!response.ok) {
|
|
456
|
+
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
457
|
+
}
|
|
458
|
+
headers = h;
|
|
459
|
+
return (await response.json()) as unknown;
|
|
422
460
|
}),
|
|
423
461
|
discoverLlamaCppServerMetadata(ctx, baseUrl, h),
|
|
424
462
|
]);
|
|
425
|
-
|
|
426
|
-
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
427
|
-
}
|
|
428
|
-
headers = h;
|
|
429
|
-
return [response, metadata] as const;
|
|
463
|
+
return [payload, metadata] as const;
|
|
430
464
|
};
|
|
431
465
|
const apiKey = await ctx.getBearerApiKeyResolver(providerConfig.provider);
|
|
432
|
-
const [
|
|
466
|
+
const [payload, serverMetadata] = apiKey
|
|
433
467
|
? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
|
|
434
468
|
: await attempt(baseHeaders);
|
|
435
|
-
const payload = (await response.json()) as unknown;
|
|
436
469
|
const models = parseLlamaCppModelList(payload);
|
|
437
470
|
const discovered: Model<Api>[] = [];
|
|
438
471
|
for (const item of models) {
|
|
@@ -476,17 +509,22 @@ export async function discoverLlamaCppModelRuntimeMetadata(
|
|
|
476
509
|
const modelsUrl = `${baseUrl}/models`;
|
|
477
510
|
const baseHeaders: Record<string, string> = { ...(model.headers ?? {}) };
|
|
478
511
|
const attempt = async (headers: Record<string, string>) => {
|
|
479
|
-
const [
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
512
|
+
const [entries, serverMetadata] = await Promise.all([
|
|
513
|
+
withTimeoutSignal(250, async signal => {
|
|
514
|
+
const response = await ctx.fetch(modelsUrl, {
|
|
515
|
+
headers,
|
|
516
|
+
signal,
|
|
517
|
+
});
|
|
518
|
+
if (!response.ok) {
|
|
519
|
+
return undefined;
|
|
520
|
+
}
|
|
521
|
+
return parseLlamaCppModelList(await response.json());
|
|
483
522
|
}),
|
|
484
523
|
discoverLlamaCppServerMetadata(ctx, baseUrl, headers),
|
|
485
524
|
]);
|
|
486
|
-
if (!
|
|
525
|
+
if (!entries) {
|
|
487
526
|
return undefined;
|
|
488
527
|
}
|
|
489
|
-
const entries = parseLlamaCppModelList(await response.json());
|
|
490
528
|
const entry = entries.find(entry => entry.id === model.id);
|
|
491
529
|
if (!entry) {
|
|
492
530
|
return undefined;
|
|
@@ -524,26 +562,28 @@ export async function discoverOpenAIModelsList(
|
|
|
524
562
|
providerConfig.discovery.type === "lm-studio"
|
|
525
563
|
? fetchLmStudioNativeModelMetadata(baseUrl, ctx.fetch, { headers: h })
|
|
526
564
|
: Promise.resolve(null);
|
|
527
|
-
const [
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
565
|
+
const [payload, nativeMetadata] = await Promise.all([
|
|
566
|
+
withTimeoutSignal(10_000, async signal => {
|
|
567
|
+
const res = await ctx.fetch(modelsUrl, {
|
|
568
|
+
headers: h,
|
|
569
|
+
signal,
|
|
570
|
+
});
|
|
571
|
+
if (!res.ok) {
|
|
572
|
+
throw new Error(`HTTP ${res.status} from ${modelsUrl}`);
|
|
573
|
+
}
|
|
574
|
+
headers = h;
|
|
575
|
+
return (await res.json()) as {
|
|
576
|
+
data?: Array<{ id?: string; max_model_len?: unknown; context_length?: unknown }>;
|
|
577
|
+
};
|
|
531
578
|
}),
|
|
532
579
|
nativeMetadataPromise,
|
|
533
580
|
]);
|
|
534
|
-
|
|
535
|
-
throw new Error(`HTTP ${res.status} from ${modelsUrl}`);
|
|
536
|
-
}
|
|
537
|
-
headers = h;
|
|
538
|
-
return [res, nativeMetadata] as const;
|
|
581
|
+
return [payload, nativeMetadata] as const;
|
|
539
582
|
};
|
|
540
583
|
const apiKey = await ctx.getBearerApiKeyResolver(providerConfig.provider);
|
|
541
|
-
const [
|
|
584
|
+
const [payload, nativeMetadata] = apiKey
|
|
542
585
|
? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
|
|
543
586
|
: await attempt(baseHeaders);
|
|
544
|
-
const payload = (await response.json()) as {
|
|
545
|
-
data?: Array<{ id?: string; max_model_len?: unknown; context_length?: unknown }>;
|
|
546
|
-
};
|
|
547
587
|
const models = payload.data ?? [];
|
|
548
588
|
const discovered: Model<Api>[] = [];
|
|
549
589
|
for (const item of models) {
|
|
@@ -600,15 +640,17 @@ export async function discoverLiteLLMModels(
|
|
|
600
640
|
}
|
|
601
641
|
return response;
|
|
602
642
|
};
|
|
603
|
-
const models = await
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
643
|
+
const models = await withTimeoutSignal(10_000, signal =>
|
|
644
|
+
fetchLiteLLMRichModels({
|
|
645
|
+
api: providerConfig.api,
|
|
646
|
+
provider: providerConfig.provider,
|
|
647
|
+
baseUrl,
|
|
648
|
+
headers: h,
|
|
649
|
+
fetch: authAwareFetch,
|
|
650
|
+
referenceResolver: resolveReference,
|
|
651
|
+
signal,
|
|
652
|
+
}),
|
|
653
|
+
);
|
|
612
654
|
if (authError && models === null) {
|
|
613
655
|
throw authError;
|
|
614
656
|
}
|
|
@@ -657,24 +699,24 @@ export async function discoverProxyModels(
|
|
|
657
699
|
|
|
658
700
|
const baseHeaders: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
659
701
|
let headers = baseHeaders;
|
|
660
|
-
const attempt = async (h: Record<string, string>) =>
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
702
|
+
const attempt = async (h: Record<string, string>) =>
|
|
703
|
+
withTimeoutSignal(10_000, async signal => {
|
|
704
|
+
const res = await ctx.fetch(modelsUrl, {
|
|
705
|
+
headers: h,
|
|
706
|
+
signal,
|
|
707
|
+
});
|
|
708
|
+
if (!res.ok) {
|
|
709
|
+
throw new Error(`HTTP ${res.status} from ${modelsUrl}`);
|
|
710
|
+
}
|
|
711
|
+
headers = h;
|
|
712
|
+
return (await res.json()) as {
|
|
713
|
+
data?: Array<{ id?: string; name?: string; supported_endpoint_types?: string[]; context_length?: number }>;
|
|
714
|
+
};
|
|
664
715
|
});
|
|
665
|
-
if (!res.ok) {
|
|
666
|
-
throw new Error(`HTTP ${res.status} from ${modelsUrl}`);
|
|
667
|
-
}
|
|
668
|
-
headers = h;
|
|
669
|
-
return res;
|
|
670
|
-
};
|
|
671
716
|
const apiKey = await ctx.getBearerApiKeyResolver(providerConfig.provider);
|
|
672
|
-
const
|
|
717
|
+
const payload = apiKey
|
|
673
718
|
? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
|
|
674
719
|
: await attempt(baseHeaders);
|
|
675
|
-
const payload = (await response.json()) as {
|
|
676
|
-
data?: Array<{ id?: string; name?: string; supported_endpoint_types?: string[]; context_length?: number }>;
|
|
677
|
-
};
|
|
678
720
|
const items = payload.data ?? [];
|
|
679
721
|
const discovered: Model<Api>[] = [];
|
|
680
722
|
for (const item of items) {
|
|
@@ -384,6 +384,83 @@ export function getSkillSlashCommandName(skill: Pick<Skill, "name">): string {
|
|
|
384
384
|
return `skill:${skill.name}`;
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Parsed `/skill:<name>` invocation: either at the start of the draft (the
|
|
389
|
+
* traditional slash-command position) or as a `/skill:<name>` token embedded
|
|
390
|
+
* mid-prompt. For the mid-prompt form the surrounding prose is threaded
|
|
391
|
+
* through as `args` so the skill sees the full user request.
|
|
392
|
+
*/
|
|
393
|
+
export interface ParsedSkillInvocation {
|
|
394
|
+
/** Bare skill name without the leading `skill:` prefix. */
|
|
395
|
+
name: string;
|
|
396
|
+
/** User-supplied arguments (everything outside the `/skill:<name>` token). */
|
|
397
|
+
args: string;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const MID_PROMPT_SKILL_RE = /(^|\s)\/skill:([^\s/]+)(\s|$)/;
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Detect a `/skill:<name>` invocation in a user draft.
|
|
404
|
+
*
|
|
405
|
+
* Returns `undefined` when the text contains no skill token. Otherwise:
|
|
406
|
+
* - Leading form (`/skill:foo bar baz`): name=`foo`, args=`bar baz`.
|
|
407
|
+
* - Mid-prompt form (`fix the bug /skill:foo focus on auth`): name=`foo`,
|
|
408
|
+
* args=`fix the bug focus on auth` — the surrounding prose collapsed
|
|
409
|
+
* into a single args string.
|
|
410
|
+
*
|
|
411
|
+
* Mid-prompt detection is disabled when the draft itself starts with a
|
|
412
|
+
* different slash command (e.g. `/compact /skill:foo`) or a local-execution
|
|
413
|
+
* sigil — `!cmd` / `!!cmd` for the bash tool and `$ cmd` / `$$ cmd` for the
|
|
414
|
+
* python tool. Those handlers run after the skill-command dispatcher and
|
|
415
|
+
* their bodies routinely contain `/skill:<name>` references that are not
|
|
416
|
+
* meant as skill invocations.
|
|
417
|
+
*/
|
|
418
|
+
export function parseSkillInvocation(text: string): ParsedSkillInvocation | undefined {
|
|
419
|
+
const trimmedStart = text.trimStart();
|
|
420
|
+
if (trimmedStart.startsWith("/skill:")) {
|
|
421
|
+
const spaceIndex = trimmedStart.indexOf(" ");
|
|
422
|
+
const name =
|
|
423
|
+
spaceIndex === -1 ? trimmedStart.slice("/skill:".length) : trimmedStart.slice("/skill:".length, spaceIndex);
|
|
424
|
+
if (!name) return undefined;
|
|
425
|
+
const args = spaceIndex === -1 ? "" : trimmedStart.slice(spaceIndex + 1).trim();
|
|
426
|
+
return { name, args };
|
|
427
|
+
}
|
|
428
|
+
if (trimmedStart.startsWith("/")) return undefined;
|
|
429
|
+
if (startsWithLocalExecutionPrefix(trimmedStart)) return undefined;
|
|
430
|
+
const match = MID_PROMPT_SKILL_RE.exec(text);
|
|
431
|
+
if (!match) return undefined;
|
|
432
|
+
const leading = match[1] ?? "";
|
|
433
|
+
const trailing = match[3] ?? "";
|
|
434
|
+
const tokenStart = match.index + leading.length;
|
|
435
|
+
const tokenEnd = match.index + match[0].length - trailing.length;
|
|
436
|
+
const name = match[2] ?? "";
|
|
437
|
+
if (!name) return undefined;
|
|
438
|
+
const before = text.slice(0, tokenStart).trimEnd();
|
|
439
|
+
const after = text.slice(tokenEnd).trimStart();
|
|
440
|
+
const args = [before, after]
|
|
441
|
+
.filter(part => part.length > 0)
|
|
442
|
+
.join(" ")
|
|
443
|
+
.trim();
|
|
444
|
+
return { name, args };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Whether the (already left-trimmed) draft begins with a TUI local-execution
|
|
449
|
+
* sigil that downstream branches will consume verbatim — `!`/`!!` for the bash
|
|
450
|
+
* tool and `$`/`$$` followed by ASCII whitespace for the python tool. Mirrors
|
|
451
|
+
* `pythonCommandPrefixLength` in `modes/controllers/input-controller` so the
|
|
452
|
+
* two checks agree without forcing a circular import.
|
|
453
|
+
*/
|
|
454
|
+
function startsWithLocalExecutionPrefix(trimmedStart: string): boolean {
|
|
455
|
+
if (trimmedStart.startsWith("!")) return true;
|
|
456
|
+
if (trimmedStart.charCodeAt(0) !== 36 /* $ */) return false;
|
|
457
|
+
if (trimmedStart.charCodeAt(1) === 123 /* { */) return false;
|
|
458
|
+
const sigilLength = trimmedStart.charCodeAt(1) === 36 /* $ */ ? 2 : 1;
|
|
459
|
+
const next = trimmedStart.charCodeAt(sigilLength);
|
|
460
|
+
if (Number.isNaN(next)) return true;
|
|
461
|
+
return next === 32 /* space */ || next === 9 /* tab */ || next === 10 /* LF */ || next === 13 /* CR */;
|
|
462
|
+
}
|
|
463
|
+
|
|
387
464
|
export async function buildSkillPromptMessage(
|
|
388
465
|
skill: Pick<Skill, "name" | "filePath">,
|
|
389
466
|
args: string,
|