commit
e4292bd497
7 changed files with 196 additions and 0 deletions
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
node_modules/ |
||||
.dsh-pro/ |
||||
.vscode/ |
||||
*.log |
||||
@ -0,0 +1,29 @@
@@ -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]<base64>[/DSH_B64] |
||||
- DSH_FILE — raw file content, no escaping: [DSH_FILE path="..."]<raw>[/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. |
||||
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
'use strict'; |
||||
|
||||
console.log('dsh-memory ready'); |
||||
@ -0,0 +1,93 @@
@@ -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 }; |
||||
@ -0,0 +1,13 @@
@@ -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"] |
||||
}] |
||||
} |
||||
@ -0,0 +1,11 @@
@@ -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" |
||||
} |
||||
@ -0,0 +1,43 @@
@@ -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); |
||||
Loading…
Reference in new issue