@hachej/boring-agent 0.1.59 → 0.1.61
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/front/index.d.ts +46 -1
- package/dist/front/index.js +187 -0
- package/dist/server/index.js +165 -114
- package/package.json +2 -2
package/dist/front/index.d.ts
CHANGED
|
@@ -523,6 +523,51 @@ interface SessionListProps {
|
|
|
523
523
|
declare function SessionList({ sessions, activeId, loading, onSwitch, onCreate, onDelete, onLoadMore, hasMore, loadingMore, onClose, className, }: SessionListProps): react_jsx_runtime.JSX.Element;
|
|
524
524
|
declare const SessionBrowser: typeof SessionList;
|
|
525
525
|
|
|
526
|
+
interface PiSessionSearchItem {
|
|
527
|
+
id: string;
|
|
528
|
+
/** boring-ui session title maps to pi's session display name. */
|
|
529
|
+
title?: string | null;
|
|
530
|
+
/** Optional pi-style session name for callers that already expose it. */
|
|
531
|
+
name?: string | null;
|
|
532
|
+
/** Optional full transcript search text when available from a pi session listing. */
|
|
533
|
+
allMessagesText?: string | null;
|
|
534
|
+
/** Optional cwd from the underlying pi session. */
|
|
535
|
+
cwd?: string | null;
|
|
536
|
+
updatedAt?: string | number | Date | null;
|
|
537
|
+
}
|
|
538
|
+
type PiSessionSearchSortMode = "fuzzy" | "recent";
|
|
539
|
+
interface PiSessionSearchOptions {
|
|
540
|
+
sortMode?: PiSessionSearchSortMode;
|
|
541
|
+
limit?: number;
|
|
542
|
+
}
|
|
543
|
+
type SearchToken = {
|
|
544
|
+
kind: "fuzzy" | "phrase";
|
|
545
|
+
value: string;
|
|
546
|
+
};
|
|
547
|
+
type ParsedSearchQuery = {
|
|
548
|
+
mode: "tokens";
|
|
549
|
+
tokens: SearchToken[];
|
|
550
|
+
regex: null;
|
|
551
|
+
error?: undefined;
|
|
552
|
+
} | {
|
|
553
|
+
mode: "regex";
|
|
554
|
+
tokens: [];
|
|
555
|
+
regex: RegExp | null;
|
|
556
|
+
error?: string;
|
|
557
|
+
};
|
|
558
|
+
/**
|
|
559
|
+
* Parse session-search queries with the same user-facing syntax as pi's native
|
|
560
|
+
* session picker: whitespace fuzzy tokens, quoted exact phrases, and `re:`
|
|
561
|
+
* regex mode. Kept intentionally small so boring-agent frontends can reuse the
|
|
562
|
+
* pi search behavior without importing the terminal-only session picker UI.
|
|
563
|
+
*/
|
|
564
|
+
declare function parsePiSessionSearchQuery(query: string): ParsedSearchQuery;
|
|
565
|
+
declare function matchPiSessionSearch(session: PiSessionSearchItem, parsed: ParsedSearchQuery): {
|
|
566
|
+
matches: boolean;
|
|
567
|
+
score: number;
|
|
568
|
+
};
|
|
569
|
+
declare function searchPiSessions<T extends PiSessionSearchItem>(sessions: readonly T[], query: string, options?: PiSessionSearchOptions): T[];
|
|
570
|
+
|
|
526
571
|
interface ComposerBlockerAction {
|
|
527
572
|
id: string;
|
|
528
573
|
label: string;
|
|
@@ -721,4 +766,4 @@ declare const CodeBlock: ({ code, language, showLineNumbers, className, children
|
|
|
721
766
|
|
|
722
767
|
declare function cn(...inputs: ClassValue[]): string;
|
|
723
768
|
|
|
724
|
-
export { type AgentCommandContribution, type AgentCommandOptions, ArtifactOpenProvider, ChatEmptyState, type ChatEmptyStateProps, PiChatPanel as ChatPanel, type PiChatPanelProps as ChatPanelProps, type ChatSuggestion, CodeBlock, type CommandRegistry, type ComposerBlocker, type ComposerBlockerAction, Conversation, ConversationContent, ConversationScrollButton, DebugDrawer, type GroupedToolEntry, Message, MessageAction, MessageActions, MessageContent, MessageResponse, type OpenArtifactHandler, type ParsedCommand, PiChatPanel, type PiChatPanelProps, SessionBrowser as PiSessionBrowser, type PiSessionCreateInit, SessionList as PiSessionList, PromptInput, PromptInputFooter, PromptInputSubmit, PromptInputTextarea, Reasoning, ReasoningContent, ReasoningTrigger, type SessionListProps, type SlashCommand, type SlashCommandContext, ToolCallGroup, type ToolPart, type ToolRenderer, type ToolRendererOverrides, type UploadFileOptions, type UploadFileResult, type UsePiSessionsOptions, type UsePiSessionsResult, activeSessionStorageKey, builtinCommands, clearActiveSessionId, cn, createCommandRegistry, defaultChatSuggestions, defaultToolRenderers, getAgentCommands, mergeShadcnToolRenderers, mergeToolRenderers, parseSlashCommand, readActiveSessionId, resolveToolRenderer, uploadFile, useOpenArtifact, usePiSessions, writeActiveSessionId };
|
|
769
|
+
export { type AgentCommandContribution, type AgentCommandOptions, ArtifactOpenProvider, ChatEmptyState, type ChatEmptyStateProps, PiChatPanel as ChatPanel, type PiChatPanelProps as ChatPanelProps, type ChatSuggestion, CodeBlock, type CommandRegistry, type ComposerBlocker, type ComposerBlockerAction, Conversation, ConversationContent, ConversationScrollButton, DebugDrawer, type GroupedToolEntry, Message, MessageAction, MessageActions, MessageContent, MessageResponse, type OpenArtifactHandler, type ParsedCommand, PiChatPanel, type PiChatPanelProps, SessionBrowser as PiSessionBrowser, type PiSessionCreateInit, SessionList as PiSessionList, type PiSessionSearchItem, type PiSessionSearchOptions, type PiSessionSearchSortMode, PromptInput, PromptInputFooter, PromptInputSubmit, PromptInputTextarea, Reasoning, ReasoningContent, ReasoningTrigger, type SessionListProps, type SlashCommand, type SlashCommandContext, ToolCallGroup, type ToolPart, type ToolRenderer, type ToolRendererOverrides, type UploadFileOptions, type UploadFileResult, type UsePiSessionsOptions, type UsePiSessionsResult, activeSessionStorageKey, builtinCommands, clearActiveSessionId, cn, createCommandRegistry, defaultChatSuggestions, defaultToolRenderers, getAgentCommands, matchPiSessionSearch, mergeShadcnToolRenderers, mergeToolRenderers, parsePiSessionSearchQuery, parseSlashCommand, readActiveSessionId, resolveToolRenderer, searchPiSessions, uploadFile, useOpenArtifact, usePiSessions, writeActiveSessionId };
|
package/dist/front/index.js
CHANGED
|
@@ -5663,6 +5663,190 @@ function sortByUpdatedDesc(a, b) {
|
|
|
5663
5663
|
return a.id.localeCompare(b.id);
|
|
5664
5664
|
}
|
|
5665
5665
|
|
|
5666
|
+
// ../../node_modules/.pnpm/@earendil-works+pi-tui@0.75.5/node_modules/@earendil-works/pi-tui/dist/fuzzy.js
|
|
5667
|
+
function fuzzyMatch(query, text) {
|
|
5668
|
+
const queryLower = query.toLowerCase();
|
|
5669
|
+
const textLower = text.toLowerCase();
|
|
5670
|
+
const matchQuery = (normalizedQuery) => {
|
|
5671
|
+
if (normalizedQuery.length === 0) {
|
|
5672
|
+
return { matches: true, score: 0 };
|
|
5673
|
+
}
|
|
5674
|
+
if (normalizedQuery.length > textLower.length) {
|
|
5675
|
+
return { matches: false, score: 0 };
|
|
5676
|
+
}
|
|
5677
|
+
let queryIndex = 0;
|
|
5678
|
+
let score = 0;
|
|
5679
|
+
let lastMatchIndex = -1;
|
|
5680
|
+
let consecutiveMatches = 0;
|
|
5681
|
+
for (let i = 0; i < textLower.length && queryIndex < normalizedQuery.length; i++) {
|
|
5682
|
+
if (textLower[i] === normalizedQuery[queryIndex]) {
|
|
5683
|
+
const isWordBoundary = i === 0 || /[\s\-_./:]/.test(textLower[i - 1]);
|
|
5684
|
+
if (lastMatchIndex === i - 1) {
|
|
5685
|
+
consecutiveMatches++;
|
|
5686
|
+
score -= consecutiveMatches * 5;
|
|
5687
|
+
} else {
|
|
5688
|
+
consecutiveMatches = 0;
|
|
5689
|
+
if (lastMatchIndex >= 0) {
|
|
5690
|
+
score += (i - lastMatchIndex - 1) * 2;
|
|
5691
|
+
}
|
|
5692
|
+
}
|
|
5693
|
+
if (isWordBoundary) {
|
|
5694
|
+
score -= 10;
|
|
5695
|
+
}
|
|
5696
|
+
score += i * 0.1;
|
|
5697
|
+
lastMatchIndex = i;
|
|
5698
|
+
queryIndex++;
|
|
5699
|
+
}
|
|
5700
|
+
}
|
|
5701
|
+
if (queryIndex < normalizedQuery.length) {
|
|
5702
|
+
return { matches: false, score: 0 };
|
|
5703
|
+
}
|
|
5704
|
+
if (normalizedQuery === textLower) {
|
|
5705
|
+
score -= 100;
|
|
5706
|
+
}
|
|
5707
|
+
return { matches: true, score };
|
|
5708
|
+
};
|
|
5709
|
+
const primaryMatch = matchQuery(queryLower);
|
|
5710
|
+
if (primaryMatch.matches) {
|
|
5711
|
+
return primaryMatch;
|
|
5712
|
+
}
|
|
5713
|
+
const alphaNumericMatch = queryLower.match(/^(?<letters>[a-z]+)(?<digits>[0-9]+)$/);
|
|
5714
|
+
const numericAlphaMatch = queryLower.match(/^(?<digits>[0-9]+)(?<letters>[a-z]+)$/);
|
|
5715
|
+
const swappedQuery = alphaNumericMatch ? `${alphaNumericMatch.groups?.digits ?? ""}${alphaNumericMatch.groups?.letters ?? ""}` : numericAlphaMatch ? `${numericAlphaMatch.groups?.letters ?? ""}${numericAlphaMatch.groups?.digits ?? ""}` : "";
|
|
5716
|
+
if (!swappedQuery) {
|
|
5717
|
+
return primaryMatch;
|
|
5718
|
+
}
|
|
5719
|
+
const swappedMatch = matchQuery(swappedQuery);
|
|
5720
|
+
if (!swappedMatch.matches) {
|
|
5721
|
+
return primaryMatch;
|
|
5722
|
+
}
|
|
5723
|
+
return { matches: true, score: swappedMatch.score + 5 };
|
|
5724
|
+
}
|
|
5725
|
+
|
|
5726
|
+
// src/front/chat/session/piSessionSearch.ts
|
|
5727
|
+
function normalizeWhitespaceLower(text) {
|
|
5728
|
+
return text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
5729
|
+
}
|
|
5730
|
+
function sessionModifiedMs(session) {
|
|
5731
|
+
const value = session.updatedAt;
|
|
5732
|
+
if (value instanceof Date) return value.getTime();
|
|
5733
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : 0;
|
|
5734
|
+
if (typeof value === "string") {
|
|
5735
|
+
const parsed = Date.parse(value);
|
|
5736
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
5737
|
+
}
|
|
5738
|
+
return 0;
|
|
5739
|
+
}
|
|
5740
|
+
function getSessionSearchText(session) {
|
|
5741
|
+
const name = session.name ?? session.title ?? "";
|
|
5742
|
+
return `${session.id} ${name} ${session.allMessagesText ?? ""} ${session.cwd ?? ""}`;
|
|
5743
|
+
}
|
|
5744
|
+
function parsePiSessionSearchQuery(query) {
|
|
5745
|
+
const trimmed = query.trim();
|
|
5746
|
+
if (!trimmed) return { mode: "tokens", tokens: [], regex: null };
|
|
5747
|
+
if (trimmed.startsWith("re:")) {
|
|
5748
|
+
const pattern = trimmed.slice(3).trim();
|
|
5749
|
+
if (!pattern) return { mode: "regex", tokens: [], regex: null, error: "Empty regex" };
|
|
5750
|
+
try {
|
|
5751
|
+
return { mode: "regex", tokens: [], regex: new RegExp(pattern, "i") };
|
|
5752
|
+
} catch (error) {
|
|
5753
|
+
return {
|
|
5754
|
+
mode: "regex",
|
|
5755
|
+
tokens: [],
|
|
5756
|
+
regex: null,
|
|
5757
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5758
|
+
};
|
|
5759
|
+
}
|
|
5760
|
+
}
|
|
5761
|
+
const tokens = [];
|
|
5762
|
+
let buffer = "";
|
|
5763
|
+
let inQuote = false;
|
|
5764
|
+
let hadUnclosedQuote = false;
|
|
5765
|
+
const flush = (kind) => {
|
|
5766
|
+
const value = buffer.trim();
|
|
5767
|
+
buffer = "";
|
|
5768
|
+
if (value) tokens.push({ kind, value });
|
|
5769
|
+
};
|
|
5770
|
+
for (let index = 0; index < trimmed.length; index += 1) {
|
|
5771
|
+
const char = trimmed[index];
|
|
5772
|
+
if (char === '"') {
|
|
5773
|
+
if (inQuote) {
|
|
5774
|
+
flush("phrase");
|
|
5775
|
+
inQuote = false;
|
|
5776
|
+
} else {
|
|
5777
|
+
flush("fuzzy");
|
|
5778
|
+
inQuote = true;
|
|
5779
|
+
}
|
|
5780
|
+
continue;
|
|
5781
|
+
}
|
|
5782
|
+
if (!inQuote && /\s/.test(char)) {
|
|
5783
|
+
flush("fuzzy");
|
|
5784
|
+
continue;
|
|
5785
|
+
}
|
|
5786
|
+
buffer += char;
|
|
5787
|
+
}
|
|
5788
|
+
if (inQuote) hadUnclosedQuote = true;
|
|
5789
|
+
if (hadUnclosedQuote) {
|
|
5790
|
+
return {
|
|
5791
|
+
mode: "tokens",
|
|
5792
|
+
tokens: trimmed.split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 0).map((value) => ({ kind: "fuzzy", value })),
|
|
5793
|
+
regex: null
|
|
5794
|
+
};
|
|
5795
|
+
}
|
|
5796
|
+
flush(inQuote ? "phrase" : "fuzzy");
|
|
5797
|
+
return { mode: "tokens", tokens, regex: null };
|
|
5798
|
+
}
|
|
5799
|
+
function matchPiSessionSearch(session, parsed) {
|
|
5800
|
+
const text = getSessionSearchText(session);
|
|
5801
|
+
if (parsed.mode === "regex") {
|
|
5802
|
+
if (!parsed.regex) return { matches: false, score: 0 };
|
|
5803
|
+
const index = text.search(parsed.regex);
|
|
5804
|
+
if (index < 0) return { matches: false, score: 0 };
|
|
5805
|
+
return { matches: true, score: index * 0.1 };
|
|
5806
|
+
}
|
|
5807
|
+
if (parsed.tokens.length === 0) return { matches: true, score: 0 };
|
|
5808
|
+
let totalScore = 0;
|
|
5809
|
+
let normalizedText = null;
|
|
5810
|
+
for (const token of parsed.tokens) {
|
|
5811
|
+
if (token.kind === "phrase") {
|
|
5812
|
+
normalizedText ??= normalizeWhitespaceLower(text);
|
|
5813
|
+
const phrase = normalizeWhitespaceLower(token.value);
|
|
5814
|
+
if (!phrase) continue;
|
|
5815
|
+
const index = normalizedText.indexOf(phrase);
|
|
5816
|
+
if (index < 0) return { matches: false, score: 0 };
|
|
5817
|
+
totalScore += index * 0.1;
|
|
5818
|
+
continue;
|
|
5819
|
+
}
|
|
5820
|
+
const match = fuzzyMatch(token.value, text);
|
|
5821
|
+
if (!match.matches) return { matches: false, score: 0 };
|
|
5822
|
+
totalScore += match.score;
|
|
5823
|
+
}
|
|
5824
|
+
return { matches: true, score: totalScore };
|
|
5825
|
+
}
|
|
5826
|
+
function searchPiSessions(sessions, query, options = {}) {
|
|
5827
|
+
const { sortMode = "fuzzy", limit } = options;
|
|
5828
|
+
const trimmed = query.trim();
|
|
5829
|
+
const source = [...sessions];
|
|
5830
|
+
if (!trimmed) return limit == null ? source : source.slice(0, limit);
|
|
5831
|
+
const parsed = parsePiSessionSearchQuery(trimmed);
|
|
5832
|
+
if (parsed.error) return [];
|
|
5833
|
+
if (sortMode === "recent") {
|
|
5834
|
+
const filtered = source.filter((session) => matchPiSessionSearch(session, parsed).matches);
|
|
5835
|
+
return limit == null ? filtered : filtered.slice(0, limit);
|
|
5836
|
+
}
|
|
5837
|
+
const scored = [];
|
|
5838
|
+
for (const session of source) {
|
|
5839
|
+
const result = matchPiSessionSearch(session, parsed);
|
|
5840
|
+
if (result.matches) scored.push({ session, score: result.score });
|
|
5841
|
+
}
|
|
5842
|
+
scored.sort((a, b) => {
|
|
5843
|
+
if (a.score !== b.score) return a.score - b.score;
|
|
5844
|
+
return sessionModifiedMs(b.session) - sessionModifiedMs(a.session);
|
|
5845
|
+
});
|
|
5846
|
+
const results = scored.map((result) => result.session);
|
|
5847
|
+
return limit == null ? results : results.slice(0, limit);
|
|
5848
|
+
}
|
|
5849
|
+
|
|
5666
5850
|
// src/front/chat/components/PiConversationSurface.tsx
|
|
5667
5851
|
import { Loader2 as Loader22 } from "lucide-react";
|
|
5668
5852
|
import { useCallback as useCallback14, useEffect as useEffect14, useLayoutEffect, useRef as useRef10, useState as useState15 } from "react";
|
|
@@ -10358,11 +10542,14 @@ export {
|
|
|
10358
10542
|
defaultChatSuggestions,
|
|
10359
10543
|
defaultToolRenderers,
|
|
10360
10544
|
getAgentCommands,
|
|
10545
|
+
matchPiSessionSearch,
|
|
10361
10546
|
mergeShadcnToolRenderers,
|
|
10362
10547
|
mergeToolRenderers,
|
|
10548
|
+
parsePiSessionSearchQuery,
|
|
10363
10549
|
parseSlashCommand,
|
|
10364
10550
|
readActiveSessionId,
|
|
10365
10551
|
resolveToolRenderer,
|
|
10552
|
+
searchPiSessions,
|
|
10366
10553
|
uploadFile,
|
|
10367
10554
|
useOpenArtifact,
|
|
10368
10555
|
usePiSessions,
|
package/dist/server/index.js
CHANGED
|
@@ -2619,6 +2619,37 @@ function buildFileRecordsResult(args) {
|
|
|
2619
2619
|
return result;
|
|
2620
2620
|
}
|
|
2621
2621
|
|
|
2622
|
+
// src/server/http/readonlySkillFiles.ts
|
|
2623
|
+
import { readFile as readFile4, stat as stat4 } from "fs/promises";
|
|
2624
|
+
function isReadonlySkillFilePath(path4) {
|
|
2625
|
+
if (!path4.startsWith("/")) return false;
|
|
2626
|
+
if (path4.includes("\0")) return false;
|
|
2627
|
+
if (!path4.endsWith("/SKILL.md")) return false;
|
|
2628
|
+
return path4.includes("/.pi/agent/") && path4.includes("/skills/");
|
|
2629
|
+
}
|
|
2630
|
+
async function readReadonlySkillFile(path4) {
|
|
2631
|
+
const [content, fsStat2] = await Promise.all([
|
|
2632
|
+
readFile4(path4, "utf-8"),
|
|
2633
|
+
stat4(path4)
|
|
2634
|
+
]);
|
|
2635
|
+
return {
|
|
2636
|
+
content,
|
|
2637
|
+
stat: {
|
|
2638
|
+
kind: fsStat2.isDirectory() ? "directory" : "file",
|
|
2639
|
+
size: fsStat2.size,
|
|
2640
|
+
mtimeMs: fsStat2.mtimeMs
|
|
2641
|
+
}
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2644
|
+
async function statReadonlySkillFile(path4) {
|
|
2645
|
+
const fsStat2 = await stat4(path4);
|
|
2646
|
+
return {
|
|
2647
|
+
kind: fsStat2.isDirectory() ? "directory" : "file",
|
|
2648
|
+
size: fsStat2.size,
|
|
2649
|
+
mtimeMs: fsStat2.mtimeMs
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2622
2653
|
// src/server/http/routes/file.ts
|
|
2623
2654
|
var log2 = createLogger("boring/workspace-settings");
|
|
2624
2655
|
var BORING_SETTINGS_PATH = ".boring/settings";
|
|
@@ -2828,8 +2859,8 @@ function fileRoutes(app, opts, done) {
|
|
|
2828
2859
|
error: { code: ERROR_CODE_INTERNAL, message: "workspace does not support binary reads" }
|
|
2829
2860
|
});
|
|
2830
2861
|
}
|
|
2831
|
-
const
|
|
2832
|
-
if (
|
|
2862
|
+
const stat12 = await workspace.stat(path4);
|
|
2863
|
+
if (stat12.kind !== "file") {
|
|
2833
2864
|
return reply.code(400).send({
|
|
2834
2865
|
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "path is not a file", field: "path" }
|
|
2835
2866
|
});
|
|
@@ -2844,11 +2875,11 @@ function fileRoutes(app, opts, done) {
|
|
|
2844
2875
|
try {
|
|
2845
2876
|
const parsed = parseFileRecordsRequest(request.query);
|
|
2846
2877
|
const workspace = await resolveWorkspace(request);
|
|
2847
|
-
const
|
|
2848
|
-
if (
|
|
2878
|
+
const stat12 = await workspace.stat(parsed.path);
|
|
2879
|
+
if (stat12.kind !== "file") {
|
|
2849
2880
|
return sendValidationError(reply, "path is not a file", "path");
|
|
2850
2881
|
}
|
|
2851
|
-
if (
|
|
2882
|
+
if (stat12.size > MAX_RECORD_FILE_BYTES) {
|
|
2852
2883
|
return sendValidationError(reply, "records file is too large", "path");
|
|
2853
2884
|
}
|
|
2854
2885
|
const content = await workspace.readFile(parsed.path);
|
|
@@ -2858,7 +2889,7 @@ function fileRoutes(app, opts, done) {
|
|
|
2858
2889
|
return buildFileRecordsResult({
|
|
2859
2890
|
path: parsed.path,
|
|
2860
2891
|
content,
|
|
2861
|
-
mtimeMs:
|
|
2892
|
+
mtimeMs: stat12.mtimeMs,
|
|
2862
2893
|
offset: parsed.offset,
|
|
2863
2894
|
limit: parsed.limit,
|
|
2864
2895
|
q: parsed.q
|
|
@@ -2875,14 +2906,23 @@ function fileRoutes(app, opts, done) {
|
|
|
2875
2906
|
const path4 = requireStringParam(query.path, "path", reply);
|
|
2876
2907
|
if (path4 === null) return;
|
|
2877
2908
|
try {
|
|
2909
|
+
if (isReadonlySkillFilePath(path4)) {
|
|
2910
|
+
const { content: content2, stat: stat13 } = await readReadonlySkillFile(path4);
|
|
2911
|
+
if (stat13.kind !== "file") {
|
|
2912
|
+
return reply.code(400).send({
|
|
2913
|
+
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "path is not a file", field: "path" }
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
return { content: content2, mtimeMs: stat13.mtimeMs };
|
|
2917
|
+
}
|
|
2878
2918
|
const workspace = await resolveWorkspace(request);
|
|
2879
2919
|
if (workspace.readFileWithStat) {
|
|
2880
|
-
const { content: content2, stat:
|
|
2881
|
-
return { content: content2, mtimeMs:
|
|
2920
|
+
const { content: content2, stat: stat13 } = await workspace.readFileWithStat(path4);
|
|
2921
|
+
return { content: content2, mtimeMs: stat13.kind === "file" ? stat13.mtimeMs : void 0 };
|
|
2882
2922
|
}
|
|
2883
2923
|
const content = await workspace.readFile(path4);
|
|
2884
|
-
const
|
|
2885
|
-
return { content, mtimeMs:
|
|
2924
|
+
const stat12 = await workspace.stat(path4);
|
|
2925
|
+
return { content, mtimeMs: stat12.kind === "file" ? stat12.mtimeMs : void 0 };
|
|
2886
2926
|
} catch (err) {
|
|
2887
2927
|
return classifyError(err, reply, "file");
|
|
2888
2928
|
}
|
|
@@ -2936,11 +2976,11 @@ function fileRoutes(app, opts, done) {
|
|
|
2936
2976
|
await workspace.writeFile(path4, content);
|
|
2937
2977
|
return { ok: true };
|
|
2938
2978
|
}
|
|
2939
|
-
const
|
|
2979
|
+
const stat12 = workspace.writeFileWithStat ? await workspace.writeFileWithStat(path4, content) : await (async () => {
|
|
2940
2980
|
await workspace.writeFile(path4, content);
|
|
2941
2981
|
return await workspace.stat(path4);
|
|
2942
2982
|
})();
|
|
2943
|
-
return { ok: true, mtimeMs:
|
|
2983
|
+
return { ok: true, mtimeMs: stat12.kind === "file" ? stat12.mtimeMs : void 0 };
|
|
2944
2984
|
} catch (err) {
|
|
2945
2985
|
return classifyError(err, reply, "file");
|
|
2946
2986
|
}
|
|
@@ -2969,7 +3009,7 @@ function fileRoutes(app, opts, done) {
|
|
|
2969
3009
|
}
|
|
2970
3010
|
const bytes = Buffer.from(contentBase64, "base64");
|
|
2971
3011
|
await workspace.mkdir(dir, { recursive: true });
|
|
2972
|
-
const
|
|
3012
|
+
const stat12 = workspace.writeBinaryFileWithStat ? await workspace.writeBinaryFileWithStat(path4, bytes) : await (async () => {
|
|
2973
3013
|
if (!workspace.writeBinaryFile) {
|
|
2974
3014
|
throw new Error("workspace does not support binary uploads");
|
|
2975
3015
|
}
|
|
@@ -2983,7 +3023,7 @@ function fileRoutes(app, opts, done) {
|
|
|
2983
3023
|
ok: true,
|
|
2984
3024
|
path: path4,
|
|
2985
3025
|
markdownUrl: markdownUrlFor(sourcePath, path4),
|
|
2986
|
-
mtimeMs:
|
|
3026
|
+
mtimeMs: stat12.kind === "file" ? stat12.mtimeMs : void 0
|
|
2987
3027
|
};
|
|
2988
3028
|
} catch (err) {
|
|
2989
3029
|
return classifyError(err, reply, "upload");
|
|
@@ -3069,9 +3109,12 @@ function fileRoutes(app, opts, done) {
|
|
|
3069
3109
|
const path4 = requireStringParam(query.path, "path", reply);
|
|
3070
3110
|
if (path4 === null) return;
|
|
3071
3111
|
try {
|
|
3112
|
+
if (isReadonlySkillFilePath(path4)) {
|
|
3113
|
+
return await statReadonlySkillFile(path4);
|
|
3114
|
+
}
|
|
3072
3115
|
const workspace = await resolveWorkspace(request);
|
|
3073
|
-
const
|
|
3074
|
-
return
|
|
3116
|
+
const stat12 = await workspace.stat(path4);
|
|
3117
|
+
return stat12;
|
|
3075
3118
|
} catch (err) {
|
|
3076
3119
|
return classifyError(err, reply, "path");
|
|
3077
3120
|
}
|
|
@@ -3082,7 +3125,7 @@ function fileRoutes(app, opts, done) {
|
|
|
3082
3125
|
// src/server/workspace/provisionRuntime.ts
|
|
3083
3126
|
import { createHash as createHash3 } from "crypto";
|
|
3084
3127
|
import { constants as constants2 } from "fs";
|
|
3085
|
-
import { access as access2, chmod as chmod3, cp, mkdir as mkdir4, readdir as readdir3, readFile as
|
|
3128
|
+
import { access as access2, chmod as chmod3, cp, mkdir as mkdir4, readdir as readdir3, readFile as readFile5, realpath as realpath2, stat as stat5, writeFile as writeFile4 } from "fs/promises";
|
|
3086
3129
|
import { dirname as dirname6, isAbsolute as isAbsolute4, join as join4, relative as relative5, resolve as resolve5 } from "path";
|
|
3087
3130
|
import { fileURLToPath } from "url";
|
|
3088
3131
|
import { execFile } from "child_process";
|
|
@@ -3117,7 +3160,7 @@ async function commandExists(cmd) {
|
|
|
3117
3160
|
}
|
|
3118
3161
|
}
|
|
3119
3162
|
async function hashPath(path4, hash) {
|
|
3120
|
-
const info = await
|
|
3163
|
+
const info = await stat5(path4);
|
|
3121
3164
|
if (info.isDirectory()) {
|
|
3122
3165
|
const entries = (await readdir3(path4)).filter((entry) => entry !== "__pycache__" && entry !== "build" && !entry.endsWith(".egg-info")).sort();
|
|
3123
3166
|
for (const entry of entries) {
|
|
@@ -3130,7 +3173,7 @@ async function hashPath(path4, hash) {
|
|
|
3130
3173
|
if (!info.isFile()) return;
|
|
3131
3174
|
hash.update(`file:${path4}
|
|
3132
3175
|
`);
|
|
3133
|
-
hash.update(await
|
|
3176
|
+
hash.update(await readFile5(path4));
|
|
3134
3177
|
}
|
|
3135
3178
|
async function fingerprint(contributions) {
|
|
3136
3179
|
const hash = createHash3("sha256");
|
|
@@ -3323,7 +3366,7 @@ VENV_BIN="$WORKSPACE_ROOT/.boring-agent/venv/bin"
|
|
|
3323
3366
|
for (const entry of await readdir3(venvBin)) {
|
|
3324
3367
|
if (["python", "python3", "pip", "pip3"].includes(entry)) continue;
|
|
3325
3368
|
const full = join4(venvBin, entry);
|
|
3326
|
-
const info = await
|
|
3369
|
+
const info = await stat5(full).catch(() => null);
|
|
3327
3370
|
if (!info?.isFile()) continue;
|
|
3328
3371
|
await writeExecutable(join4(shimDir, entry), `${base}TARGET="$VENV_BIN"/${bashSingleQuote(entry)}
|
|
3329
3372
|
SHEBANG="$(head -n 1 "$TARGET" 2>/dev/null || true)"
|
|
@@ -3352,7 +3395,7 @@ async function provisionRuntimeWorkspace({
|
|
|
3352
3395
|
const binDir = join4(workspaceRoot, ".boring-agent", "bin");
|
|
3353
3396
|
if (!force && await exists(markerPath)) {
|
|
3354
3397
|
try {
|
|
3355
|
-
const marker = JSON.parse(await
|
|
3398
|
+
const marker = JSON.parse(await readFile5(markerPath, "utf8"));
|
|
3356
3399
|
if (marker.fingerprint === hash && await isRuntimeMaterialized(workspaceRoot, active)) {
|
|
3357
3400
|
await writeShims(workspaceRoot, env);
|
|
3358
3401
|
return { fingerprint: hash, changed: false, env, binDir };
|
|
@@ -3410,11 +3453,11 @@ function createTimedLruCache(ttlMs, maxEntries) {
|
|
|
3410
3453
|
}
|
|
3411
3454
|
};
|
|
3412
3455
|
}
|
|
3413
|
-
function cloneStat(
|
|
3456
|
+
function cloneStat(stat12) {
|
|
3414
3457
|
return {
|
|
3415
|
-
size:
|
|
3416
|
-
mtimeMs:
|
|
3417
|
-
kind:
|
|
3458
|
+
size: stat12.size,
|
|
3459
|
+
mtimeMs: stat12.mtimeMs,
|
|
3460
|
+
kind: stat12.kind
|
|
3418
3461
|
};
|
|
3419
3462
|
}
|
|
3420
3463
|
function cloneEntries(entries) {
|
|
@@ -3622,15 +3665,15 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
|
|
|
3622
3665
|
remote.fs.readFile(sandboxPath, "utf8"),
|
|
3623
3666
|
remote.fs.stat(sandboxPath)
|
|
3624
3667
|
]);
|
|
3625
|
-
const
|
|
3668
|
+
const stat12 = {
|
|
3626
3669
|
size: fileStat.size,
|
|
3627
3670
|
mtimeMs: fileStat.mtimeMs,
|
|
3628
3671
|
kind: fileStat.isDirectory() ? "dir" : "file"
|
|
3629
3672
|
};
|
|
3630
|
-
statCache.set(sandboxPath,
|
|
3673
|
+
statCache.set(sandboxPath, stat12);
|
|
3631
3674
|
return {
|
|
3632
3675
|
content: typeof content === "string" ? content : Buffer.from(content).toString("utf-8"),
|
|
3633
|
-
stat: cloneStat(
|
|
3676
|
+
stat: cloneStat(stat12)
|
|
3634
3677
|
};
|
|
3635
3678
|
}
|
|
3636
3679
|
const version = metadataVersion;
|
|
@@ -3896,7 +3939,7 @@ function createVercelProvisioningAdapter(options) {
|
|
|
3896
3939
|
// src/server/workspace/provisioning/fingerprint.ts
|
|
3897
3940
|
import { createHash as createHash4 } from "crypto";
|
|
3898
3941
|
import { constants as constants3 } from "fs";
|
|
3899
|
-
import { access as access3, mkdir as mkdir6, open, readFile as
|
|
3942
|
+
import { access as access3, mkdir as mkdir6, open, readFile as readFile6, rename as rename5, rm as rm2 } from "fs/promises";
|
|
3900
3943
|
import { dirname as dirname8 } from "path";
|
|
3901
3944
|
var FINGERPRINT_RE = /^sha256:[a-f0-9]{64}$/;
|
|
3902
3945
|
function isValidFingerprint(value) {
|
|
@@ -4278,7 +4321,7 @@ async function ensurePythonRuntime(options) {
|
|
|
4278
4321
|
}
|
|
4279
4322
|
|
|
4280
4323
|
// src/server/workspace/provisioning/skills.ts
|
|
4281
|
-
import { stat as
|
|
4324
|
+
import { stat as stat6 } from "fs/promises";
|
|
4282
4325
|
import { join as join8 } from "path";
|
|
4283
4326
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
4284
4327
|
var GENERATED_SKILLS_REL = ".boring-agent/skills";
|
|
@@ -4309,7 +4352,7 @@ async function mirrorPluginSkills(options) {
|
|
|
4309
4352
|
}
|
|
4310
4353
|
seen.add(key);
|
|
4311
4354
|
const sourcePath = sourceToPath2(skill.source);
|
|
4312
|
-
const sourceStat = await
|
|
4355
|
+
const sourceStat = await stat6(sourcePath);
|
|
4313
4356
|
const skillTarget = `${GENERATED_SKILLS_REL}/${plugin.id}/${skill.name}`;
|
|
4314
4357
|
const target = sourceStat.isDirectory() ? skillTarget : `${skillTarget}/SKILL.md`;
|
|
4315
4358
|
await options.adapter.workspaceFs.copyFromHost(skill.source, target);
|
|
@@ -4325,7 +4368,7 @@ async function mirrorPluginSkills(options) {
|
|
|
4325
4368
|
// src/server/workspace/provisioning/workspaceFiles.ts
|
|
4326
4369
|
import { basename, join as joinPath } from "path";
|
|
4327
4370
|
import { posix as posix2 } from "path";
|
|
4328
|
-
import { readdir as readdir4, stat as
|
|
4371
|
+
import { readdir as readdir4, stat as stat7 } from "fs/promises";
|
|
4329
4372
|
import { fileURLToPath as fileURLToPath5, pathToFileURL } from "url";
|
|
4330
4373
|
function sourceToPath3(source) {
|
|
4331
4374
|
return source instanceof URL ? fileURLToPath5(source) : source;
|
|
@@ -4338,7 +4381,7 @@ function sourceForChild(rootSource, childPath) {
|
|
|
4338
4381
|
}
|
|
4339
4382
|
async function collectTemplateWorkItems(template) {
|
|
4340
4383
|
const sourcePath = sourceToPath3(template.path);
|
|
4341
|
-
const sourceStat = await
|
|
4384
|
+
const sourceStat = await stat7(sourcePath);
|
|
4342
4385
|
const targetPrefix = template.target ?? "";
|
|
4343
4386
|
if (!sourceStat.isDirectory()) {
|
|
4344
4387
|
const targetRel = toWorkspaceRel3(targetPrefix || basename(sourcePath));
|
|
@@ -4645,7 +4688,7 @@ async function copyTemplate(templatePath, workspaceRoot) {
|
|
|
4645
4688
|
|
|
4646
4689
|
// src/server/runtime/modes/provisioningAdapter.ts
|
|
4647
4690
|
import { spawn as spawn3 } from "child_process";
|
|
4648
|
-
import { cp as cp3, lstat as lstat3, mkdir as mkdir7, readFile as
|
|
4691
|
+
import { cp as cp3, lstat as lstat3, mkdir as mkdir7, readFile as readFile7, realpath as realpath3, rm as rm3, stat as stat8, writeFile as writeFile5 } from "fs/promises";
|
|
4649
4692
|
import { dirname as dirname10, isAbsolute as isAbsolute6, relative as relative8, resolve as resolve6, sep as sep5 } from "path";
|
|
4650
4693
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
4651
4694
|
var LOCAL_SANDBOX_WORKSPACE_ROOT = "/workspace";
|
|
@@ -4754,7 +4797,7 @@ function createWorkspaceFs(workspaceRoot, opts) {
|
|
|
4754
4797
|
async exists(workspaceRelativePath) {
|
|
4755
4798
|
const absPath = validatePath(workspaceRoot, workspaceRelativePath);
|
|
4756
4799
|
try {
|
|
4757
|
-
await
|
|
4800
|
+
await stat8(absPath);
|
|
4758
4801
|
return true;
|
|
4759
4802
|
} catch (error) {
|
|
4760
4803
|
if (error.code === "ENOENT") return false;
|
|
@@ -4780,12 +4823,12 @@ function createWorkspaceFs(workspaceRoot, opts) {
|
|
|
4780
4823
|
async readText(workspaceRelativePath) {
|
|
4781
4824
|
const absPath = await assertExistingInsideWorkspace(workspaceRoot, workspaceRelativePath, enforceSymlinkBoundary);
|
|
4782
4825
|
if (!absPath) return null;
|
|
4783
|
-
return await
|
|
4826
|
+
return await readFile7(absPath, "utf8");
|
|
4784
4827
|
},
|
|
4785
4828
|
async copyFromHost(hostSourcePath, workspaceRelativeTarget) {
|
|
4786
4829
|
const sourcePath = sourceToPath4(hostSourcePath);
|
|
4787
4830
|
const absTarget = await prepareWritablePath(workspaceRoot, workspaceRelativeTarget, enforceSymlinkBoundary);
|
|
4788
|
-
const sourceStat = await
|
|
4831
|
+
const sourceStat = await stat8(sourcePath);
|
|
4789
4832
|
await cp3(sourcePath, absTarget, {
|
|
4790
4833
|
recursive: sourceStat.isDirectory(),
|
|
4791
4834
|
force: false,
|
|
@@ -4915,7 +4958,7 @@ var localModeAdapter = {
|
|
|
4915
4958
|
};
|
|
4916
4959
|
|
|
4917
4960
|
// src/server/runtime/modes/vercel-sandbox.ts
|
|
4918
|
-
import { readFile as
|
|
4961
|
+
import { readFile as readFile9, readdir as readdir6, stat as stat9 } from "fs/promises";
|
|
4919
4962
|
import { join as join9 } from "path";
|
|
4920
4963
|
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
4921
4964
|
import { Sandbox as VercelSandbox } from "@vercel/sandbox";
|
|
@@ -5062,7 +5105,7 @@ function createVercelSandboxExec(sandbox, execOpts = {}) {
|
|
|
5062
5105
|
|
|
5063
5106
|
// src/server/sandbox/vercel-sandbox/packageTemplate.ts
|
|
5064
5107
|
import { createHash as createHash5 } from "crypto";
|
|
5065
|
-
import { readFile as
|
|
5108
|
+
import { readFile as readFile8, readdir as readdir5 } from "fs/promises";
|
|
5066
5109
|
import path3 from "path";
|
|
5067
5110
|
import { Readable } from "stream";
|
|
5068
5111
|
import { createGzip } from "zlib";
|
|
@@ -5083,7 +5126,7 @@ async function collectFiles(dir, base = "") {
|
|
|
5083
5126
|
if (entry.isDirectory()) {
|
|
5084
5127
|
files.push(...await collectFiles(fullPath, relPath));
|
|
5085
5128
|
} else if (entry.isFile()) {
|
|
5086
|
-
files.push({ rel: relPath, content: await
|
|
5129
|
+
files.push({ rel: relPath, content: await readFile8(fullPath) });
|
|
5087
5130
|
}
|
|
5088
5131
|
}
|
|
5089
5132
|
return files.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
@@ -5360,7 +5403,7 @@ function shellSingleQuote(value) {
|
|
|
5360
5403
|
}
|
|
5361
5404
|
async function copyHostPathToVercelWorkspace(options) {
|
|
5362
5405
|
const sourcePath = provisioningSourceToPath(options.source);
|
|
5363
|
-
const sourceStat = await
|
|
5406
|
+
const sourceStat = await stat9(sourcePath);
|
|
5364
5407
|
if (sourceStat.isDirectory()) {
|
|
5365
5408
|
await options.workspace.mkdir(options.targetRel, { recursive: true });
|
|
5366
5409
|
for (const entry of await readdir6(sourcePath, { withFileTypes: true })) {
|
|
@@ -5373,7 +5416,7 @@ async function copyHostPathToVercelWorkspace(options) {
|
|
|
5373
5416
|
return;
|
|
5374
5417
|
}
|
|
5375
5418
|
if (!sourceStat.isFile()) return;
|
|
5376
|
-
const bytes = new Uint8Array(await
|
|
5419
|
+
const bytes = new Uint8Array(await readFile9(sourcePath));
|
|
5377
5420
|
if (options.workspace.writeBinaryFile) await options.workspace.writeBinaryFile(options.targetRel, bytes);
|
|
5378
5421
|
else await options.workspace.writeFile(options.targetRel, Buffer.from(bytes).toString("utf8"));
|
|
5379
5422
|
}
|
|
@@ -6159,7 +6202,7 @@ function createPiAgentSessionAdapter(session, options = {}) {
|
|
|
6159
6202
|
import { randomUUID } from "crypto";
|
|
6160
6203
|
import {
|
|
6161
6204
|
readdir as readdir7,
|
|
6162
|
-
readFile as
|
|
6205
|
+
readFile as readFile10,
|
|
6163
6206
|
stat as fsStat,
|
|
6164
6207
|
rm as rm4,
|
|
6165
6208
|
mkdir as mkdir11,
|
|
@@ -6322,7 +6365,7 @@ var PiSessionStore = class {
|
|
|
6322
6365
|
const filepath = await this.resolveSessionFile(sessionId);
|
|
6323
6366
|
let content;
|
|
6324
6367
|
try {
|
|
6325
|
-
content = await
|
|
6368
|
+
content = await readFile10(filepath, "utf-8");
|
|
6326
6369
|
} catch {
|
|
6327
6370
|
throw new Error(`Session not found: ${sessionId}`);
|
|
6328
6371
|
}
|
|
@@ -6403,7 +6446,7 @@ var PiSessionStore = class {
|
|
|
6403
6446
|
let filepath = direct;
|
|
6404
6447
|
let content;
|
|
6405
6448
|
try {
|
|
6406
|
-
content = await
|
|
6449
|
+
content = await readFile10(direct, "utf-8");
|
|
6407
6450
|
} catch {
|
|
6408
6451
|
const files = await readdir7(this.sessionDir).catch(() => []);
|
|
6409
6452
|
const match = files.find(
|
|
@@ -6411,7 +6454,7 @@ var PiSessionStore = class {
|
|
|
6411
6454
|
);
|
|
6412
6455
|
if (!match) return null;
|
|
6413
6456
|
filepath = join10(this.sessionDir, match);
|
|
6414
|
-
content = await
|
|
6457
|
+
content = await readFile10(filepath, "utf-8");
|
|
6415
6458
|
}
|
|
6416
6459
|
const entries = safeParseEntries(content);
|
|
6417
6460
|
const linkedPiFile = extractPiSessionFilePath(entries);
|
|
@@ -6488,7 +6531,7 @@ var PiSessionStore = class {
|
|
|
6488
6531
|
}
|
|
6489
6532
|
async linkedPiFileFor(filepath) {
|
|
6490
6533
|
try {
|
|
6491
|
-
const content = await
|
|
6534
|
+
const content = await readFile10(filepath, "utf-8");
|
|
6492
6535
|
return extractPiSessionFilePath(safeParseEntries(content));
|
|
6493
6536
|
} catch {
|
|
6494
6537
|
return null;
|
|
@@ -6496,9 +6539,9 @@ var PiSessionStore = class {
|
|
|
6496
6539
|
}
|
|
6497
6540
|
async referencedPiFiles(files) {
|
|
6498
6541
|
const referenced = /* @__PURE__ */ new Set();
|
|
6499
|
-
await Promise.all(files.map(async ({ filepath, stat:
|
|
6542
|
+
await Promise.all(files.map(async ({ filepath, stat: stat12 }) => {
|
|
6500
6543
|
try {
|
|
6501
|
-
const piFilePath = (await this.readPrefixCache(filepath,
|
|
6544
|
+
const piFilePath = (await this.readPrefixCache(filepath, stat12)).referencedPiFile;
|
|
6502
6545
|
if (piFilePath && resolve7(piFilePath) !== resolve7(filepath)) {
|
|
6503
6546
|
referenced.add(resolve7(piFilePath));
|
|
6504
6547
|
}
|
|
@@ -6507,10 +6550,10 @@ var PiSessionStore = class {
|
|
|
6507
6550
|
}));
|
|
6508
6551
|
return referenced;
|
|
6509
6552
|
}
|
|
6510
|
-
async sessionSortMtimeMs({ filepath, stat:
|
|
6511
|
-
let sortMtimeMs =
|
|
6553
|
+
async sessionSortMtimeMs({ filepath, stat: stat12 }) {
|
|
6554
|
+
let sortMtimeMs = stat12.mtime.getTime();
|
|
6512
6555
|
try {
|
|
6513
|
-
const linkedPiFile = (await this.readPrefixCache(filepath,
|
|
6556
|
+
const linkedPiFile = (await this.readPrefixCache(filepath, stat12)).referencedPiFile;
|
|
6514
6557
|
if (linkedPiFile && resolve7(linkedPiFile) !== resolve7(filepath)) {
|
|
6515
6558
|
const linkedStat = await fsStat(linkedPiFile);
|
|
6516
6559
|
sortMtimeMs = Math.max(sortMtimeMs, linkedStat.mtime.getTime());
|
|
@@ -6604,7 +6647,7 @@ var PiSessionStore = class {
|
|
|
6604
6647
|
const batch = visibleFiles.slice(index, index + batchSize);
|
|
6605
6648
|
index += batch.length;
|
|
6606
6649
|
const summaries = await Promise.all(
|
|
6607
|
-
batch.map(({ filepath, stat:
|
|
6650
|
+
batch.map(({ filepath, stat: stat12 }) => this.summarizeFile(filepath, stat12))
|
|
6608
6651
|
);
|
|
6609
6652
|
for (const summary of summaries) {
|
|
6610
6653
|
if (!summary) continue;
|
|
@@ -6706,7 +6749,7 @@ var PiSessionStore = class {
|
|
|
6706
6749
|
try {
|
|
6707
6750
|
const [fileStat, content] = await Promise.all([
|
|
6708
6751
|
fsStat(filepath),
|
|
6709
|
-
|
|
6752
|
+
readFile10(filepath, "utf-8")
|
|
6710
6753
|
]);
|
|
6711
6754
|
return { entries: safeParseEntries(content), mtime: fileStat.mtime, size: Number(fileStat.size) };
|
|
6712
6755
|
} catch {
|
|
@@ -7449,7 +7492,7 @@ ${entry.message}`;
|
|
|
7449
7492
|
}
|
|
7450
7493
|
|
|
7451
7494
|
// src/server/harness/pi-coding-agent/pluginLoader.ts
|
|
7452
|
-
import { readdir as readdir8, stat as
|
|
7495
|
+
import { readdir as readdir8, stat as stat10, readFile as readFile11 } from "fs/promises";
|
|
7453
7496
|
import { join as join12, extname as extname4, resolve as resolve8, sep as sep6, relative as relative9 } from "path";
|
|
7454
7497
|
import { homedir as homedir4 } from "os";
|
|
7455
7498
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
@@ -7459,7 +7502,7 @@ var LOCAL_DIR = ".pi/extensions";
|
|
|
7459
7502
|
var EXTENSIONS_JSON = ".pi/extensions.json";
|
|
7460
7503
|
async function dirExists2(path4) {
|
|
7461
7504
|
try {
|
|
7462
|
-
const s = await
|
|
7505
|
+
const s = await stat10(path4);
|
|
7463
7506
|
return s.isDirectory();
|
|
7464
7507
|
} catch {
|
|
7465
7508
|
return false;
|
|
@@ -7467,7 +7510,7 @@ async function dirExists2(path4) {
|
|
|
7467
7510
|
}
|
|
7468
7511
|
async function fileExists(path4) {
|
|
7469
7512
|
try {
|
|
7470
|
-
const s = await
|
|
7513
|
+
const s = await stat10(path4);
|
|
7471
7514
|
return s.isFile();
|
|
7472
7515
|
} catch {
|
|
7473
7516
|
return false;
|
|
@@ -7518,7 +7561,7 @@ async function discoverNpmPlugins(cwd) {
|
|
|
7518
7561
|
async function loadNpmPlugin(pkgDir, importFn) {
|
|
7519
7562
|
const pkgJsonPath = join12(pkgDir, "package.json");
|
|
7520
7563
|
if (!await fileExists(pkgJsonPath)) return [];
|
|
7521
|
-
const pkgJson = JSON.parse(await
|
|
7564
|
+
const pkgJson = JSON.parse(await readFile11(pkgJsonPath, "utf-8"));
|
|
7522
7565
|
const main = pkgJson.main ?? "index.js";
|
|
7523
7566
|
const resolvedMain = resolve8(pkgDir, main);
|
|
7524
7567
|
const resolvedPkgDir = resolve8(pkgDir);
|
|
@@ -7535,7 +7578,7 @@ async function loadExtensionsJson(cwd) {
|
|
|
7535
7578
|
const configPath = join12(cwd, EXTENSIONS_JSON);
|
|
7536
7579
|
if (!await fileExists(configPath)) return null;
|
|
7537
7580
|
try {
|
|
7538
|
-
const raw = await
|
|
7581
|
+
const raw = await readFile11(configPath, "utf-8");
|
|
7539
7582
|
return JSON.parse(raw);
|
|
7540
7583
|
} catch {
|
|
7541
7584
|
return null;
|
|
@@ -7609,7 +7652,7 @@ import {
|
|
|
7609
7652
|
|
|
7610
7653
|
// src/server/tools/operations/bound.ts
|
|
7611
7654
|
import { constants as constants4 } from "fs";
|
|
7612
|
-
import { access as access4, lstat as lstat4, mkdir as mkdir12, readFile as
|
|
7655
|
+
import { access as access4, lstat as lstat4, mkdir as mkdir12, readFile as readFile12, readdir as readdir9, readlink, realpath as realpath4, stat as stat11, writeFile as writeFile7 } from "fs/promises";
|
|
7613
7656
|
import { dirname as dirname11, isAbsolute as isAbsolute7, join as join13, relative as relative10, resolve as resolve9 } from "path";
|
|
7614
7657
|
function toPosixPath(value) {
|
|
7615
7658
|
return value.split("\\").join("/");
|
|
@@ -7681,7 +7724,7 @@ async function findNearestExistingAncestor(absPath) {
|
|
|
7681
7724
|
let current = absPath;
|
|
7682
7725
|
for (; ; ) {
|
|
7683
7726
|
try {
|
|
7684
|
-
await
|
|
7727
|
+
await stat11(current);
|
|
7685
7728
|
return current;
|
|
7686
7729
|
} catch {
|
|
7687
7730
|
const parent = dirname11(current);
|
|
@@ -7756,7 +7799,7 @@ function boundFs(workspaceRoot, opts = {}) {
|
|
|
7756
7799
|
async readFile(absolutePath) {
|
|
7757
7800
|
const storagePath = toStoragePath(absolutePath);
|
|
7758
7801
|
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7759
|
-
return await
|
|
7802
|
+
return await readFile12(storagePath);
|
|
7760
7803
|
},
|
|
7761
7804
|
async access(absolutePath) {
|
|
7762
7805
|
const storagePath = toStoragePath(absolutePath);
|
|
@@ -7781,7 +7824,7 @@ function boundFs(workspaceRoot, opts = {}) {
|
|
|
7781
7824
|
async readFile(absolutePath) {
|
|
7782
7825
|
const storagePath = toStoragePath(absolutePath);
|
|
7783
7826
|
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7784
|
-
return await
|
|
7827
|
+
return await readFile12(storagePath);
|
|
7785
7828
|
},
|
|
7786
7829
|
async writeFile(absolutePath, content) {
|
|
7787
7830
|
const storagePath = toStoragePath(absolutePath);
|
|
@@ -7799,7 +7842,7 @@ function boundFs(workspaceRoot, opts = {}) {
|
|
|
7799
7842
|
const storagePath = toStoragePath(absolutePath);
|
|
7800
7843
|
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7801
7844
|
try {
|
|
7802
|
-
await
|
|
7845
|
+
await stat11(storagePath);
|
|
7803
7846
|
return true;
|
|
7804
7847
|
} catch (err) {
|
|
7805
7848
|
if (err.code === "ENOENT") return false;
|
|
@@ -7818,12 +7861,12 @@ function boundFs(workspaceRoot, opts = {}) {
|
|
|
7818
7861
|
async isDirectory(absolutePath) {
|
|
7819
7862
|
const storagePath = toStoragePath(absolutePath);
|
|
7820
7863
|
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7821
|
-
return (await
|
|
7864
|
+
return (await stat11(storagePath)).isDirectory();
|
|
7822
7865
|
},
|
|
7823
7866
|
async readFile(absolutePath) {
|
|
7824
7867
|
const storagePath = toStoragePath(absolutePath);
|
|
7825
7868
|
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7826
|
-
return await
|
|
7869
|
+
return await readFile12(storagePath, "utf8");
|
|
7827
7870
|
}
|
|
7828
7871
|
};
|
|
7829
7872
|
const ls = {
|
|
@@ -7831,7 +7874,7 @@ function boundFs(workspaceRoot, opts = {}) {
|
|
|
7831
7874
|
const storagePath = toStoragePath(absolutePath);
|
|
7832
7875
|
try {
|
|
7833
7876
|
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7834
|
-
await
|
|
7877
|
+
await stat11(storagePath);
|
|
7835
7878
|
return true;
|
|
7836
7879
|
} catch {
|
|
7837
7880
|
return false;
|
|
@@ -7840,7 +7883,7 @@ function boundFs(workspaceRoot, opts = {}) {
|
|
|
7840
7883
|
async stat(absolutePath) {
|
|
7841
7884
|
const storagePath = toStoragePath(absolutePath);
|
|
7842
7885
|
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7843
|
-
return await
|
|
7886
|
+
return await stat11(storagePath);
|
|
7844
7887
|
},
|
|
7845
7888
|
async readdir(absolutePath) {
|
|
7846
7889
|
const storagePath = toStoragePath(absolutePath);
|
|
@@ -8023,8 +8066,8 @@ function remoteWorkspaceLsOps(workspace, opts = {}) {
|
|
|
8023
8066
|
},
|
|
8024
8067
|
async stat(absolutePath) {
|
|
8025
8068
|
const rel = toRelPath(workspace, absolutePath, opts);
|
|
8026
|
-
const
|
|
8027
|
-
return { isDirectory: () =>
|
|
8069
|
+
const stat12 = await workspace.stat(rel);
|
|
8070
|
+
return { isDirectory: () => stat12.kind === "dir" };
|
|
8028
8071
|
},
|
|
8029
8072
|
async readdir(absolutePath) {
|
|
8030
8073
|
const rel = toRelPath(workspace, absolutePath, opts);
|
|
@@ -8467,6 +8510,10 @@ function bwrapSpawnHook(workspaceRoot, runtime, sandboxRoot = "/workspace") {
|
|
|
8467
8510
|
const bwrapPrefix = ["bwrap", ...args].map(shellEscape2).join(" ");
|
|
8468
8511
|
return (context) => ({
|
|
8469
8512
|
...context,
|
|
8513
|
+
// The inner command runs at sandboxRoot inside bwrap, but the host-side
|
|
8514
|
+
// process must spawn from a real host path. GitHub runners do not have a
|
|
8515
|
+
// /workspace directory, so keep the outer cwd on the mounted storage root.
|
|
8516
|
+
cwd: workspaceRoot,
|
|
8470
8517
|
command: `${bwrapPrefix} bash -lc ${shellEscape2(context.command)}`,
|
|
8471
8518
|
env: withWorkspacePythonEnv({
|
|
8472
8519
|
workspaceRoot,
|
|
@@ -9092,49 +9139,53 @@ import {
|
|
|
9092
9139
|
var CACHE_TTL_MS2 = 3e4;
|
|
9093
9140
|
function skillsRoutes(app, opts, done) {
|
|
9094
9141
|
const cached = /* @__PURE__ */ new Map();
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
agentDir,
|
|
9116
|
-
piPackages ?? []
|
|
9117
|
-
);
|
|
9118
|
-
const packageManager = new DefaultPackageManager({
|
|
9119
|
-
cwd: workspaceRoot,
|
|
9120
|
-
agentDir,
|
|
9121
|
-
settingsManager
|
|
9122
|
-
});
|
|
9123
|
-
const resolved = await packageManager.resolve();
|
|
9124
|
-
return resolved.skills.filter((resource) => resource.enabled).map((resource) => resource.path);
|
|
9125
|
-
})();
|
|
9126
|
-
const result = loadSkills2({
|
|
9142
|
+
async function resolveSkillsForRequest(request, refresh = false) {
|
|
9143
|
+
const workspaceRoot = opts.getWorkspaceRoot ? await opts.getWorkspaceRoot(request) : opts.workspaceRoot;
|
|
9144
|
+
const additionalSkillPaths = opts.getAdditionalSkillPaths ? await opts.getAdditionalSkillPaths(request) : opts.additionalSkillPaths;
|
|
9145
|
+
const piPackages = opts.getPiPackages ? await opts.getPiPackages(request) : opts.piPackages;
|
|
9146
|
+
const noSkills = (opts.getNoSkills ? await opts.getNoSkills(request) : opts.noSkills) ?? withPiHarnessDefaults().noSkills;
|
|
9147
|
+
const cacheKey = JSON.stringify([workspaceRoot, additionalSkillPaths ?? [], piPackages ?? [], noSkills]);
|
|
9148
|
+
const now = Date.now();
|
|
9149
|
+
for (const [key, entry2] of cached) {
|
|
9150
|
+
if (entry2.expiresAt <= now) cached.delete(key);
|
|
9151
|
+
}
|
|
9152
|
+
const cachedEntry = cached.get(cacheKey);
|
|
9153
|
+
if (!refresh && cachedEntry && cachedEntry.expiresAt > now) return cachedEntry;
|
|
9154
|
+
const agentDir = getAgentDir3();
|
|
9155
|
+
const packageSkillPaths = noSkills ? [] : await (async () => {
|
|
9156
|
+
const settingsManager = createResourceSettingsManager(
|
|
9157
|
+
workspaceRoot,
|
|
9158
|
+
agentDir,
|
|
9159
|
+
piPackages ?? []
|
|
9160
|
+
);
|
|
9161
|
+
const packageManager = new DefaultPackageManager({
|
|
9127
9162
|
cwd: workspaceRoot,
|
|
9128
9163
|
agentDir,
|
|
9129
|
-
|
|
9130
|
-
includeDefaults: !noSkills
|
|
9164
|
+
settingsManager
|
|
9131
9165
|
});
|
|
9132
|
-
const
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9166
|
+
const resolved = await packageManager.resolve();
|
|
9167
|
+
return resolved.skills.filter((resource) => resource.enabled).map((resource) => resource.path);
|
|
9168
|
+
})();
|
|
9169
|
+
const result = loadSkills2({
|
|
9170
|
+
cwd: workspaceRoot,
|
|
9171
|
+
agentDir,
|
|
9172
|
+
skillPaths: [...packageSkillPaths, ...additionalSkillPaths ?? []],
|
|
9173
|
+
includeDefaults: !noSkills
|
|
9174
|
+
});
|
|
9175
|
+
const skills = result.skills.map((s) => ({
|
|
9176
|
+
name: String(s.name),
|
|
9177
|
+
description: String(s.description ?? ""),
|
|
9178
|
+
...typeof s.filePath === "string" ? { filePath: s.filePath } : {},
|
|
9179
|
+
...typeof s.sourceInfo?.scope === "string" ? { source: s.sourceInfo.scope } : {}
|
|
9180
|
+
}));
|
|
9181
|
+
const entry = { skills, expiresAt: now + CACHE_TTL_MS2 };
|
|
9182
|
+
cached.set(cacheKey, entry);
|
|
9183
|
+
return entry;
|
|
9184
|
+
}
|
|
9185
|
+
app.get("/api/v1/agent/skills", async (request, reply) => {
|
|
9186
|
+
try {
|
|
9187
|
+
const entry = await resolveSkillsForRequest(request, request.query.refresh === "1");
|
|
9188
|
+
return reply.code(200).send({ skills: entry.skills });
|
|
9138
9189
|
} catch (error) {
|
|
9139
9190
|
const message = error instanceof Error ? error.message : String(error);
|
|
9140
9191
|
request.log.warn({ err: error }, "[agent] failed to load skills");
|
|
@@ -12075,8 +12126,8 @@ async function promptImagesFromAttachments(attachments, workspace) {
|
|
|
12075
12126
|
}
|
|
12076
12127
|
if (!workspace?.readBinaryFile || !isWorkspaceImageAttachment(attachment)) continue;
|
|
12077
12128
|
try {
|
|
12078
|
-
const
|
|
12079
|
-
if (
|
|
12129
|
+
const stat12 = await workspace.stat(attachment.path);
|
|
12130
|
+
if (stat12.kind !== "file" || stat12.size <= 0 || stat12.size > MAX_PROMPT_IMAGE_BYTES) continue;
|
|
12080
12131
|
const bytes = await workspace.readBinaryFile(attachment.path);
|
|
12081
12132
|
if (bytes.byteLength <= 0 || bytes.byteLength > MAX_PROMPT_IMAGE_BYTES) continue;
|
|
12082
12133
|
const detectedMimeType = detectPromptImageMimeType(bytes);
|
|
@@ -12376,7 +12427,7 @@ function mergeTools(options) {
|
|
|
12376
12427
|
}
|
|
12377
12428
|
|
|
12378
12429
|
// src/server/tools/upload/index.ts
|
|
12379
|
-
import { readFile as
|
|
12430
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
12380
12431
|
import { extname as extname5, join as join14 } from "path";
|
|
12381
12432
|
var DEFAULT_UPLOAD_DIR = "assets/images";
|
|
12382
12433
|
var MAX_UPLOAD_BYTES2 = 10 * 1024 * 1024;
|
|
@@ -12445,7 +12496,7 @@ function buildUploadAgentTools(bundle) {
|
|
|
12445
12496
|
const rawDir = typeof input.directory === "string" ? input.directory.trim() : "";
|
|
12446
12497
|
const dir = rawDir ? rawDir.replace(/^\.\/+/, "").replace(/\/+$/, "") : DEFAULT_UPLOAD_DIR;
|
|
12447
12498
|
try {
|
|
12448
|
-
const bytes = workspace.readBinaryFile ? Buffer.from(await workspace.readBinaryFile(filePath)) : await
|
|
12499
|
+
const bytes = workspace.readBinaryFile ? Buffer.from(await workspace.readBinaryFile(filePath)) : await readFile13(join14(getRuntimeBundleStorageRoot(bundle), filePath));
|
|
12449
12500
|
if (bytes.byteLength === 0 || bytes.byteLength > MAX_UPLOAD_BYTES2) {
|
|
12450
12501
|
return {
|
|
12451
12502
|
content: [{ type: "text", text: `file must be between 1 byte and ${MAX_UPLOAD_BYTES2} bytes` }],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.61",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Pane-embeddable coding agent. Ships direct/local/vercel-sandbox execution modes behind one interface.",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"use-stick-to-bottom": "^1.1.3",
|
|
75
75
|
"yaml": "^2.8.3",
|
|
76
76
|
"zod": "^3.25.76",
|
|
77
|
-
"@hachej/boring-ui-kit": "0.1.
|
|
77
|
+
"@hachej/boring-ui-kit": "0.1.61"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@antithesishq/bombadil": "0.5.0",
|