commit e4292bd4976c48b0893ffd5678b7faf27c77ebdd Author: Serge Alex Date: Thu Oct 29 18:55:02 2026 +0300 init-parser-v0-1-0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a69ecd6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.dsh-pro/ +.vscode/ +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..d651493 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# dsh-memory + +Unified Chrome extension: DSH tools (write/edit/bash etc) + persistent memory, in one app. + +## Protocols supported + +- DSH — classic JSON marker: [DSH]{"tool":...,"args":{...}}[/DSH] +- DSH_B64 — base64-encoded UTF-8 JSON: [DSH_B64][/DSH_B64] +- DSH_FILE — raw file content, no escaping: [DSH_FILE path="..."][/DSH_FILE] + +(Real markers use U+27E6 / U+27E7 brackets.) + +## Layout + +- content/parser.js — extracts calls from chat text +- background.js — service worker +- manifest.json — MV3 manifest + +## Test + + +npm test + + +Runs test/parser.test.mjs against all three marker types. + +## Status + +v0.1.0 — parser skeleton only. Bridge, UI, memory migration follow in separate iterations. diff --git a/background.js b/background.js new file mode 100644 index 0000000..89a0ee0 --- /dev/null +++ b/background.js @@ -0,0 +1,3 @@ +'use strict'; + +console.log('dsh-memory ready'); diff --git a/content/parser.js b/content/parser.js new file mode 100644 index 0000000..4ee5e0d --- /dev/null +++ b/content/parser.js @@ -0,0 +1,93 @@ +'use strict'; + +// DSH unified parser. Three markers: +// DSH - classic JSON +// DSH_B64 - base64 UTF-8 JSON +// DSH_FILE - raw file content + +const L = String.fromCharCode(10214); +const R = String.fromCharCode(10215); +const BS = String.fromCharCode(92); +const Q = String.fromCharCode(34); + +const OPEN_DSH = L + 'DSH' + R; +const CLOSE_DSH = L + '/DSH' + R; +const OPEN_B64 = L + 'DSH_B64' + R; +const CLOSE_B64 = L + '/DSH_B64' + R; +const OPEN_FILE = L + 'DSH_FILE'; +const CLOSE_FILE = L + '/DSH_FILE' + R; +const B64_CLEAN = new RegExp('[^A-Za-z0-9+/=]', 'g'); + +function b64DecodeUtf8(b64) { + const clean = String(b64 || '').replace(B64_CLEAN, ''); + const bin = atob(clean); + const bytes = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); + return new TextDecoder('utf-8').decode(bytes); +} + +function tryParseJson(s) { + try { return JSON.parse(s); } catch (e) { return null; } +} + +function parseFileHeader(header) { + const pIdx = header.indexOf('path'); + if (pIdx < 0) return null; + const eq = header.indexOf('=', pIdx); + if (eq < 0) return null; + const q1 = header.indexOf(Q, eq); + if (q1 < 0) return null; + const q2 = header.indexOf(Q, q1 + 1); + if (q2 < 0) return null; + return header.slice(q1 + 1, q2); +} + +function parseOne(text, startIdx) { + if (text.startsWith(OPEN_FILE, startIdx)) { + const openEnd = text.indexOf(R, startIdx + OPEN_FILE.length); + if (openEnd < 0) return null; + const header = text.slice(startIdx + OPEN_FILE.length, openEnd); + const path = parseFileHeader(header); + if (!path) return null; + const closeIdx = text.indexOf(CLOSE_FILE, openEnd + 1); + if (closeIdx < 0) return null; + let content = text.slice(openEnd + 1, closeIdx); + if (content.charCodeAt(0) === 10) content = content.slice(1); + return { call: { tool: 'dsh_write', args: { path: path, content: content } }, nextIdx: closeIdx + CLOSE_FILE.length }; + } + if (text.startsWith(OPEN_B64, startIdx)) { + const closeIdx = text.indexOf(CLOSE_B64, startIdx + OPEN_B64.length); + if (closeIdx < 0) return null; + const raw = text.slice(startIdx + OPEN_B64.length, closeIdx); + const json = b64DecodeUtf8(raw); + const obj = tryParseJson(json); + if (!obj || typeof obj !== 'object' || !obj.tool) return null; + return { call: obj, nextIdx: closeIdx + CLOSE_B64.length }; + } + if (text.startsWith(OPEN_DSH, startIdx)) { + const closeIdx = text.indexOf(CLOSE_DSH, startIdx + OPEN_DSH.length); + if (closeIdx < 0) return null; + const raw = text.slice(startIdx + OPEN_DSH.length, closeIdx); + const obj = tryParseJson(raw); + if (!obj || typeof obj !== 'object' || !obj.tool) return null; + return { call: obj, nextIdx: closeIdx + CLOSE_DSH.length }; + } + return null; +} + +function extractCalls(text, maxCalls) { + const limit = Math.max(1, maxCalls | 0) || 3; + const calls = []; + let pos = 0; + while (calls.length < limit) { + const i = text.indexOf(L, pos); + if (i < 0) break; + const parsed = parseOne(text, i); + if (!parsed) { pos = i + L.length; continue; } + calls.push(parsed.call); + pos = parsed.nextIdx; + } + return { calls: calls, consumed: pos }; +} + +module.exports = { extractCalls: extractCalls, b64DecodeUtf8: b64DecodeUtf8 }; diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..01ec0ef --- /dev/null +++ b/manifest.json @@ -0,0 +1,13 @@ +{ + "manifest_version": 3, + "name": "DSH Memory (unified)", + "version": "0.1.0", + "description": "DSH tools + persistent memory, unified", + "permissions": ["storage", "scripting", "activeTab"], + "host_permissions": ["https://chat.deepseek.com/"], + "background": { "service_worker": "background.js" }, + "content_scripts": [{ + "matches": ["https://chat.deepseek.com/"], + "js": ["content/parser.js"] + }] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8472ddf --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "dsh-memory", + "version": "0.1.0", + "private": true, + "description": "Unified DSH tools + persistent memory extension", + "type": "commonjs", + "scripts": { + "test": "node test/parser.test.mjs" + }, + "license": "MIT" +} diff --git a/test/parser.test.mjs b/test/parser.test.mjs new file mode 100644 index 0000000..4e32211 --- /dev/null +++ b/test/parser.test.mjs @@ -0,0 +1,43 @@ +const mod = await import('node:module'); +const require = mod.createRequire(import.meta.url); +const parser = require('../content/parser.js'); + +const L = String.fromCharCode(10214); +const R = String.fromCharCode(10215); +const LF = String.fromCharCode(10); +const Q = String.fromCharCode(34); +const LB = String.fromCharCode(123); +const RB = String.fromCharCode(125); +const BS = String.fromCharCode(92); +const LT = String.fromCharCode(60); + +let pass = 0; +let fail = 0; +const ok = (name, cond) => cond ? (pass++, console.log('OK ' + name)) : (fail++, console.log('FAIL ' + name)); + +// Test 1: DSH JSON +const json1 = LB + Q + 'tool' + Q + ':' + Q + 'dsh_read' + Q + ',' + Q + 'args' + Q + ':' + LB + Q + 'path' + Q + ':' + Q + 'README.md' + Q + RB + RB; +const t1 = L + 'DSH' + R + json1 + L + '/DSH' + R; +const r1 = parser.extractCalls(t1, 3); +ok('DSH count', r1.calls.length === 1); +ok('DSH tool', !!r1.calls[0] ? r1.calls[0].tool === 'dsh_read' : false); +ok('DSH path', !!r1.calls[0] ? r1.calls[0].args.path === 'README.md' : false); + +// Test 2: DSH_B64 +const b64 = 'eyJ0b29sIjoiZHNoX3RyZWUiLCJhcmdzIjp7InBhdGgiOiIuIn19'; +const t2 = L + 'DSH_B64' + R + b64 + L + '/DSH_B64' + R; +const r2 = parser.extractCalls(t2, 3); +ok('B64 count', r2.calls.length === 1); +ok('B64 tool', !!r2.calls[0] ? r2.calls[0].tool === 'dsh_tree' : false); + +// Test 3: DSH_FILE +const phpBody = LT + '?php' + LF + 'namespace app' + BS + 'models;' + LF; +const t3 = L + 'DSH_FILE path=' + Q + 't.php' + Q + R + LF + phpBody + L + '/DSH_FILE' + R; +const r3 = parser.extractCalls(t3, 3); +ok('FILE count', r3.calls.length === 1); +ok('FILE path', !!r3.calls[0] ? r3.calls[0].args.path === 't.php' : false); +ok('FILE tool', !!r3.calls[0] ? r3.calls[0].tool === 'dsh_write' : false); +ok('FILE content', !!r3.calls[0] ? r3.calls[0].args.content.indexOf('namespace app' + BS + 'models') !== -1 : false); + +console.log('passed ' + pass + ', failed ' + fail); +if (fail > 0) process.exit(1);