Compare commits

..

No commits in common. "c408289e0fb820016432aa5ad65e065527c030bd" and "f49d61f14d48576ca567972a4ef5057493a13d96" have entirely different histories.

16 changed files with 84 additions and 865 deletions

View File

@ -1,90 +0,0 @@
@startuml
title AI Repo Commander — Config overview (safe)
class ConfigManager {
+load() : object
+save() : void
+get(keyPath) : any
+set(keyPath, value) : void
+mergeConfigs(defaults, saved) : object
+deepClone(o) : any
}
class STORAGE_KEYS {
history : "ai_repo_commander_executed"
cfg : "ai_repo_commander_cfg"
panel : "ai_repo_commander_panel_state"
}
' Split DEFAULT_CONFIG into simple classes (no quotes, no =)
class DEFAULT_META {
version : 1.6.2
}
class DEFAULT_API {
enabled : boolean
timeout : number
maxRetries : number
bridgeKey : string
}
class DEFAULT_DEBUG {
enabled : boolean
level : number
maxLines : number
showPanel : boolean
}
class DEFAULT_EXECUTION {
debounceDelay : ms
settleCheckMs : ms
settlePollMs : ms
requireTerminator : boolean
coldStartMs : ms
stuckAfterMs : ms
scanDebounceMs : ms
fastWarnMs : ms
slowWarnMs : ms
clusterRescanMs : ms
clusterMaxLookahead : count
}
class DEFAULT_QUEUE {
minDelayMs : ms
maxPerMinute : count
maxPerMessage : count
waitForComposerMs : ms
}
class DEFAULT_UI {
autoSubmit : boolean
appendTrailingNewline : boolean
postPasteDelayMs : ms
showExecutedMarker : boolean
processExisting : boolean
submitMode : "enter|ctrl+enter"
maxComposerWaitMs : ms
submitMaxRetries : count
}
class DEFAULT_STORAGE {
dedupeTtlMs : ms
cleanupAfterMs : ms
cleanupIntervalMs : ms
}
class DEFAULT_RESPONSE {
bufferFlushDelayMs : ms
sectionHeadings : boolean
maxPasteChars : number
splitLongResponses : boolean
}
class DEFAULT_RUNTIME {
paused : boolean
'(note: runtime is not persisted)
}
ConfigManager --> STORAGE_KEYS : uses localStorage keys
ConfigManager --> DEFAULT_META : deepClone/merge
ConfigManager --> DEFAULT_API
ConfigManager --> DEFAULT_DEBUG
ConfigManager --> DEFAULT_EXECUTION
ConfigManager --> DEFAULT_QUEUE
ConfigManager --> DEFAULT_UI
ConfigManager --> DEFAULT_STORAGE
ConfigManager --> DEFAULT_RESPONSE
ConfigManager --> DEFAULT_RUNTIME : in-memory only
@enduml

View File

