fix/nanda array types and live agent publish - #4
Merged
Conversation
Anarv2104
commented
Aug 2, 2026
Collaborator
- Parse capabilities/tags as real arrays before sending to NANDA
- Add Cloudinary asset storage with signed authenticated delivery
- Add seller upload wizard with AI metadata review
- Fix search fallback when AI/NANDA search path returns no results
- Document Node v26.5.x runtime requirement
buildAgentFacts() was passing listing.capabilities/tags straight through from SQLite, where they're stored as JSON-encoded strings (e.g. "[\"code-review\",...]"), not real arrays. NANDA's index was storing and returning a string that merely looks like an array. Adds safeParseArray(): parses the stored JSON string into a real array, passes through unchanged if already an array, and falls back to [] on anything empty or unparseable. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- processUpload streams uploaded files to Cloudinary via resource_type: 'raw', type: 'authenticated' (private, not public), storing only the public_id (not a permanent URL) in stored_path. Falls back to local disk if Cloudinary credentials are missing, matching the existing fallback pattern used by Prava/Linq/NANDA. - prepareDelivery/getDeliveryPayload now self-fetch assets by listingId and generate a fresh, short-expiry (5 min) signed Cloudinary URL at the moment of delivery, after the caller's payment gate has already passed, instead of a permanent link computed once at upload time. - Fixed multer in listings.js to use memoryStorage() instead of disk storage — processUpload has always assumed req.file.buffer exists, which disk storage never populates. - Added cloudinary dependency and CLOUDINARY_* placeholders to .env.example. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real create -> upload -> generate-metadata -> review/edit -> publish flow in SellerPortal.jsx, replacing the old hardcoded capabilities: ['code_review', 'automation'] and the flow that published a listing without ever calling /upload or /generate-metadata: - Drag-and-drop file zone (.zip/.doc/.docx/.txt/.md) for static listings, wired into the real upload sequence via a raw fetch + FormData call (api.js's apiRequest is JSON-only, can't carry a file). - New MetadataReviewModal shows the AI-derived category/ capabilities/tags for the seller to confirm or edit before they're persisted via PUT and the listing is published — visibly shows the AI's real output instead of auto-publishing blind. - api.js now exports API_BASE for the raw upload fetch call. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A teammate's commit added a Weft-Agent/NANDA semantic search path in marketplace.js's /search route, tried ahead of the existing FTS fallback. Two bugs combined to silently return empty search results whenever OPENAI_API_KEY was unset or left at its placeholder value: - weft-agent.js's aiSearchNanda() only checked truthiness of OPENAI_API_KEY, so the literal placeholder string counted as "configured" (unlike llm-client.js's existing correct check elsewhere in this codebase, which explicitly excludes it). - Even when aiSearchNanda failed internally, it caught the error and returned [] instead of throwing, so marketplace.js's FTS fallback (which only triggered on a thrown exception) never ran. Fixes: weft-agent.js now excludes the placeholder key, matching llm-client.js's pattern. marketplace.js now falls back to FTS whenever aiSearchNanda produces zero results, not only when it throws. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
better-sqlite3's native binary has a confirmed ABI conflict under Node v20.18.3 on this stack: loading it crashes with EXC_BAD_ACCESS inside napi_module_register_by_symbol (confirmed with lldb). The crash reproduces identically whether using the registry's prebuilt binary or rebuilding from source against the exact v20.18.3 headers, so this isn't a build mistake — v26.5.x loads it cleanly and is what this project now targets. Also documents that .mcp.json (gitignored) belongs at the repo's own root, not a nested subfolder, after an earlier session lost time to edits landing in an unused duplicate one level down. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR improves NANDA integration correctness (arrays + search fallback), introduces optional Cloudinary-backed asset storage with signed delivery, and adds a seller-side upload + AI metadata review flow to support publishing static listings.
Changes:
- Parse
capabilities/tagsas real arrays when publishing agent facts to NANDA, and improve AI search fallback to local FTS when no results are returned. - Add Cloudinary “authenticated” raw asset uploads and short-lived signed download URLs at delivery time (fallback to local disk when not configured).
- Add a seller upload wizard for static listings, including AI metadata generation and a confirmation modal before publishing.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/services/weft-agent.js | Treat placeholder OpenAI keys as invalid to avoid misleading configuration. |
| src/services/nanda.js | Ensure capabilities/tags are sent to NANDA as arrays even when stored as JSON strings. |
| src/services/asset-processor.js | Add Cloudinary upload/delivery path and signed URL generation for assets. |
| src/routes/marketplace.js | Fall back to FTS when AI/NANDA search returns an empty result set (not only on throw). |
| src/routes/listings.js | Switch uploads to in-memory buffers to support Cloudinary streaming uploads. |
| README.md | Document Node v26.5.x runtime requirement and rationale. |
| package.json | Add cloudinary dependency. |
| package-lock.json | Lock dependency updates for Cloudinary and transitive deps. |
| frontend/src/components/SellerPortal.jsx | Add seller upload + AI metadata review publishing flow for static listings; publish live listings directly. |
| frontend/src/components/MetadataReviewModal.jsx | New UI for reviewing/editing AI-generated metadata before publish. |
| frontend/src/api.js | Export API_BASE for use by direct fetch upload helper. |
| .env.example | Document Cloudinary environment variables. |
Suppressed comments (1)
src/services/asset-processor.js:97
- The delivered/manifest path for non-zip uploads currently uses
file.originalnameverbatim. If that name contains path separators, consumers that save/extract using this path can be exposed to path traversal / zip-slip style issues. Use the same sanitized filename used for storage.
manifest = [{
relativePath: file.originalname,
role: 'main',
sizeBytes: file.size,
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+60
to
78
| const isZip = file.originalname.toLowerCase().endsWith('.zip'); | ||
| let storedPath; | ||
| let storage; | ||
|
|
||
| if (cloudinaryEnabled) { | ||
| const publicId = `weft/${listingId}/${uuidv4()}_${file.originalname}`; | ||
| const result = await uploadToCloudinary(file.buffer, publicId); | ||
| storedPath = result.public_id; // signed fresh at delivery time, never a stored permanent URL | ||
| storage = 'cloudinary'; | ||
| } else { | ||
| const listingDir = path.join(UPLOADS_DIR, listingId); | ||
| if (!fs.existsSync(listingDir)) { | ||
| fs.mkdirSync(listingDir, { recursive: true }); | ||
| } | ||
| const storedFileName = `${uuidv4()}_${file.originalname}`; | ||
| storedPath = path.join(listingDir, storedFileName); | ||
| fs.writeFileSync(storedPath, file.buffer); | ||
| storage = 'local'; | ||
| } |
Comment on lines
144
to
146
| if (asset.manifest_json) { | ||
| completeManifest = completeManifest.concat(typeof asset.manifest_json === 'string' ? JSON.parse(asset.manifest_json) : asset.manifest_json); | ||
| } |
Comment on lines
+148
to
+152
| if (cloudinaryEnabled && isCloudinaryPublicId(asset.stored_path)) { | ||
| // Generated here, at the moment of delivery — after the caller's payment | ||
| // gate has already passed — so it's never a permanent link computed once | ||
| // at upload time. Short TTL instead of embedding buyer context: Cloudinary's | ||
| // signed-URL API has no field for arbitrary audit metadata. |
|
|
||
| const router = Router(); | ||
| const upload = multer({ dest: './uploads/temp' }); | ||
| const upload = multer({ storage: multer.memoryStorage() }); |
Comment on lines
+15
to
17
| if (!process.env.OPENAI_API_KEY || process.env.OPENAI_API_KEY === 'sk-your-openai-key') { | ||
| throw new Error('OPENAI_API_KEY is not set'); | ||
| } |
Comment on lines
+24
to
+30
| <div style={{ position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(0,0,0,0.85)', backdropFilter: 'blur(10px)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '20px' }}> | ||
| <div style={{ background: 'var(--bg-card)', border: '1px solid var(--border-strong)', borderRadius: 'var(--radius-lg)', padding: '32px', maxWidth: '520px', width: '100%', boxShadow: 'var(--shadow-card)' }}> | ||
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}> | ||
| <h3 style={{ fontSize: '1.25rem', fontWeight: 700, display: 'flex', alignItems: 'center', gap: '8px' }}> | ||
| <Sparkles size={20} color="var(--accent-brand)" /> Review AI-Generated Metadata | ||
| </h3> | ||
| <button onClick={onCancel} style={{ background: 'none', border: 'none', color: 'var(--text-secondary)', cursor: 'pointer' }}><X size={20} /></button> |
Comment on lines
+654
to
+658
| <button | ||
| type="button" | ||
| onClick={(e) => { e.stopPropagation(); setUploadFile(null); }} | ||
| style={{ background: 'none', border: 'none', color: 'var(--text-secondary)', cursor: 'pointer' }} | ||
| > |
Priyank911
approved these changes
Aug 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.