@kortix/sandbox 0.4.1
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/config/customize.sh +143 -0
- package/config/kortix-env-setup.sh +25 -0
- package/kortix-master/package.json +22 -0
- package/kortix-master/src/config.ts +22 -0
- package/kortix-master/src/index.ts +44 -0
- package/kortix-master/src/routes/env.ts +65 -0
- package/kortix-master/src/routes/proxy.ts +108 -0
- package/kortix-master/src/routes/update.ts +185 -0
- package/kortix-master/src/services/proxy.ts +43 -0
- package/kortix-master/src/services/secret-store.ts +156 -0
- package/kortix-master/tsconfig.json +14 -0
- package/opencode/agents/kortix-browser.md +142 -0
- package/opencode/agents/kortix-build.md +62 -0
- package/opencode/agents/kortix-explore.md +66 -0
- package/opencode/agents/kortix-image-gen.md +33 -0
- package/opencode/agents/kortix-main.md +450 -0
- package/opencode/agents/kortix-plan.md +100 -0
- package/opencode/agents/kortix-research.md +84 -0
- package/opencode/agents/kortix-sheets.md +61 -0
- package/opencode/agents/kortix-slides.md +64 -0
- package/opencode/agents/kortix-web-dev.md +572 -0
- package/opencode/commands/email.md +36 -0
- package/opencode/commands/init.md +43 -0
- package/opencode/commands/journal.md +44 -0
- package/opencode/commands/memory-init.md +81 -0
- package/opencode/commands/memory-search.md +50 -0
- package/opencode/commands/memory-status.md +56 -0
- package/opencode/commands/research.md +36 -0
- package/opencode/commands/search.md +38 -0
- package/opencode/commands/slides.md +32 -0
- package/opencode/commands/spreadsheet.md +30 -0
- package/opencode/memory.json +37 -0
- package/opencode/ocx.jsonc +10 -0
- package/opencode/opencode.jsonc +103 -0
- package/opencode/package.json +25 -0
- package/opencode/patches/apply.sh +19 -0
- package/opencode/patches/opencode-pty-spawn.txt +49 -0
- package/opencode/plugin/background-agents.ts.disabled +483 -0
- package/opencode/plugin/kdco-primitives/get-project-id.ts +172 -0
- package/opencode/plugin/kdco-primitives/index.ts +26 -0
- package/opencode/plugin/kdco-primitives/log-warn.ts +51 -0
- package/opencode/plugin/kdco-primitives/mutex.ts +122 -0
- package/opencode/plugin/kdco-primitives/shell.ts +138 -0
- package/opencode/plugin/kdco-primitives/temp.ts +36 -0
- package/opencode/plugin/kdco-primitives/terminal-detect.ts +34 -0
- package/opencode/plugin/kdco-primitives/types.ts +13 -0
- package/opencode/plugin/kdco-primitives/with-timeout.ts +84 -0
- package/opencode/plugin/memory.ts +306 -0
- package/opencode/plugin/worktree/state.ts +412 -0
- package/opencode/plugin/worktree/terminal.ts +1002 -0
- package/opencode/plugin/worktree.ts +861 -0
- package/opencode/skills/KORTIX-browser/SKILL.md +478 -0
- package/opencode/skills/KORTIX-cron-triggers/SKILL.md +173 -0
- package/opencode/skills/KORTIX-deep-research/SKILL.md +278 -0
- package/opencode/skills/KORTIX-docx/SKILL.md +398 -0
- package/opencode/skills/KORTIX-docx/scripts/__init__.py +1 -0
- package/opencode/skills/KORTIX-docx/scripts/accept_changes.py +104 -0
- package/opencode/skills/KORTIX-docx/scripts/comment.py +244 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/__init__.py +0 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/merge_runs.py +199 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/opencode/skills/KORTIX-docx/scripts/office/pack.py +159 -0
- package/opencode/skills/KORTIX-docx/scripts/office/soffice.py +183 -0
- package/opencode/skills/KORTIX-docx/scripts/office/unpack.py +132 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validate.py +111 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/__init__.py +15 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/base.py +847 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/docx.py +446 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/pptx.py +275 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/redlining.py +247 -0
- package/opencode/skills/KORTIX-docx/scripts/render_docx.py +179 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/comments.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtended.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtensible.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsIds.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/people.xml +3 -0
- package/opencode/skills/KORTIX-domain-research/SKILL.md +96 -0
- package/opencode/skills/KORTIX-domain-research/scripts/domain-lookup.py +810 -0
- package/opencode/skills/KORTIX-elevenlabs/SKILL.md +230 -0
- package/opencode/skills/KORTIX-elevenlabs/scripts/tts.py +389 -0
- package/opencode/skills/KORTIX-email/SKILL.md +145 -0
- package/opencode/skills/KORTIX-legal-writer/SKILL.md +409 -0
- package/opencode/skills/KORTIX-legal-writer/references/bluebook.md +152 -0
- package/opencode/skills/KORTIX-legal-writer/references/document-types.md +416 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/courtlistener.py +291 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/ecfr_lookup.py +299 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/verify-legal.py +507 -0
- package/opencode/skills/KORTIX-logo-creator/SKILL.md +293 -0
- package/opencode/skills/KORTIX-logo-creator/references/prompt-patterns.md +134 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/compose_logo.py +406 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/create_logo_sheet.py +258 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/remove_bg.py +96 -0
- package/opencode/skills/KORTIX-memory/SKILL.md +261 -0
- package/opencode/skills/KORTIX-memory/scripts/export-sessions.py +409 -0
- package/opencode/skills/KORTIX-paper-creator/SKILL.md +549 -0
- package/opencode/skills/KORTIX-paper-creator/assets/template.tex +101 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/compile.sh +177 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/openalex_to_bibtex.py +220 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/verify.sh +354 -0
- package/opencode/skills/KORTIX-paper-search/SKILL.md +418 -0
- package/opencode/skills/KORTIX-pdf/SKILL.md +232 -0
- package/opencode/skills/KORTIX-pdf/forms.md +36 -0
- package/opencode/skills/KORTIX-pdf/reference.md +105 -0
- package/opencode/skills/KORTIX-pdf/scripts/check_bounding_boxes.py +65 -0
- package/opencode/skills/KORTIX-pdf/scripts/check_fillable_fields.py +11 -0
- package/opencode/skills/KORTIX-pdf/scripts/convert_pdf_to_images.py +33 -0
- package/opencode/skills/KORTIX-pdf/scripts/create_validation_image.py +37 -0
- package/opencode/skills/KORTIX-pdf/scripts/extract_form_field_info.py +122 -0
- package/opencode/skills/KORTIX-pdf/scripts/extract_form_structure.py +115 -0
- package/opencode/skills/KORTIX-pdf/scripts/fill_fillable_fields.py +98 -0
- package/opencode/skills/KORTIX-pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
- package/opencode/skills/KORTIX-plan/SKILL.md +228 -0
- package/opencode/skills/KORTIX-presentation-viewer/SKILL.md +87 -0
- package/opencode/skills/KORTIX-presentation-viewer/serve.ts +136 -0
- package/opencode/skills/KORTIX-presentation-viewer/viewer.html +559 -0
- package/opencode/skills/KORTIX-presentations/SKILL.md +344 -0
- package/opencode/skills/KORTIX-remotion/SKILL.md +56 -0
- package/opencode/skills/KORTIX-remotion/rules/3d.md +86 -0
- package/opencode/skills/KORTIX-remotion/rules/animations.md +29 -0
- package/opencode/skills/KORTIX-remotion/rules/assets.md +78 -0
- package/opencode/skills/KORTIX-remotion/rules/audio-visualization.md +198 -0
- package/opencode/skills/KORTIX-remotion/rules/audio.md +169 -0
- package/opencode/skills/KORTIX-remotion/rules/calculate-metadata.md +104 -0
- package/opencode/skills/KORTIX-remotion/rules/can-decode.md +75 -0
- package/opencode/skills/KORTIX-remotion/rules/charts.md +120 -0
- package/opencode/skills/KORTIX-remotion/rules/compositions.md +141 -0
- package/opencode/skills/KORTIX-remotion/rules/display-captions.md +184 -0
- package/opencode/skills/KORTIX-remotion/rules/extract-frames.md +229 -0
- package/opencode/skills/KORTIX-remotion/rules/ffmpeg.md +38 -0
- package/opencode/skills/KORTIX-remotion/rules/fonts.md +152 -0
- package/opencode/skills/KORTIX-remotion/rules/get-audio-duration.md +58 -0
- package/opencode/skills/KORTIX-remotion/rules/get-video-dimensions.md +68 -0
- package/opencode/skills/KORTIX-remotion/rules/get-video-duration.md +58 -0
- package/opencode/skills/KORTIX-remotion/rules/gifs.md +141 -0
- package/opencode/skills/KORTIX-remotion/rules/images.md +130 -0
- package/opencode/skills/KORTIX-remotion/rules/import-srt-captions.md +69 -0
- package/opencode/skills/KORTIX-remotion/rules/light-leaks.md +73 -0
- package/opencode/skills/KORTIX-remotion/rules/lottie.md +68 -0
- package/opencode/skills/KORTIX-remotion/rules/maps.md +401 -0
- package/opencode/skills/KORTIX-remotion/rules/measuring-dom-nodes.md +35 -0
- package/opencode/skills/KORTIX-remotion/rules/measuring-text.md +143 -0
- package/opencode/skills/KORTIX-remotion/rules/parameters.md +98 -0
- package/opencode/skills/KORTIX-remotion/rules/sequencing.md +118 -0
- package/opencode/skills/KORTIX-remotion/rules/subtitles.md +36 -0
- package/opencode/skills/KORTIX-remotion/rules/tailwind.md +11 -0
- package/opencode/skills/KORTIX-remotion/rules/text-animations.md +20 -0
- package/opencode/skills/KORTIX-remotion/rules/timing.md +179 -0
- package/opencode/skills/KORTIX-remotion/rules/transcribe-captions.md +70 -0
- package/opencode/skills/KORTIX-remotion/rules/transitions.md +197 -0
- package/opencode/skills/KORTIX-remotion/rules/transparent-videos.md +106 -0
- package/opencode/skills/KORTIX-remotion/rules/trimming.md +53 -0
- package/opencode/skills/KORTIX-remotion/rules/videos.md +171 -0
- package/opencode/skills/KORTIX-secrets/SKILL.md +280 -0
- package/opencode/skills/KORTIX-semantic-search/SKILL.md +213 -0
- package/opencode/skills/KORTIX-session-search/SKILL.md +807 -0
- package/opencode/skills/KORTIX-session-search/Untitled +1 -0
- package/opencode/skills/KORTIX-skill-creator/SKILL.md +163 -0
- package/opencode/skills/KORTIX-web-research/SKILL.md +69 -0
- package/opencode/skills/KORTIX-xlsx/LICENSE.txt +30 -0
- package/opencode/skills/KORTIX-xlsx/SKILL.md +549 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/__init__.py +0 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/merge_runs.py +199 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/pack.py +159 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/soffice.py +183 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/unpack.py +132 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validate.py +111 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/__init__.py +15 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/base.py +847 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/docx.py +446 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/pptx.py +275 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/redlining.py +247 -0
- package/opencode/skills/KORTIX-xlsx/scripts/recalc.py +184 -0
- package/opencode/tools/image-gen.ts +342 -0
- package/opencode/tools/image-search.ts +190 -0
- package/opencode/tools/memory-get.ts +168 -0
- package/opencode/tools/memory-search.ts +247 -0
- package/opencode/tools/presentation-gen.ts +723 -0
- package/opencode/tools/scrape-webpage.ts +115 -0
- package/opencode/tools/scripts/.python-version +1 -0
- package/opencode/tools/scripts/convert_pdf.py +184 -0
- package/opencode/tools/scripts/convert_pptx.py +562 -0
- package/opencode/tools/scripts/pyproject.toml +11 -0
- package/opencode/tools/scripts/uv.lock +287 -0
- package/opencode/tools/scripts/validate_slide.py +74 -0
- package/opencode/tools/show-user.ts +217 -0
- package/opencode/tools/tests/e2e-presentation-fix.ts +277 -0
- package/opencode/tools/tests/image-gen.test.ts +215 -0
- package/opencode/tools/tests/image-search.test.ts +125 -0
- package/opencode/tools/tests/memory-system-benchmark.ts +1076 -0
- package/opencode/tools/tests/presentation-gen.test.ts +389 -0
- package/opencode/tools/tests/scrape-webpage.test.ts +74 -0
- package/opencode/tools/tests/show-user.test.ts +241 -0
- package/opencode/tools/tests/video-gen.test.ts +110 -0
- package/opencode/tools/tests/web-search.test.ts +106 -0
- package/opencode/tools/video-gen.ts +200 -0
- package/opencode/tools/web-search.ts +153 -0
- package/opencode/tsconfig.json +29 -0
- package/package.json +36 -0
- package/patch-agent-browser.js +100 -0
- package/postinstall.sh +88 -0
- package/services/KORTIX-presentation-viewer/run +37 -0
- package/services/agent-browser-viewer/run +48 -0
- package/services/kortix-master/run +16 -0
- package/services/lss-sync/run +22 -0
- package/services/opencode-serve/run +25 -0
- package/services/opencode-web/run +21 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import Replicate from "replicate";
|
|
3
|
+
import { writeFileSync, mkdirSync, readFileSync, existsSync } from "fs";
|
|
4
|
+
import { resolve, basename, extname, dirname } from "path";
|
|
5
|
+
|
|
6
|
+
const GENERATE_MODEL = "black-forest-labs/flux-schnell";
|
|
7
|
+
const EDIT_MODEL = "black-forest-labs/flux-redux-dev";
|
|
8
|
+
const UPSCALE_MODEL = "recraft-ai/recraft-crisp-upscale";
|
|
9
|
+
const REMOVE_BG_MODEL = "bria/remove-background";
|
|
10
|
+
|
|
11
|
+
type Action = "generate" | "edit" | "upscale" | "remove_bg";
|
|
12
|
+
|
|
13
|
+
interface FileOutput {
|
|
14
|
+
url(): string | URL;
|
|
15
|
+
blob(): Promise<Blob>;
|
|
16
|
+
[Symbol.iterator](): Iterator<Uint8Array>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ensureDir(filePath: string): void {
|
|
20
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function slugify(text: string): string {
|
|
24
|
+
return text
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
27
|
+
.replace(/^-|-$/g, "")
|
|
28
|
+
.slice(0, 60);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function generateFilename(
|
|
32
|
+
action: Action,
|
|
33
|
+
prompt?: string,
|
|
34
|
+
ext = ".webp",
|
|
35
|
+
): string {
|
|
36
|
+
const ts = Date.now();
|
|
37
|
+
const label = prompt ? slugify(prompt) : action;
|
|
38
|
+
return `${label}-${ts}${ext}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function outputToBytes(
|
|
42
|
+
output: unknown,
|
|
43
|
+
): Promise<{ bytes: Buffer; url: string }> {
|
|
44
|
+
if (output && typeof output === "object" && "url" in output) {
|
|
45
|
+
const fo = output as FileOutput;
|
|
46
|
+
const blob = await fo.blob();
|
|
47
|
+
const urlVal = fo.url();
|
|
48
|
+
return {
|
|
49
|
+
bytes: Buffer.from(await blob.arrayBuffer()),
|
|
50
|
+
url: typeof urlVal === "string" ? urlVal : urlVal.toString(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (Array.isArray(output) && output.length > 0) {
|
|
55
|
+
return outputToBytes(output[0]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (typeof output === "string") {
|
|
59
|
+
if (output.startsWith("data:")) {
|
|
60
|
+
const b64 = output.split(",")[1] ?? "";
|
|
61
|
+
return { bytes: Buffer.from(b64, "base64"), url: "" };
|
|
62
|
+
}
|
|
63
|
+
if (output.startsWith("http")) {
|
|
64
|
+
const res = await fetch(output);
|
|
65
|
+
return {
|
|
66
|
+
bytes: Buffer.from(await res.arrayBuffer()),
|
|
67
|
+
url: output,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
throw new Error("Unexpected model output format");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function loadImageAsBase64(imagePath: string): string {
|
|
76
|
+
const abs = resolve(imagePath);
|
|
77
|
+
if (!existsSync(abs)) throw new Error(`Image not found: ${abs}`);
|
|
78
|
+
const bytes = readFileSync(abs);
|
|
79
|
+
const ext = extname(abs).toLowerCase();
|
|
80
|
+
const mimeMap: Record<string, string> = {
|
|
81
|
+
".png": "image/png",
|
|
82
|
+
".jpg": "image/jpeg",
|
|
83
|
+
".jpeg": "image/jpeg",
|
|
84
|
+
".webp": "image/webp",
|
|
85
|
+
".gif": "image/gif",
|
|
86
|
+
};
|
|
87
|
+
const mime = mimeMap[ext] ?? "image/png";
|
|
88
|
+
return `data:${mime};base64,${bytes.toString("base64")}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const SIZE_TO_ASPECT: Record<string, string> = {
|
|
92
|
+
"1024x1024": "1:1",
|
|
93
|
+
"1536x1024": "3:2",
|
|
94
|
+
"1024x1536": "2:3",
|
|
95
|
+
"1792x1024": "16:9",
|
|
96
|
+
"1024x1792": "9:16",
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const QUALITY_MAP: Record<string, number> = {
|
|
100
|
+
low: 60,
|
|
101
|
+
medium: 80,
|
|
102
|
+
high: 95,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
async function runGenerate(
|
|
106
|
+
replicate: Replicate,
|
|
107
|
+
prompt: string,
|
|
108
|
+
size: string,
|
|
109
|
+
quality: string,
|
|
110
|
+
outputDir: string,
|
|
111
|
+
): Promise<{ path: string; url: string }> {
|
|
112
|
+
const aspectRatio = SIZE_TO_ASPECT[size] ?? "1:1";
|
|
113
|
+
const outputQuality = QUALITY_MAP[quality] ?? 80;
|
|
114
|
+
|
|
115
|
+
const output = await replicate.run(GENERATE_MODEL, {
|
|
116
|
+
input: {
|
|
117
|
+
prompt,
|
|
118
|
+
num_outputs: 1,
|
|
119
|
+
aspect_ratio: aspectRatio,
|
|
120
|
+
output_format: "webp",
|
|
121
|
+
output_quality: outputQuality,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const { bytes, url } = await outputToBytes(output);
|
|
126
|
+
const filename = generateFilename("generate", prompt, ".webp");
|
|
127
|
+
const outPath = resolve(outputDir, filename);
|
|
128
|
+
ensureDir(outPath);
|
|
129
|
+
writeFileSync(outPath, bytes);
|
|
130
|
+
return { path: outPath, url };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function runEdit(
|
|
134
|
+
replicate: Replicate,
|
|
135
|
+
prompt: string,
|
|
136
|
+
imagePath: string,
|
|
137
|
+
size: string,
|
|
138
|
+
quality: string,
|
|
139
|
+
outputDir: string,
|
|
140
|
+
): Promise<{ path: string; url: string }> {
|
|
141
|
+
const aspectRatio = SIZE_TO_ASPECT[size] ?? "1:1";
|
|
142
|
+
const outputQuality = QUALITY_MAP[quality] ?? 80;
|
|
143
|
+
const imageDataUrl = loadImageAsBase64(imagePath);
|
|
144
|
+
|
|
145
|
+
const output = await replicate.run(EDIT_MODEL, {
|
|
146
|
+
input: {
|
|
147
|
+
prompt,
|
|
148
|
+
redux_image: imageDataUrl,
|
|
149
|
+
num_outputs: 1,
|
|
150
|
+
aspect_ratio: aspectRatio,
|
|
151
|
+
output_format: "webp",
|
|
152
|
+
output_quality: outputQuality,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const { bytes, url } = await outputToBytes(output);
|
|
157
|
+
const filename = generateFilename("edit", prompt, ".webp");
|
|
158
|
+
const outPath = resolve(outputDir, filename);
|
|
159
|
+
ensureDir(outPath);
|
|
160
|
+
writeFileSync(outPath, bytes);
|
|
161
|
+
return { path: outPath, url };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function runUpscale(
|
|
165
|
+
replicate: Replicate,
|
|
166
|
+
imagePath: string,
|
|
167
|
+
outputDir: string,
|
|
168
|
+
): Promise<{ path: string; url: string }> {
|
|
169
|
+
const imageDataUrl = loadImageAsBase64(imagePath);
|
|
170
|
+
|
|
171
|
+
const output = await replicate.run(UPSCALE_MODEL, {
|
|
172
|
+
input: { image: imageDataUrl },
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const { bytes, url } = await outputToBytes(output);
|
|
176
|
+
const base = basename(imagePath, extname(imagePath));
|
|
177
|
+
const filename = `${base}-upscaled-${Date.now()}.webp`;
|
|
178
|
+
const outPath = resolve(outputDir, filename);
|
|
179
|
+
ensureDir(outPath);
|
|
180
|
+
writeFileSync(outPath, bytes);
|
|
181
|
+
return { path: outPath, url };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function runRemoveBg(
|
|
185
|
+
replicate: Replicate,
|
|
186
|
+
imagePath: string,
|
|
187
|
+
outputDir: string,
|
|
188
|
+
): Promise<{ path: string; url: string }> {
|
|
189
|
+
const imageDataUrl = loadImageAsBase64(imagePath);
|
|
190
|
+
|
|
191
|
+
const output = await replicate.run(REMOVE_BG_MODEL, {
|
|
192
|
+
input: { image: imageDataUrl },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const { bytes, url } = await outputToBytes(output);
|
|
196
|
+
const base = basename(imagePath, extname(imagePath));
|
|
197
|
+
const filename = `${base}-nobg-${Date.now()}.png`;
|
|
198
|
+
const outPath = resolve(outputDir, filename);
|
|
199
|
+
ensureDir(outPath);
|
|
200
|
+
writeFileSync(outPath, bytes);
|
|
201
|
+
return { path: outPath, url };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function friendlyError(e: unknown): string {
|
|
205
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
206
|
+
|
|
207
|
+
if (msg.includes("moderation") || msg.includes("safety"))
|
|
208
|
+
return "Content was blocked by the safety filter. Try a different prompt.";
|
|
209
|
+
if (msg.includes("rate") || msg.includes("429"))
|
|
210
|
+
return "Rate limited. Wait a moment and try again.";
|
|
211
|
+
if (msg.includes("invalid") && msg.includes("image"))
|
|
212
|
+
return "The input image format is not supported. Use PNG, JPEG, or WebP.";
|
|
213
|
+
if (msg.includes("timeout"))
|
|
214
|
+
return "The operation timed out. Try again or use a simpler prompt.";
|
|
215
|
+
|
|
216
|
+
return msg;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export default tool({
|
|
220
|
+
description:
|
|
221
|
+
"Generate, edit, upscale, or remove backgrounds from images using AI models via Replicate. " +
|
|
222
|
+
"Actions: 'generate' (text-to-image via Flux Schnell), 'edit' (modify existing image with prompt via Flux Redux), " +
|
|
223
|
+
"'upscale' (enhance resolution via Recraft Crisp Upscale), 'remove_bg' (remove background via BRIA RMBG 2.0). " +
|
|
224
|
+
"Requires REPLICATE_API_TOKEN. You MUST specify output_dir to control where files are saved.",
|
|
225
|
+
args: {
|
|
226
|
+
action: tool.schema
|
|
227
|
+
.string()
|
|
228
|
+
.describe(
|
|
229
|
+
"Action to perform: 'generate', 'edit', 'upscale', or 'remove_bg'",
|
|
230
|
+
),
|
|
231
|
+
prompt: tool.schema
|
|
232
|
+
.string()
|
|
233
|
+
.optional()
|
|
234
|
+
.describe(
|
|
235
|
+
"Text prompt for generate/edit. Required for 'generate' and 'edit'. " +
|
|
236
|
+
"Be specific and descriptive for best results.",
|
|
237
|
+
),
|
|
238
|
+
image_path: tool.schema
|
|
239
|
+
.string()
|
|
240
|
+
.optional()
|
|
241
|
+
.describe(
|
|
242
|
+
"Path to input image. Required for 'edit', 'upscale', and 'remove_bg'. " +
|
|
243
|
+
"Supports PNG, JPEG, WebP.",
|
|
244
|
+
),
|
|
245
|
+
size: tool.schema
|
|
246
|
+
.string()
|
|
247
|
+
.optional()
|
|
248
|
+
.describe(
|
|
249
|
+
"Image size for generate/edit: '1024x1024' (1:1, default), '1536x1024' (3:2 landscape), " +
|
|
250
|
+
"'1024x1536' (2:3 portrait), '1792x1024' (16:9 wide), '1024x1792' (9:16 tall)",
|
|
251
|
+
),
|
|
252
|
+
quality: tool.schema
|
|
253
|
+
.string()
|
|
254
|
+
.optional()
|
|
255
|
+
.describe("Image quality: 'low', 'medium' (default), or 'high'"),
|
|
256
|
+
output_dir: tool.schema
|
|
257
|
+
.string()
|
|
258
|
+
.describe(
|
|
259
|
+
"Output directory where the generated image will be saved. The directory will be created if it doesn't exist. Required.",
|
|
260
|
+
),
|
|
261
|
+
},
|
|
262
|
+
async execute(args, _context) {
|
|
263
|
+
const token = process.env.REPLICATE_API_TOKEN;
|
|
264
|
+
if (!token) return "Error: REPLICATE_API_TOKEN not set.";
|
|
265
|
+
|
|
266
|
+
const action = args.action as Action;
|
|
267
|
+
if (!["generate", "edit", "upscale", "remove_bg"].includes(action))
|
|
268
|
+
return `Error: Invalid action '${action}'. Use 'generate', 'edit', 'upscale', or 'remove_bg'.`;
|
|
269
|
+
|
|
270
|
+
if ((action === "generate" || action === "edit") && !args.prompt)
|
|
271
|
+
return `Error: 'prompt' is required for '${action}' action.`;
|
|
272
|
+
|
|
273
|
+
if (
|
|
274
|
+
(action === "edit" || action === "upscale" || action === "remove_bg") &&
|
|
275
|
+
!args.image_path
|
|
276
|
+
)
|
|
277
|
+
return `Error: 'image_path' is required for '${action}' action.`;
|
|
278
|
+
|
|
279
|
+
if (!args.output_dir)
|
|
280
|
+
return "Error: 'output_dir' is required. Specify where to save the output.";
|
|
281
|
+
|
|
282
|
+
const replicate = new Replicate({ auth: token });
|
|
283
|
+
const outputDir = resolve(args.output_dir);
|
|
284
|
+
const size = args.size ?? "1024x1024";
|
|
285
|
+
const quality = args.quality ?? "medium";
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
let result: { path: string; url: string };
|
|
289
|
+
|
|
290
|
+
switch (action) {
|
|
291
|
+
case "generate":
|
|
292
|
+
result = await runGenerate(
|
|
293
|
+
replicate,
|
|
294
|
+
args.prompt!,
|
|
295
|
+
size,
|
|
296
|
+
quality,
|
|
297
|
+
outputDir,
|
|
298
|
+
);
|
|
299
|
+
break;
|
|
300
|
+
case "edit":
|
|
301
|
+
result = await runEdit(
|
|
302
|
+
replicate,
|
|
303
|
+
args.prompt!,
|
|
304
|
+
args.image_path!,
|
|
305
|
+
size,
|
|
306
|
+
quality,
|
|
307
|
+
outputDir,
|
|
308
|
+
);
|
|
309
|
+
break;
|
|
310
|
+
case "upscale":
|
|
311
|
+
result = await runUpscale(replicate, args.image_path!, outputDir);
|
|
312
|
+
break;
|
|
313
|
+
case "remove_bg":
|
|
314
|
+
result = await runRemoveBg(replicate, args.image_path!, outputDir);
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return JSON.stringify(
|
|
319
|
+
{
|
|
320
|
+
success: true,
|
|
321
|
+
action,
|
|
322
|
+
output_path: result.path,
|
|
323
|
+
replicate_url: result.url,
|
|
324
|
+
...(args.prompt && { prompt: args.prompt }),
|
|
325
|
+
...(args.image_path && { input_image: args.image_path }),
|
|
326
|
+
},
|
|
327
|
+
null,
|
|
328
|
+
2,
|
|
329
|
+
);
|
|
330
|
+
} catch (e) {
|
|
331
|
+
return JSON.stringify(
|
|
332
|
+
{
|
|
333
|
+
success: false,
|
|
334
|
+
action,
|
|
335
|
+
error: friendlyError(e),
|
|
336
|
+
},
|
|
337
|
+
null,
|
|
338
|
+
2,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import Replicate from "replicate";
|
|
3
|
+
|
|
4
|
+
const SERPER_IMAGES_URL = "https://google.serper.dev/images";
|
|
5
|
+
const MOONDREAM_MODEL =
|
|
6
|
+
"lucataco/moondream2:72ccb656353c348c1385df54b237eeb7bfa874bf11486cf0b9473e691b662d31";
|
|
7
|
+
const MOONDREAM_PROMPT =
|
|
8
|
+
"Describe this image in detail. Include any text visible in the image.";
|
|
9
|
+
const IMAGE_DOWNLOAD_TIMEOUT_MS = 15_000;
|
|
10
|
+
|
|
11
|
+
interface SerperImage {
|
|
12
|
+
imageUrl: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
link?: string;
|
|
15
|
+
imageWidth?: number;
|
|
16
|
+
imageHeight?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SerperResponse {
|
|
20
|
+
images?: SerperImage[];
|
|
21
|
+
searchParameters?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface EnrichedImage {
|
|
25
|
+
url: string;
|
|
26
|
+
title: string;
|
|
27
|
+
source: string;
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
description: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function extractImages(data: SerperResponse): EnrichedImage[] {
|
|
34
|
+
return (data.images ?? []).map((img) => ({
|
|
35
|
+
url: img.imageUrl,
|
|
36
|
+
title: img.title ?? "",
|
|
37
|
+
source: img.link ?? "",
|
|
38
|
+
width: img.imageWidth ?? 0,
|
|
39
|
+
height: img.imageHeight ?? 0,
|
|
40
|
+
description: "",
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function describeImage(
|
|
45
|
+
replicate: Replicate,
|
|
46
|
+
imageUrl: string,
|
|
47
|
+
): Promise<string> {
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch(imageUrl, {
|
|
50
|
+
headers: {
|
|
51
|
+
"User-Agent":
|
|
52
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
53
|
+
},
|
|
54
|
+
signal: AbortSignal.timeout(IMAGE_DOWNLOAD_TIMEOUT_MS),
|
|
55
|
+
redirect: "follow",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (!res.ok) return "";
|
|
59
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
60
|
+
if (!contentType.startsWith("image/")) return "";
|
|
61
|
+
|
|
62
|
+
const imageBytes = await res.arrayBuffer();
|
|
63
|
+
const b64 = Buffer.from(imageBytes).toString("base64");
|
|
64
|
+
const dataUrl = `data:${contentType};base64,${b64}`;
|
|
65
|
+
|
|
66
|
+
const output: unknown = await replicate.run(MOONDREAM_MODEL, {
|
|
67
|
+
input: { image: dataUrl, prompt: MOONDREAM_PROMPT },
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (typeof output === "string") return output.trim();
|
|
71
|
+
if (output && typeof output === "object" && Symbol.iterator in output) {
|
|
72
|
+
return Array.from(output as Iterable<unknown>)
|
|
73
|
+
.map(String)
|
|
74
|
+
.join("")
|
|
75
|
+
.trim();
|
|
76
|
+
}
|
|
77
|
+
return "";
|
|
78
|
+
} catch {
|
|
79
|
+
return "";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function enrichImages(images: EnrichedImage[]): Promise<EnrichedImage[]> {
|
|
84
|
+
const replicateToken = process.env.REPLICATE_API_TOKEN;
|
|
85
|
+
if (!replicateToken || images.length === 0) return images;
|
|
86
|
+
|
|
87
|
+
const replicate = new Replicate({ auth: replicateToken });
|
|
88
|
+
|
|
89
|
+
return Promise.all(
|
|
90
|
+
images.map(async (img) => {
|
|
91
|
+
try {
|
|
92
|
+
const description = await describeImage(replicate, img.url);
|
|
93
|
+
return { ...img, description: description || img.description };
|
|
94
|
+
} catch {
|
|
95
|
+
return img;
|
|
96
|
+
}
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default tool({
|
|
102
|
+
description:
|
|
103
|
+
"Search for images using the Serper Google Images API. " +
|
|
104
|
+
"Returns image URLs with titles, source pages, dimensions, and AI-generated descriptions. " +
|
|
105
|
+
"When REPLICATE_API_TOKEN is set, enriches results with Moondream2 vision descriptions. " +
|
|
106
|
+
"Supports batch queries separated by |||. " +
|
|
107
|
+
"Use specific descriptive queries including topic/brand names for best results.",
|
|
108
|
+
args: {
|
|
109
|
+
query: tool.schema
|
|
110
|
+
.string()
|
|
111
|
+
.describe(
|
|
112
|
+
"Image search query. For batch, separate with ||| (e.g. 'cats ||| dogs')",
|
|
113
|
+
),
|
|
114
|
+
num_results: tool.schema
|
|
115
|
+
.number()
|
|
116
|
+
.optional()
|
|
117
|
+
.describe("Images per query (1-100). Default: 12"),
|
|
118
|
+
enrich: tool.schema
|
|
119
|
+
.boolean()
|
|
120
|
+
.optional()
|
|
121
|
+
.describe(
|
|
122
|
+
"Enrich images with AI descriptions via Moondream2. Requires REPLICATE_API_TOKEN. Default: true",
|
|
123
|
+
),
|
|
124
|
+
},
|
|
125
|
+
async execute(args, _context) {
|
|
126
|
+
const apiKey = process.env.SERPER_API_KEY;
|
|
127
|
+
if (!apiKey) return "Error: SERPER_API_KEY not set.";
|
|
128
|
+
|
|
129
|
+
const numResults = Math.max(1, Math.min(args.num_results ?? 12, 100));
|
|
130
|
+
const shouldEnrich = args.enrich !== false;
|
|
131
|
+
const queries = args.query
|
|
132
|
+
.split("|||")
|
|
133
|
+
.map((q) => q.trim())
|
|
134
|
+
.filter(Boolean);
|
|
135
|
+
if (queries.length === 0) return "Error: empty query.";
|
|
136
|
+
|
|
137
|
+
const headers = {
|
|
138
|
+
"X-API-KEY": apiKey,
|
|
139
|
+
"Content-Type": "application/json",
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
if (queries.length === 1) {
|
|
144
|
+
const res = await fetch(SERPER_IMAGES_URL, {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers,
|
|
147
|
+
body: JSON.stringify({ q: queries[0], num: numResults }),
|
|
148
|
+
});
|
|
149
|
+
if (!res.ok)
|
|
150
|
+
return `Error: Serper API returned ${res.status}: ${await res.text()}`;
|
|
151
|
+
|
|
152
|
+
const data = (await res.json()) as SerperResponse;
|
|
153
|
+
let images = extractImages(data);
|
|
154
|
+
|
|
155
|
+
if (images.length === 0) return `No images found for: '${queries[0]}'`;
|
|
156
|
+
if (shouldEnrich) images = await enrichImages(images);
|
|
157
|
+
|
|
158
|
+
return JSON.stringify(
|
|
159
|
+
{ query: queries[0], total: images.length, images },
|
|
160
|
+
null,
|
|
161
|
+
2,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const payload = queries.map((q) => ({ q, num: numResults }));
|
|
166
|
+
const res = await fetch(SERPER_IMAGES_URL, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers,
|
|
169
|
+
body: JSON.stringify(payload),
|
|
170
|
+
});
|
|
171
|
+
if (!res.ok)
|
|
172
|
+
return `Error: Serper API returned ${res.status}: ${await res.text()}`;
|
|
173
|
+
|
|
174
|
+
const data = await res.json();
|
|
175
|
+
const dataArr: SerperResponse[] = Array.isArray(data) ? data : [data];
|
|
176
|
+
|
|
177
|
+
const results = await Promise.all(
|
|
178
|
+
dataArr.map(async (d, i) => {
|
|
179
|
+
let images = extractImages(d);
|
|
180
|
+
if (shouldEnrich) images = await enrichImages(images);
|
|
181
|
+
return { query: queries[i], total: images.length, images };
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return JSON.stringify({ batch_mode: true, results }, null, 2);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
return `Error: ${String(e)}`;
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-get — Read a specific memory file by path.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors OpenClaw's `memory_get` tool:
|
|
5
|
+
* - Path validation (restricted to .kortix/ directory)
|
|
6
|
+
* - Symlink rejection (prevents path traversal)
|
|
7
|
+
* - Line range slicing support
|
|
8
|
+
* - Non-Markdown rejection
|
|
9
|
+
*
|
|
10
|
+
* Provides safe, structured access to memory files without
|
|
11
|
+
* needing bash or the generic read tool.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { tool } from "@opencode-ai/plugin"
|
|
15
|
+
import { readFile, lstat, realpath } from "node:fs/promises"
|
|
16
|
+
import * as path from "node:path"
|
|
17
|
+
|
|
18
|
+
const BASE_PATH = "/workspace/.kortix"
|
|
19
|
+
|
|
20
|
+
const ALLOWED_EXTENSIONS = new Set([
|
|
21
|
+
".md",
|
|
22
|
+
".txt",
|
|
23
|
+
".json",
|
|
24
|
+
".yaml",
|
|
25
|
+
".yml",
|
|
26
|
+
".toml",
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
function isSubPath(parent: string, child: string): boolean {
|
|
30
|
+
const relative = path.relative(parent, child)
|
|
31
|
+
return !relative.startsWith("..") && !path.isAbsolute(relative)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default tool({
|
|
35
|
+
description:
|
|
36
|
+
"Read a specific memory file by path. " +
|
|
37
|
+
"Restricted to files under workspace/.kortix/ for security. " +
|
|
38
|
+
"Supports reading full files or specific line ranges. " +
|
|
39
|
+
"Use memory_search to find relevant files first, then memory_get to read the full content.",
|
|
40
|
+
args: {
|
|
41
|
+
path: tool.schema.string().describe(
|
|
42
|
+
"Path to the memory file. Can be absolute (e.g., '/workspace/.kortix/MEMORY.md') " +
|
|
43
|
+
"or relative to .kortix/ (e.g., 'MEMORY.md', 'memory/decisions.md', 'journal/2025-01-15.md'). " +
|
|
44
|
+
"Must be under workspace/.kortix/.",
|
|
45
|
+
),
|
|
46
|
+
start_line: tool.schema
|
|
47
|
+
.number()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("Start reading from this line number (1-indexed). Default: 1"),
|
|
50
|
+
lines: tool.schema
|
|
51
|
+
.number()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe(
|
|
54
|
+
"Number of lines to read. Default: all. Useful for large files.",
|
|
55
|
+
),
|
|
56
|
+
},
|
|
57
|
+
async execute(args, _ctx) {
|
|
58
|
+
// Resolve path — support both absolute and relative
|
|
59
|
+
let filePath = args.path
|
|
60
|
+
if (!path.isAbsolute(filePath)) {
|
|
61
|
+
filePath = path.join(BASE_PATH, filePath)
|
|
62
|
+
}
|
|
63
|
+
filePath = path.resolve(filePath)
|
|
64
|
+
|
|
65
|
+
// Security: must be under BASE_PATH
|
|
66
|
+
if (!isSubPath(BASE_PATH, filePath)) {
|
|
67
|
+
return JSON.stringify(
|
|
68
|
+
{
|
|
69
|
+
error: "Access denied",
|
|
70
|
+
message: `Path must be under ${BASE_PATH}. Got: ${args.path}`,
|
|
71
|
+
allowed_paths: [
|
|
72
|
+
"MEMORY.md",
|
|
73
|
+
"memory/*.md",
|
|
74
|
+
"journal/*.md",
|
|
75
|
+
"knowledge/*.md",
|
|
76
|
+
"sessions/*.md",
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
null,
|
|
80
|
+
2,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Security: reject symlinks that point outside BASE_PATH
|
|
85
|
+
try {
|
|
86
|
+
const stats = await lstat(filePath)
|
|
87
|
+
if (stats.isSymbolicLink()) {
|
|
88
|
+
const resolved = await realpath(filePath)
|
|
89
|
+
if (!isSubPath(BASE_PATH, resolved)) {
|
|
90
|
+
return JSON.stringify(
|
|
91
|
+
{
|
|
92
|
+
error: "Access denied",
|
|
93
|
+
message: "Symlinks pointing outside memory directory are not allowed.",
|
|
94
|
+
},
|
|
95
|
+
null,
|
|
96
|
+
2,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
return JSON.stringify(
|
|
102
|
+
{
|
|
103
|
+
error: "File not found",
|
|
104
|
+
message: `No file at: ${args.path}`,
|
|
105
|
+
suggestion: "Use memory_search to find available memory files.",
|
|
106
|
+
},
|
|
107
|
+
null,
|
|
108
|
+
2,
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Validate extension
|
|
113
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
114
|
+
if (!ALLOWED_EXTENSIONS.has(ext)) {
|
|
115
|
+
return JSON.stringify(
|
|
116
|
+
{
|
|
117
|
+
error: "Invalid file type",
|
|
118
|
+
message: `Only Markdown and text files are allowed. Got: ${ext}`,
|
|
119
|
+
allowed: Array.from(ALLOWED_EXTENSIONS),
|
|
120
|
+
},
|
|
121
|
+
null,
|
|
122
|
+
2,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Read the file
|
|
127
|
+
try {
|
|
128
|
+
const content = await readFile(filePath, "utf-8")
|
|
129
|
+
const allLines = content.split("\n")
|
|
130
|
+
const totalLines = allLines.length
|
|
131
|
+
|
|
132
|
+
// Apply line range if specified
|
|
133
|
+
const startLine = Math.max(1, args.start_line ?? 1)
|
|
134
|
+
const endLine = args.lines
|
|
135
|
+
? Math.min(startLine + args.lines - 1, totalLines)
|
|
136
|
+
: totalLines
|
|
137
|
+
const sliced = allLines.slice(startLine - 1, endLine)
|
|
138
|
+
|
|
139
|
+
// Calculate relative path for display
|
|
140
|
+
const relativePath = path.relative(BASE_PATH, filePath)
|
|
141
|
+
|
|
142
|
+
return JSON.stringify(
|
|
143
|
+
{
|
|
144
|
+
path: relativePath,
|
|
145
|
+
absolute_path: filePath,
|
|
146
|
+
total_lines: totalLines,
|
|
147
|
+
showing: {
|
|
148
|
+
start: startLine,
|
|
149
|
+
end: endLine,
|
|
150
|
+
count: sliced.length,
|
|
151
|
+
},
|
|
152
|
+
content: sliced.join("\n"),
|
|
153
|
+
},
|
|
154
|
+
null,
|
|
155
|
+
2,
|
|
156
|
+
)
|
|
157
|
+
} catch (e) {
|
|
158
|
+
return JSON.stringify(
|
|
159
|
+
{
|
|
160
|
+
error: "Read failed",
|
|
161
|
+
message: `Failed to read ${args.path}: ${e instanceof Error ? e.message : String(e)}`,
|
|
162
|
+
},
|
|
163
|
+
null,
|
|
164
|
+
2,
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
})
|