@ -1,445 +0,0 @@
' ===================================================================
' File: ConfigManager.puml
' Purpose: Single source of truth for class-level activity and per-method sequences.
' Edit rules: Follow the legend at bottom; preserve VIEW/METHOD anchors for automation.
' ===================================================================
' (Optional) neutral defaults — typography/layout only (keeps your colors intact)
skinparam Shadowing false
skinparam SequenceMessageAlign center
skinparam SequenceLifeLineBorderColor #666666
skinparam SequenceLifeLineBorderThickness 1
' ==== VIEW: Branch Flow (full class) ==========================================
@startuml
title ConfigManager — Branch Flow (full class)
start
:ConfigManager;
' Fan-out to each method
fork
' -------- constructor() --------
partition "constructor()" #E7FAE3 {
:constructor();
:this.config = load();
kill
}
fork again
' -------- load() --------
partition "load()" #FFF6D1 {
:load();
:raw = localStorage.getItem(STORAGE_KEYS.cfg);
if (raw is null/empty?) then (yes)
:config = deepClone(DEFAULT_CONFIG);
kill
else (no)
:try parse = JSON.parse(raw);
if (parse ok?) then (yes)
:saved = parse;
:config = mergeConfigs(DEFAULT_CONFIG, saved);
kill
else (no / parse error)
:config = deepClone(DEFAULT_CONFIG);
kill
endif
endif
}
fork again
' -------- save() --------
partition "save()" #FFE1DB {
:save();
:persistable = deepClone(config);
if (persistable has runtime?) then (yes)
:delete persistable.runtime;
endif
:try json = JSON.stringify(persistable);
if (stringify ok?) then (yes)
:localStorage.setItem(STORAGE_KEYS.cfg, json);
kill
else (no / stringify error)
:/* log/notify failure */;
kill
endif
}
fork again
' -------- get(keyPath) --------
partition "get(keyPath)" #DCF9EE {
:get(keyPath);
:parts = keyPath.split('.');
:node = config;
while (more parts?)
:p = next part;
if (node[p] exists?) then (yes)
:node = node[p];
else (no)
:return undefined;
kill
endif
endwhile
:return node;
kill
}
fork again
' -------- set(keyPath, value) --------
partition "set(keyPath, value)" #FFE6F0 {
:set(keyPath, value);
:parts = keyPath.split('.');
:node = config;
while (more parts?)
:p = next part;
if (node[p] exists?) then (yes)
:node = node[p];
else (no)
:node[p] = {};
:node = node[p];
endif
endwhile
:assign value at final key;
:save();
kill
}
fork again
' -------- mergeConfigs(defaults, saved) --------
partition "mergeConfigs(defaults, saved)" #E6F3FF {
:mergeConfigs(defaults, saved);
:result = deepClone(defaults);
if (saved is object?) then (yes)
:for each key k in saved;
while (keys left?)
:k = next key;
if (k == "runtime"?) then (yes)
:skip;
else (no)
if (both result[k] and saved[k] are plain objects?) then (yes)
:result[k] = mergeConfigs(result[k], saved[k]);
else (no)
:result[k] = deepClone(saved[k]);
endif
endif
endwhile
else (no)
:/* nothing to merge */;
endif
:return result;
kill
}
fork again
' -------- deepClone(o) --------
partition "deepClone(o)" #F0E6FA {
:deepClone(o);
if (o is null or primitive?) then (yes)
:return o;
kill
else (no)
if (o is Array?) then (yes)
:clone = [];
:for each item -> push( deepClone(item) );
:return clone;
kill
else (no)
:clone = {};
:for each key -> clone[key] = deepClone(o[key]);
:return clone;
kill
endif
endif
}
end fork
@enduml
' ==== METHOD: constructor() ================================================
@startuml
title ConfigManager:constructor(): \n Populate this.config at instantiation
actor Page as PG
participant "constructor()" as CTOR
participant "load()" as LD
PG -> CTOR : new ConfigManager()
activate CTOR
CTOR -> LD : populate this.config
LD --> CTOR : config object
CTOR --> PG : this.config set
deactivate CTOR
@enduml
' ==== METHOD: load() =======================================================
@startuml
title ConfigManager:load(): \n Read from localStorage, parse+merge or fallback to defaults
participant "Caller" as CL
participant "load()" as LD
participant "localStorage" as LS
participant "mergeConfigs()" as MC
participant "deepClone()" as DC
activate CL
activate LD
CL -> LD : initial request
activate LS
LD -> LS : getItem(STORAGE_KEYS.cfg)
LS --> LD : getItem(STORAGE_KEYS.cfg)
deactivate LS
alt STORAGE_KEYS.cfg (Empty)
activate DC
LD -> DC : deepClone(DEFAULT_CONFIG)
LD <-- DC : defaults clone
deactivate DC
LD --> CL : return defaults
else STORAGE_KEYS.cfg (Not empty)
LD --> LD : try parse STORAGE_KEYS.cfg
alt parse ok
LD --> LD : saved = parsed
activate MC
LD -> MC : mergeConfigs(DEFAULT_CONFIG, saved)
MC --> LD : merged config
deactivate MC
LD --> CL : return merged config
else parse error
activate DC
LD -> DC : deepClone(DEFAULT_CONFIG)
LD <-- DC : defaults clone
deactivate DC
LD --> CL : return defaults
end
end
@enduml
' ==== METHOD: save() =======================================================
@startuml
title ConfigManager:save(): \n Strip runtime, stringify, persist to localStorage
participant "Caller" as CL
participant "save()" as SV
participant "deepClone()" as DC
participant "JSON" as JS
participant "localStorage" as LS
participant "Console" as CLG
activate CL
CL -> SV : initial request
deactivate CL
activate SV
activate DC
SV -> DC : deepClone(config)
DC --> SV : persistable clone
deactivate DC
SV -> SV : delete any persistable.runtime
activate JS
SV -> JS : JSON.stringify(persistable)
alt stringify ok
JS --> SV : json string
activate LS
SV -> LS : setItem(STORAGE_KEYS.cfg, json)
deactivate LS
else stringify error
JS --> SV : error
activate CLG
SV -> CLG : log/notify failure
deactivate CLG
end
deactivate JS
@enduml
' ==== METHOD: get(keyPath) ================================================
@startuml
title ConfigManager:get(keyPath): \n Resolve a dotted path or return undefined
participant "Caller" as CL
participant "get(keyPath)" as GET
activate CL
CL -> GET : initial request (keyPath)
activate GET
GET -> GET : parts = keyPath.split('.'); node = config
loop for each part
alt node has part
GET -> GET : node = node[part]
else missing segment
GET --> CL : undefined
end
end
GET --> CL : value (final node)
deactivate GET
deactivate CL
@enduml
' ==== METHOD: set(keyPath, value) =========================================
@startuml
title ConfigManager:set(keyPath, value): \n Create missing path segments, assign, then persist
participant "Caller" as CL
participant "set(keyPath, value)" as SET
participant "save()" as SV
activate CL
CL -> SET : initial request (keyPath, value)
activate SET
SET -> SET : parts = keyPath.split('.'); node = config
loop for each part (except last)
alt node has part
SET -> SET : node = node[part]
else missing
SET -> SET : node[part] = {}; node = node[part]
end
end
SET -> SET : assign value at final key
SET -> SV : save()
SV --> SET : persisted
SET --> CL : done
deactivate SET
deactivate CL
@enduml
' ==== METHOD: mergeConfigs(defaults, saved) ================================
@startuml
title ConfigManager:mergeConfigs(defaults, saved): \n Deep merge saved over defaults (skip runtime)
participant "Caller" as CL
participant "mergeConfigs()" as MC
participant "deepClone()" as DC
activate CL
CL -> MC : initial request (defaults, saved)
activate MC
MC -> DC : deepClone(defaults)
DC --> MC : result (clone of defaults)
alt saved is plain object
loop for each key k in saved
alt k == "runtime"
MC -> MC : skip key
else not runtime
alt both result[k] and saved[k] are plain objects
MC -> MC : result[k] = mergeConfigs(result[k], saved[k]) ' recursive
else overwrite
MC -> DC : deepClone(saved[k])
DC --> MC : cloned value
MC -> MC : result[k] = cloned value
end
end
end
else saved not object
MC -> MC : nothing to merge
end
MC --> CL : result
deactivate MC
deactivate CL
@enduml
' ==== METHOD: deepClone(o) ================================================
@startuml
title ConfigManager:deepClone(o): \n Structural clone for arrays/objects; primitives by value
participant "Caller" as CL
participant "deepClone()" as DC
activate CL
CL -> DC : initial request (o)
activate DC
alt o is null or primitive
DC --> CL : o
deactivate DC
deactivate CL
return
else non-primitive
alt Array
DC -> DC : clone = []
loop each item
DC -> DC : clone.push( deepClone(item) )
end
DC --> CL : clone
else Object
DC -> DC : clone = {}
loop each key
DC -> DC : clone[key] = deepClone(o[key])
end
DC --> CL : clone
end
end
deactivate DC
deactivate CL
@enduml
' ==== LEGEND ===============================================================
@startuml
legend bottom
== Config UML Style Guide (for future edits) ==
• Scope: One .puml per class or file. Keep two views:
(1) Activity "Branch Flow" for all methods for the class or functions in the file.
- (partitions + soft colors),
(2) Per-method Sequence diagrams for each of the methods or functions.
• Sequence conventions:
1) First participant is the external caller (use "Caller" or "Page").
2) Do NOT add the class lifeline unless needed (constructor). Class name appears in title only.
3) Include every directly-called method or subsystem as a participant
(e.g., "load()", "mergeConfigs()", "deepClone()", "JSON", "localStorage", "Console").
4) Prefer simple messages.
5) Use activate/deactivate for the method under focus and key collaborators.
6) Use alt blocks only when branches meaningfully change the message flow.
For trivial checks (e.g., delete runtime if exists), inline the action.
7) Titles: "ClassName:method(): \n Detailed description of the flow".
• Activity view conventions:
A) Start with Class(or filename) node then fork partitions for each method or function.
B) One partition per method; soft background color; terminate branches with 'kill'.
C) Keep wording aligned with code (e.g., "deepClone(DEFAULT_CONFIG)", "mergeConfigs(...)").
• Color palette (soft pastels)
• Use --> for returns; -> for calls.
• Participants use quoted method names for internals (e.g., "save()"), and plain nouns for systems ("JSON", "localStorage", "Console").
• When modifying this file keep this legend at the end of the file to standardize edits.
UML_Example
------------------------------------------
title ClassName:methodName(args): \n Detailed description of what this method does
participant "Caller" as CL
participant "methodName()" as M ' the method under focus
' Add collaborators as needed:
' participant "SomeDependency" as DEP
' participant "AnotherMethod()" as AM
' participant "JSON" as JS
' participant "localStorage" as LS
activate CL
CL -> M : initial request (args)
activate M
' -- inner flow (keep alt blocks only if they clarify) --
' Example pattern:
activate LS
' M -> LS : getItem(KEY)
' LS --> M : value
deactivate LS
' alt branch condition
activate AM
' M -> AM : call anotherMethod(...)
' AM --> M : result
deactivate AM
' else other branch
activate DEP
' M -> DEP : do something
' DEP -> M : ok
deactivate DEP
' end
' Return to caller
M -> CL : return value
deactivate M
deactivate CL
---------------------------------------------
endlegend
@enduml

View File

@ -0,0 +1,42 @@
@startuml Command Processing State Machine
skinparam shadowing false
skinparam ArrowColor #555
skinparam state {
StartColor #A5D6A7
BackgroundColor #FAFAFA
}
[*] --> DETECTED : YAML block found
state DETECTED {
}
DETECTED --> PARSING : debounce/settle passed
PARSING --> VALIDATING : YAML parsed
PARSING --> ERROR : parse failure
VALIDATING --> DEDUPE_CHECK : required fields ok
VALIDATING --> ERROR : validation failed
state DEDUPE_CHECK
DEDUPE_CHECK --> SKIPPED : duplicate or example:true
DEDUPE_CHECK --> READY : new and runnable
state READY
READY --> EXECUTING : user intent or auto-exec policy
READY --> [*] : STOP triggered
state EXECUTING
EXECUTING --> COMPLETE : success
EXECUTING --> ERROR : API/network failure
state SKIPPED
SKIPPED --> [*]
state COMPLETE
COMPLETE --> [*]
state ERROR
ERROR --> [*]
@enduml

View File

@ -1,9 +1,4 @@
// ==COMMAND EXECUTOR START==
// Module: command-executor.js
// Depends on: config.js, logger.js
// Purpose: Execute validated repo commands via the bridge API (GM_xmlhttpRequest),
// handling retries/timeouts and rendering success/error effects. Stores large
// response bodies for paste-submit.
/* global GM_xmlhttpRequest */
/* global GM_notification */
(function () {

View File

@ -1,20 +1,5 @@
// ==COMMAND PARSER START==
// Module: command-parser.js
// Purpose: Extract and parse YAML-like command blocks embedded in assistant messages.
// - Looks for a complete block delimited by @bridge@ ... @end@
// - Parses simple key/value pairs and multiline "|" blocks
// - Applies sane defaults (url, owner, source_branch)
// - Validates presence of required fields per action
// This module is side-effect free and exposes a single class via window.AI_REPO_PARSER.
(function () {
/**
* CommandParser
* - parse(text): Extracts first complete command block and returns a structured object
* - extractBlock(text): Returns the inner text between @bridge@ and @end@
* - parseKV(block): Minimal YAML-like parser supporting multi-line values with "|"
* - applyDefaults(obj): Applies default values and owner/repo split logic
* - validate(obj): Returns { isValid, errors, example? }
*/
class CommandParser {
static REQUIRED = {
get_file: ['action', 'repo', 'path'],

View File

@ -1,105 +1,77 @@
// ==CONFIG START==
(function () {
// LocalStorage keys used by this userscript. These names are stable across versions.
// - history: per-conversation dedupe records (fingerprints of executed commands)
// - cfg: persisted user configuration (excluding transient runtime flags)
// - panel: debug panel UI state (position, collapsed state, selected tab)
const STORAGE_KEYS = {
history: 'ai_repo_commander_executed',
cfg: 'ai_repo_commander_cfg',
panel: 'ai_repo_commander_panel_state'
};
/**
* DEFAULT_CONFIG holds all tunable settings for AI Repo Commander.
* Access values via: window.AI_REPO_CONFIG.get('path.to.key')
* Update at runtime via: window.AI_REPO_CONFIG.set('path.to.key', value)
*
* Sections:
* - meta: Script metadata (version only)
* - api: Bridge/API behavior (timeouts, retries, key)
* - debug: Logging and debug panel settings
* - execution: Detector and processing timings / hardening
* - queue: Rate limiting for executing commands
* - ui: Paste/submit behavior and UX toggles
* - storage: Dedupe/cleanup persistence settings
* - response: Paste buffer behavior for long responses
* - runtime: Transient flags (not persisted)
*/
const DEFAULT_CONFIG = {
// Script/version metadata. Not used for logic; useful for UI and logs.
meta: { version: '1.6.2' },
// Bridge/API call settings used by command-executor.js
api: {
enabled: true, // Master switch: if false, actions are mocked locally (no network)
timeout: 60000, // Request timeout for GM_xmlhttpRequest, in milliseconds
maxRetries: 2, // Number of retries after the initial attempt (total attempts = 1 + maxRetries)
bridgeKey: '' // Secret key sent as X-Bridge-Key header. Prompted if empty when enabled
enabled: true,
timeout: 60000,
maxRetries: 2,
bridgeKey: ''
},
// Debug logging configuration and debug panel behavior
debug: {
enabled: true, // Toggle logging/panel features globally
level: 3, // 0=off, 1=error, 2=warn, 3=info, 4=verbose, 5=trace (see logger.js)
watchMs: 120000, // Time window used by Logger.logLoop to limit repeated logs
maxLines: 400, // Max log entries kept in memory (oldest are dropped)
showPanel: true // Show the draggable Logs/Tools panel when true
enabled: true,
level: 3, // 0=off, 1=errors, 2=warn, 3=info, 4=verbose, 5=trace
watchMs: 120000,
maxLines: 400,
showPanel: true
},
// Execution hardening and detector timing options (see detector.js)
execution: {
debounceDelay: 6500, // Wait after a new assistant message to allow streaming to finish
settleCheckMs: 1300, // Stable-window length to consider text "settled" after last change
settlePollMs: 250, // How often to poll the DOM during the settle window
requireTerminator: true, // Require @end@ terminator inside blocks before attempting to execute
coldStartMs: 2000, // Initial delay after page load to avoid immediate re-execution
stuckAfterMs: 10 * 60 * 1000, // Consider a long-running flow "stuck" after this time (for warnings)
scanDebounceMs: 400, // Debounce for scanning existing content or rapid changes
fastWarnMs: 50, // Threshold for logging fast operations as timing markers
slowWarnMs: 60000, // Threshold for warning on slow operations
clusterRescanMs: 1000, // Interval to rescan neighboring assistant messages for chained blocks
clusterMaxLookahead: 3 // How many subsequent assistant messages to peek when clustering
debounceDelay: 6500,
settleCheckMs: 1300,
settlePollMs: 250,
requireTerminator: true,
coldStartMs: 2000,
stuckAfterMs: 10 * 60 * 1000,
scanDebounceMs: 400,
fastWarnMs: 50,
slowWarnMs: 60000,
clusterRescanMs: 1000,
clusterMaxLookahead: 3
},
// Queue/rate-limiting settings (see queue.js)
queue: {
minDelayMs: 1500, // Minimum delay between two executed commands
maxPerMinute: 15, // Rate cap: maximum commands started per rolling minute
maxPerMessage: 5, // Safety limit: maximum commands taken from a single assistant message
waitForComposerMs: 12000 // How long to wait for the chat composer to be ready before giving up
minDelayMs: 1500,
maxPerMinute: 15,
maxPerMessage: 5,
waitForComposerMs: 12000
},
// UI behavior around pasting into the composer and submitting
ui: {
autoSubmit: true, // If true, attempt to submit after pasting (button click or Enter key)
appendTrailingNewline: true, // Append a newline to pasted content to preserve code fences in some editors
postPasteDelayMs: 600, // Small delay after paste before trying to click Send/press Enter
showExecutedMarker: true, // Visually mark messages that had commands executed (left border/title)
processExisting: false, // On init, optionally scan and process messages already on the page
submitMode: 'button_first', // Submit strategy: 'button_first' tries button, then falls back to Enter
maxComposerWaitMs: 15 * 60 * 1000, // Global max wait for composer availability in edge cases
submitMaxRetries: 12 // How many times to retry submit attempts (e.g., flaky Send button)
autoSubmit: true,
appendTrailingNewline: true,
postPasteDelayMs: 600,
showExecutedMarker: true,
processExisting: false,
submitMode: 'button_first',
maxComposerWaitMs: 15 * 60 * 1000,
submitMaxRetries: 12
},
// Persistence and housekeeping settings for localStorage
storage: {
dedupeTtlMs: 30 * 24 * 60 * 60 * 1000, // 30 days; prevent re-execution in the same conversation within TTL
cleanupAfterMs: 30000, // Delay before running a cleanup pass after startup
cleanupIntervalMs: 60000 // How frequently to run periodic cleanup of stale records
dedupeTtlMs: 30 * 24 * 60 * 60 * 1000, // 30 days
cleanupAfterMs: 30000,
cleanupIntervalMs: 60000
},
// Response paste buffer settings (see response-buffer.js)
response: {
bufferFlushDelayMs: 500, // Delay before flushing buffered chunks to paste (to batch sibling results)
sectionHeadings: true, // When true, prepend small headings when pasting multiple sections
maxPasteChars: 250000, // Maximum characters to paste at once; larger results are split
splitLongResponses: true // Split long responses into multiple pastes when exceeding maxPasteChars
bufferFlushDelayMs: 500,
sectionHeadings: true,
maxPasteChars: 250000,
splitLongResponses: true
},
// Runtime state (not persisted) — toggled by UI/console helpers
// Runtime state (not persisted)
runtime: {
paused: false // When true, detectors ignore new mutations; queue continues unless stopped
paused: false
}
};

View File

@ -1,11 +1,5 @@
// ==DEBUG PANEL START==
// Module: debug-panel.js
// Depends on: config.js, logger.js, queue.js, storage.js
// Purpose: In-page draggable panel showing recent logs and exposing tools/settings.
// - Logs tab: tail of the Logger buffer with copy buttons
// - Tools & Settings: toggles and numeric inputs bound to config, quick actions
// - Pause/Stop controls and queue size indicator
// The panel stores its position/collapsed state in localStorage (see config.js STORAGE_KEYS).
/* global GM_notification */
(function () {
const cfg = () => window.AI_REPO_CONFIG;

View File

@ -1,13 +1,5 @@
// ==DETECTOR START==
// Module: detector.js
// Depends on: config.js, logger.js, queue.js, command-parser.js, command-executor.js, storage.js
// Purpose: Observe assistant messages in the DOM, wait for streaming to finish,
// extract @bridge@...@end@ command blocks, and enqueue them for execution.
// Key features:
// - Debounce window to allow AI streaming to complete
// - Settle check: ensures text remains stable for a small window
// - Per-message command cap and de-duplication via storage.js
// - Optional processing of existing messages on page load
(function () {
const cfg = () => window.AI_REPO_CONFIG;
const log = () => window.AI_REPO_LOGGER;

View File

@ -1,72 +1,23 @@
// ==FINGERPRINT (drop-in utility) ==
// Purpose: Generate a strong, stable fingerprint string for an assistant message element.
//
// This utility is designed to uniquely identify assistant messages across dynamic UIs
// (ChatGPT, Claude, Gemini). It uses multiple content- and position-aware components:
// - ch: content hash of the most "command-like" text in the element (prefers fenced code)
// - ph: hash of the immediately preceding assistant messages' trailing text (context)
// - ih: hash of the text right before the first @bridge@...@end@ block (intra-message prefix)
// - hint: small DOM hint (tag#id.class) hashed to keep size small
// - n: ordinal among elements with same ch+ph+ih on the page at this moment
//
// The combination helps disambiguate near-duplicate messages, re-ordered DOM, and small edits.
// Fingerprints are stable enough to persist in localStorage for de-duplication.
//
// Notes:
// - All hashes are short base36 strings derived from a djb2-xor style hash; fast and sufficient.
// - Inputs are normalized (trim, strip zero-width spaces, normalize whitespace before newlines).
// - The algorithm intentionally looks at at most ~2000 chars per slice for performance.
// - The module is side-effect free except for optional caching of a stable fingerprint in dataset.
(function(){
/**
* CSS selectors that identify assistant messages across supported sites.
* These are joined with "," and used for querySelectorAll when scanning neighbors.
*/
const MSG_SELECTORS = [
'[data-message-author-role="assistant"]',
'.chat-message:not([data-message-author-role="user"])',
'.message-content'
];
/**
* Normalize text by removing carriage returns and zero-width spaces, squashing trailing
* whitespace before newlines, and trimming ends. Keeps a consistent basis for hashing.
* @param {string} s
* @returns {string}
*/
function norm(s){ return (s||'').replace(/\r/g,'').replace(/\u200b/g,'').replace(/[ \t]+\n/g,'\n').trim(); }
/**
* Fast, low-collision string hash. djb2 variant using XOR; returns unsigned base36 string.
* @param {string} s
* @returns {string}
*/
function hash(s){ let h=5381; for(let i=0;i<s.length;i++) h=((h<<5)+h)^s.charCodeAt(i); return (h>>>0).toString(36); }
/**
* Extract the most relevant, command-like text from a message element.
* Preference order:
* - Any code/pre blocks that appear to contain a valid @bridge@ ... @end@ command with `action:`
* - Otherwise, fall back to the element's textContent (first 2000 chars), normalized.
* @param {Element} el
* @returns {string}
*/
function commandLikeText(el){
const blocks = el.querySelectorAll('pre code, pre, code');
for (const b of blocks) {
const t = norm(b.textContent || '');
// Must look like a complete runnable command block
if (/@end@\s*$/m.test(t) && /(^|\n)\s*@bridge@\b/m.test(t) && /(^|\n)\s*action\s*:/m.test(t)) return t;
}
return norm((el.textContent || '').slice(0, 2000));
}
/**
* Build a hash of the immediate previous assistant messages' trailing text (up to 2000 chars).
* This captures conversational context that helps distinguish repeated content.
* @param {Element} el - The current message element
* @returns {string} base36 hash of the context window
*/
function prevContextHash(el) {
const list = Array.from(document.querySelectorAll(MSG_SELECTORS.join(',')));
const idx = list.indexOf(el); if (idx <= 0) return '0';
@ -78,12 +29,6 @@
return hash(buf.slice(-2000));
}
/**
* Compute a hash of the text immediately preceding the first command block within this element.
* If there is no @bridge@ block, hashes the trailing slice of the whole element text.
* @param {Element} el
* @returns {string}
*/
function intraPrefixHash(el){
const t = el.textContent || '';
const m = t.match(/@bridge@[\s\S]*?@end@/m);
@ -91,12 +36,6 @@
return hash(norm(t.slice(Math.max(0, endIdx - 2000), endIdx)));
}
/**
* Produce a tiny DOM hint string (tag#id.class) to help separate identical content in different
* containers. Limited to 40 chars and later hashed before inclusion in the final fingerprint.
* @param {Element} node
* @returns {string}
*/
function domHint(node) {
if (!node) return '';
const id = node.id || '';
@ -104,14 +43,6 @@
return `${node.tagName || ''}#${id}.${cls}`.slice(0, 40);
}
/**
* Determine the ordinal index (0-based) of this element among all message elements that share
* the same content/context key on the page. This disambiguates duplicates that have identical
* ch+ph+ih values by adding their order of appearance.
* @param {Element} el
* @param {string} key - The key built from ch|ph|ih for this element
* @returns {number}
*/
function ordinalForKey(el, key) {
const list = Array.from(document.querySelectorAll(MSG_SELECTORS.join(',')));
let n = 0;
@ -130,12 +61,6 @@
return n;
}
/**
* Generate a composite fingerprint for the given message element.
* Format: "ch:<..>|ph:<..>|ih:<..>|hint:<..>|n:<ordinal>"
* @param {Element} el
* @returns {string}
*/
function fingerprintElement(el){
const ch = hash(commandLikeText(el).slice(0, 2000));
const ph = prevContextHash(el);
@ -146,12 +71,6 @@
return `${key}|hint:${dh}|n:${n}`;
}
/**
* Retrieve (and cache) a stable fingerprint for the element. The first time it's computed,
* the value is stored in el.dataset.aiRcStableFp so subsequent calls don't recompute.
* @param {Element} el
* @returns {string}
*/
function getStableFingerprint(el) {
if (el?.dataset?.aiRcStableFp) return el.dataset.aiRcStableFp;
const fp = fingerprintElement(el);
@ -159,7 +78,7 @@
return fp;
}
// Expose both for backward compatibility with older modules that expect these globals
// Expose both for backward compatibility
window.AI_REPO_FINGERPRINT = fingerprintElement;
window.AI_REPO_STABLE_FINGERPRINT = getStableFingerprint;
})();

View File

@ -1,52 +1,6 @@
// ==LOGGER START==
(function () {
/**
* AI Repo Commander Logger module
*
* Purpose
* - Provide structured, level-gated logging for all modules.
* - Buffer recent log entries for the in-page debug panel and copy/export.
* - Prevent log spam from hot paths via logLoop.
*
* Integration
* - Exposed as window.AI_REPO_LOGGER for use across modules.
* - Uses window.AI_REPO_CONFIG for runtime toggles and limits:
* - debug.enabled: boolean gate for all logging
* - debug.level: 05 (0=off, 1=error, 2=warn, 3=info, 4=verbose, 5=trace)
* - debug.watchMs: time window used by logLoop for anti-spam
* - debug.maxLines: max entries retained in memory buffer
*
* Console format
* - Each entry prints to the browser console as: [AI RC <LEVEL>] <message> <data?>
* - Data is sanitized to avoid dumping large strings or DOM nodes.
*
* Notes
* - No external dependencies, works in plain browser context.
* - This module does not persist logs; it keeps an in-memory ring buffer only.
*
* Example
* const log = window.AI_REPO_LOGGER;
* log.info('Starting');
* log.warn('Slow operation', { ms: 1234 });
* log.error('Failed', { error: e.message });
* console.log(log.getRecentLogs(100));
*/
/**
* Structured logger with in-memory buffer and anti-spam utilities.
* Fields:
* - config: ConfigManager; access to debug.* keys
* - buffer: Array<{timestamp, level, message, data}> recent log entries
* - loopCounts: Map used by logLoop to cap repeated messages
* - startedAt: number (ms) reference time for logLoop watch window
*/
class Logger {
/**
* Initializes the logger.
* - Grabs the global config instance.
* - Prepares the in-memory buffer and loop counters.
* - Starts a periodic cleanup that resets logLoop counters after ~2× watch window.
*/
constructor() {
this.config = window.AI_REPO_CONFIG;
this.buffer = [];
@ -63,8 +17,6 @@
}, this.config.get('debug.watchMs') || 120000);
}
// Convenience level helpers. Gated by debug.enabled and debug.level (see _log).
// Levels: 1=ERROR, 2=WARN, 3=INFO, 4=VERBOSE, 5=TRACE
error(msg, data) { this._log(1, 'ERROR', msg, data); }
warn(msg, data) { this._log(2, 'WARN', msg, data); }
info(msg, data) { this._log(3, 'INFO', msg, data); }
@ -128,24 +80,6 @@
else this.info(`${msg}${suffix}`);
}
/**
* Core logging sink. Applies gating, buffers the entry, and prints to console.
*
* Gating
* - If debug.enabled is false, nothing is logged.
* - If levelNum > debug.level, the entry is ignored.
*
* Buffering
* - Recent entries kept in memory (debug.maxLines ring buffer) for the debug panel and copy/export.
*
* Console output
* - Printed as: [AI RC <LEVEL>] message [sanitized data]
*
* @param {number} levelNum - Numeric level (1..5)
* @param {string} levelName - Label shown in output (ERROR/WARN/INFO/VERBOSE/TRACE)
* @param {any} msg - Primary message; coerced to string
* @param {any} [data] - Optional context data; sanitized to avoid huge strings/DOM elements
*/
_log(levelNum, levelName, msg, data) {
const enabled = !!this.config.get('debug.enabled');
const level = this.config.get('debug.level') ?? 0;
@ -165,19 +99,6 @@
entry.data ? console.log(prefix, msg, entry.data) : console.log(prefix, msg);
}
/**
* Best-effort redaction/sanitization of context data before logging.
* - HTMLElement "HTMLElement<TAG>" (prevents dumping live DOM trees)
* - Long strings (>200 chars) truncated with ellipsis to keep logs concise
* - Plain objects shallowly sanitize each value using the same rules
* - Other primitives are returned as-is
*
* This keeps console output readable and reduces accidental leakage of
* large payloads while still conveying useful context.
*
* @param {any} data
* @returns {any} sanitized data suitable for console/JSON
*/
_sanitize(data) {
if (!data) return null;
if (data instanceof HTMLElement) return `HTMLElement<${data.tagName}>`;
@ -193,29 +114,12 @@
return data;
}
/**
* Returns the most recent N logs as plain text, one entry per line.
* Each line format: ISO_TIMESTAMP LEVEL message {jsonData?}
*
* @param {number} [n=50] - Number of lines to include from the tail of the buffer
* @returns {string}
*/
getRecentLogs(n = 50) {
return this.buffer.slice(-n).map(e =>
`${e.timestamp} ${e.level.padEnd(7)} ${e.message}${e.data ? ' ' + JSON.stringify(e.data) : ''}`
).join('\n');
}
/**
* Sets the runtime log level in config (05) and logs the change.
* Levels:
* 0=off, 1=error, 2=warn, 3=info, 4=verbose, 5=trace
*
* Note: The debug panel level selector typically writes to the same key.
* This method is provided for console/automation convenience.
*
* @param {number} n - Desired level; values are clamped to [0,5]
*/
setLevel(n) {
const lv = Math.max(0, Math.min(5, n));
this.config.set('debug.level', lv);

View File

@ -1,10 +1,4 @@
// ==MAIN START==
// Module: main.js
// Purpose: Legacy entry point and convenience API exposure.
// - Initializes observer and optionally scans existing messages
// - Exposes window.AI_REPO with pause/resume/clearHistory helpers
// Note: detector.js implements the primary monitoring pipeline; this module
// remains for compatibility and console convenience.
(function () {
'use strict';

View File

@ -1,13 +1,5 @@
// ==PASTE SUBMIT START==
// Module: paste-submit.js
// Depends on: config.js, logger.js
// Purpose: Paste text into the site's chat composer (ChatGPT/Claude/Gemini styles)
// and optionally auto-submit. Provides multiple robust strategies for editors.
// Flow:
// - findComposer(): locate the editable area
// - pasteInto(el, text): try a series of paste strategies from cleanest to fallback
// - findSendButton(scopeEl): locate a send/submit button near the editor
// - submitToComposer(text): paste and, if enabled, click Send or press Enter
/* global GM_setClipboard */
(function () {
const cfg = () => window.AI_REPO_CONFIG;

View File

@ -1,18 +1,6 @@
// ==QUEUE START==
// Module: queue.js
// Depends on: config.js, logger.js
// Purpose: Rate-limited FIFO queue to serialize command execution.
// - Enforces min delay between tasks (queue.minDelayMs)
// - Caps tasks started per rolling minute (queue.maxPerMinute)
// - Drains asynchronously; push is fire-and-forget
(function () {
/**
* ExecutionQueue
* push(task): enqueue an async function to run later
* clear(): drop all pending tasks
* size(): return queue length
* Internals: _drain() runs tasks while respecting rate limits
*/
class ExecutionQueue {
constructor(opts = {}) {
const cfg = window.AI_REPO_CONFIG;

View File

@ -1,18 +1,6 @@
// ==RESPONSE BUFFER START==
// Module: response-buffer.js
// Depends on: config.js, logger.js, queue.js, paste-submit.js
// Purpose: Collect result chunks (e.g., from get_file/list_files) and paste them
// into the chat composer in a controlled way.
// - Buffers multiple sections briefly to batch them
// - Splits very long payloads while respecting code fences
// - Enqueues paste operations on the ExecutionQueue
(function () {
/**
* Split a string by an approximate character limit, preferring to break at newline boundaries.
* @param {string} s
* @param {number} limit
* @returns {string[]}
*/
function chunkByLines(s, limit) {
const out = []; let start = 0;
while (start < s.length) {

View File

@ -1,15 +1,5 @@
// ==STORAGE START==
// Module: storage.js
// Purpose: Conversation-aware de-duplication of executed commands using localStorage.
// - Creates a per-conversation key (host + path) to avoid cross-thread pollution
// - Stores fingerprints of message elements (plus command index) with timestamps
// - Provides isProcessed/markProcessed helpers and TTL cleanup
(function () {
/**
* ConversationHistory
* Tracks which commands (by message fingerprint + index) have been executed
* in the current conversation. Uses a 30-day TTL for records.
*/
class ConversationHistory {
constructor() {
this.conversationId = this._getConversationId();

View File

@ -1,5 +1,4 @@
// ==UserScript==
// Bootstrap loader: this userscript pulls in all modules via @require in dependency order.
// @name AI Repo Commander (Full Features)
// @namespace http://tampermonkey.net/
// @version 2.1.0