Create new OpenCode agents with a gpt-5.2-codex default.
日本語の概要は準備中です。原文の説明を表示しています。
Create an auditable Markdown report for any uploaded Microsoft Excel (.xlsx) or CSV files in an Openrind Shell sandbox.
インストール方法を見るインストールする前に、エージェントに与えられる指示の中身を確認できます。
CRITICAL INSTRUCTION FOR CLAUDE:
Task Output or wait for skill_run).Bash command in Step 2 to generate the parsed data and then write the final report.python3 (built-in zipfile, xml.etree.ElementTree, csv, math, statistics, json, datetime) is used directly.which libreoffice, which csvkit, which unzip, or apt list.pip install, uv pip install, or attempt downloading external packages. The built-in Python script parses .xlsx OpenXML and .csv natively, offline, and in milliseconds.[REDACTED]). Row-level samples must be redacted, and do not dump raw row-level data or full reports in chat when row-level or sensitive records are present.Determine the inbox directory and find the target file(s):
if mountpoint -q /sandbox/work 2>/dev/null; then
INBOX=/sandbox/work/inbox
elif [ -d "/sandbox/inbox" ]; then
INBOX=/sandbox/inbox
else
INBOX="."
fi
mkdir -p "$INBOX"
If specific file path(s) were given in the user prompt or skill argument, use those. Otherwise, discover all workbooks dynamically:
find "$INBOX" /sandbox . -maxdepth 3 -type f \( -iname '*.xlsx' -o -iname '*.csv' \) 2>/dev/null | sort -u
Create /tmp/openrind-excel-parse.py and run it against the target file(s) with python3:
cat << 'EOF' > /tmp/openrind-excel-parse.py
import sys, os, zipfile, csv, json, re, math, statistics, datetime, io
from pathlib import Path
import xml.etree.ElementTree as ET
from collections import Counter
MAX_FILE = 10 * 1024 * 1024
MAX_ROWS, MAX_COLS, MAX_CELLS = 10000, 256, 250000
csv.field_size_limit(65536)
def check_file(path):
if Path(path).stat().st_size > MAX_FILE:
raise ValueError('Input exceeds the 10 MiB safety limit')
def check_shape(rows, cols):
if rows > MAX_ROWS or cols > MAX_COLS or rows * cols > MAX_CELLS:
raise ValueError('Sheet exceeds safe row, column, or cell limits')
def col_letter_to_index(col_str):
idx = 0
for char in col_str.upper():
if 'A' <= char <= 'Z':
idx = idx * 26 + (ord(char) - ord('A') + 1)
return idx - 1
def index_to_col_letter(idx):
res = []
idx += 1
while idx > 0:
idx, rem = divmod(idx - 1, 26)
res.append(chr(ord('A') + rem))
return ''.join(reversed(res))
def parse_cell_ref(cell_ref):
m = re.match(r'([A-Za-z]+)([0-9]+)', cell_ref)
if m:
return col_letter_to_index(m.group(1)), int(m.group(2))
return None, None
def is_date_format_code(code):
if not code:
return False
cleaned = re.sub(r'"[^"]*"', '', code)
cleaned = re.sub(r'\[(?!\s*[hmsHMS]\s*\])[^\]]*\]', '', cleaned)
cleaned = re.sub(r'\\.', '', cleaned)
cleaned = re.sub(r'[_\*].', '', cleaned)
cl = cleaned.lower()
if re.search(r'[ydhs]|am/pm|a/p', cl):
return True
if re.search(r'(^|[^a-z0-9])m+($|[^a-z0-9])', cl):
return True
return False
def format_excel_date(serial, is_1904=False):
if is_1904:
base = datetime.datetime(1904, 1, 1)
dt = base + datetime.timedelta(days=serial)
else:
base = datetime.datetime(1899, 12, 31) if serial < 60 else datetime.datetime(1899, 12, 30)
dt = base + datetime.timedelta(days=serial)
if dt.microsecond >= 500000:
dt += datetime.timedelta(seconds=1)
if serial < 1 and not is_1904:
return dt.strftime('%H:%M:%S')
if dt.hour == 0 and dt.minute == 0 and dt.second == 0:
return dt.strftime('%Y-%m-%d')
return dt.strftime('%Y-%m-%d %H:%M:%S')
def read_xlsx(file_path):
check_file(file_path)
sheets_data = {}
with zipfile.ZipFile(file_path, 'r') as z:
members = z.infolist()
if (len(members) > 128 or sum(m.file_size for m in members) > 32 * 1024 * 1024
or any(m.file_size > 8 * 1024 * 1024 or m.file_size > max(1, m.compress_size) * 200 for m in members)):
raise ValueError('Workbook exceeds ZIP expansion safety limits')
is_1904 = False
if 'xl/workbook.xml' in z.namelist():
wb_tree = ET.fromstring(z.read('xl/workbook.xml'))
wb_pr = wb_tree.find('.//{*}workbookPr')
if wb_pr is not None and wb_pr.get('date1904') in ('1', 'true', 'True'):
is_1904 = True
custom_num_fmts = {}
cell_xfs_num_fmt_ids = []
if 'xl/styles.xml' in z.namelist():
styles_tree = ET.fromstring(z.read('xl/styles.xml'))
for num_fmt in styles_tree.findall('.//{*}numFmt'):
fmt_id_str = num_fmt.get('numFmtId')
fmt_code = num_fmt.get('formatCode')
if fmt_id_str is not None and fmt_code is not None:
try:
custom_num_fmts[int(fmt_id_str)] = fmt_code
except ValueError:
pass
for xf in styles_tree.findall('.//{*}cellXfs/{*}xf'):
num_fmt_id_str = xf.get('numFmtId', '0')
try:
cell_xfs_num_fmt_ids.append(int(num_fmt_id_str))
except ValueError:
cell_xfs_num_fmt_ids.append(0)
builtin_date_fmt_ids = {
14, 15, 16, 17, 18, 19, 20, 21, 22,
27, 28, 29, 30, 31, 32, 33, 34, 35, 36,
45, 46, 47, 50, 51, 52, 53, 54, 55, 56, 57, 58
}
date_style_indices = set()
for idx, fmt_id in enumerate(cell_xfs_num_fmt_ids):
if fmt_id in builtin_date_fmt_ids:
date_style_indices.add(idx)
elif fmt_id in custom_num_fmts and is_date_format_code(custom_num_fmts[fmt_id]):
date_style_indices.add(idx)
shared_strings = []
if 'xl/sharedStrings.xml' in z.namelist():
tree = ET.fromstring(z.read('xl/sharedStrings.xml'))
for si in tree.findall('.//{*}si'):
texts = [t.text for t in si.findall('.//{*}t') if t.text is not None]
shared_strings.append(''.join(texts))
rel_map = {}
if 'xl/_rels/workbook.xml.rels' in z.namelist():
rels_tree = ET.fromstring(z.read('xl/_rels/workbook.xml.rels'))
for rel in rels_tree.findall('.//{*}Relationship'):
r_id = rel.get('Id')
target = rel.get('Target', '')
if target.startswith('/'):
target = target[1:]
if not target.startswith('xl/'):
target = 'xl/' + target
rel_map[r_id] = target
sheets = []
if 'xl/workbook.xml' in z.namelist():
wb_tree = ET.fromstring(z.read('xl/workbook.xml'))
for idx, sheet_el in enumerate(wb_tree.findall('.//{*}sheet')):
name = sheet_el.get('name') or f'Sheet{idx+1}'
r_id = (sheet_el.get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id')
or sheet_el.get('r:id') or sheet_el.get('id'))
target = rel_map.get(r_id, f'xl/worksheets/sheet{idx+1}.xml')
sheets.append({'name': name, 'path': target})
if not sheets:
for name in z.namelist():
if name.startswith('xl/worksheets/sheet') and name.endswith('.xml'):
sheets.append({'name': Path(name).stem, 'path': name})
for s in sheets:
path = s['path']
if path not in z.namelist():
alt = 'xl/' + path if not path.startswith('xl/') else path[3:]
if alt in z.namelist():
path = alt
else:
continue
ws_tree = ET.fromstring(z.read(path))
rows_data = []
max_col = 0
for row_el in ws_tree.findall('.//{*}row'):
row_dict = {}
for c_el in row_el.findall('.//{*}c'):
ref = c_el.get('r')
t = c_el.get('t', 'n')
style_attr = c_el.get('s')
style_idx = int(style_attr) if style_attr and style_attr.isdigit() else 0
is_date_cell = style_idx in date_style_indices
val = None
if t == 's':
v_el = c_el.find('.//{*}v')
if v_el is not None and v_el.text:
try:
s_idx = int(v_el.text)
if s_idx < len(shared_strings):
val = shared_strings[s_idx]
except ValueError:
val = v_el.text
elif t == 'inlineStr':
texts = [t_el.text for t_el in c_el.findall('.//{*}t') if t_el.text is not None]
val = ''.join(texts) if texts else None
elif t == 'b':
v_el = c_el.find('.//{*}v')
val = (v_el.text == '1') if v_el is not None and v_el.text else False
elif t == 'd':
v_el = c_el.find('.//{*}v')
val = v_el.text if v_el is not None else None
elif t in ('str', 'e'):
v_el = c_el.find('.//{*}v')
val = v_el.text if v_el is not None else None
else:
v_el = c_el.find('.//{*}v')
if v_el is not None and v_el.text:
raw = v_el.text
try:
f_val = float(raw)
if is_date_cell:
try:
val = format_excel_date(f_val, is_1904)
except Exception:
val = int(f_val) if f_val.is_integer() else f_val
else:
val = int(f_val) if f_val.is_integer() else f_val
except ValueError:
val = raw
col_idx = None
if ref:
col_idx, _ = parse_cell_ref(ref)
if col_idx is not None:
check_shape(len(rows_data) + 1, col_idx + 1)
row_dict[col_idx] = val
if col_idx + 1 > max_col:
max_col = col_idx + 1
rows_data.append(row_dict)
check_shape(len(rows_data), max_col)
matrix = []
for r in rows_data:
row = [r.get(c, None) for c in range(max_col)]
matrix.append(row)
sheets_data[s['name']] = matrix
return sheets_data
def read_csv(file_path):
check_file(file_path)
encodings = ['utf-8-sig', 'utf-8', 'latin-1', 'cp1252']
raw_bytes = Path(file_path).read_bytes()
text = None
for enc in encodings:
try:
text = raw_bytes.decode(enc)
break
except UnicodeDecodeError:
continue
if text is None:
text = raw_bytes.decode('utf-8', errors='replace')
try:
dialect = csv.Sniffer().sniff(text[:4096])
delimiter = dialect.delimiter
except Exception:
delimiter = ','
reader = csv.reader(io.StringIO(text), delimiter=delimiter)
matrix = []
for row in reader:
check_shape(len(matrix) + 1, len(row))
converted = []
for cell in row:
val = cell.strip()
converted.append(val if val != '' else None)
matrix.append(converted)
return {'CSV Data': matrix}
def is_sensitive_name(name):
n = str(name).lower()
patterns = ['password', 'secret', 'token', 'key', 'ssn', 'auth', 'credential', 'credit_card', 'card_number', 'api_key', 'private', 'bearer', 'cvv', 'pin']
return any(p in n for p in patterns)
def analyze_matrix(matrix):
if not matrix:
return {'total_rows': 0, 'total_cols': 0, 'duplicate_rows': 0, 'headers': [], 'sample': [], 'columns': {}}
raw_headers = [str(c) if c is not None and str(c).strip() != '' else f'Column_{i+1}' for i, c in enumerate(matrix[0])]
data_rows = matrix[1:]
total_rows = len(data_rows)
total_cols = len(raw_headers)
row_tuples = [tuple(r) for r in data_rows]
dup_count = len(row_tuples) - len(set(row_tuples)) if total_rows > 0 else 0
seen_header_counts = {}
unique_keys = []
for h in raw_headers:
if h in seen_header_counts:
seen_header_counts[h] += 1
unique_keys.append(f"{h}_{seen_header_counts[h]}")
else:
seen_header_counts[h] = 1
unique_keys.append(h)
# Default-deny value disclosure: arbitrary notes, identifiers, and numeric
# account/card values cannot be reliably classified using header names.
sensitive_cols = set(range(len(raw_headers)))
redacted_sample = []
for r in data_rows[:5]:
redacted_sample.append([('[REDACTED]' if c_idx in sensitive_cols else (r[c_idx] if c_idx < len(r) else None)) for c_idx in range(len(raw_headers))])
columns_info = {}
for col_idx, (col_name, col_key) in enumerate(zip(raw_headers, unique_keys)):
values = [r[col_idx] if col_idx < len(r) else None for r in data_rows]
non_null_values = [v for v in values if v is not None and v != '']
null_count = total_rows - len(non_null_values)
null_pct = (null_count / total_rows * 100) if total_rows > 0 else 0.0
unique_count = len(set(non_null_values))
numeric_vals = []
for v in non_null_values:
if isinstance(v, (int, float)) and not isinstance(v, bool):
numeric_vals.append(float(v))
elif isinstance(v, str):
try:
numeric_vals.append(float(v.strip().replace(',', '')))
except ValueError:
pass
is_numeric = len(numeric_vals) == len(non_null_values) and len(non_null_values) > 0
col_info = {
'header': col_name,
'col_index': col_idx,
'col_letter': index_to_col_letter(col_idx),
'dtype': 'numeric' if is_numeric else 'text',
'missing_count': null_count,
'missing_pct': round(null_pct, 2),
'unique_count': unique_count
}
if col_idx in sensitive_cols:
col_info['values_withheld'] = True
elif is_numeric and numeric_vals:
numeric_vals.sort()
n = len(numeric_vals)
mean_val = statistics.mean(numeric_vals)
std_val = statistics.stdev(numeric_vals) if n > 1 else 0.0
def percentile(s, p):
idx = (len(s) - 1) * p
l, u = int(math.floor(idx)), int(math.ceil(idx))
return s[l] if l == u else s[l] * (u - idx) + s[u] * (idx - l)
q1, q3 = percentile(numeric_vals, 0.25), percentile(numeric_vals, 0.75)
iqr = q3 - q1
outliers = [v for v in numeric_vals if v < (q1 - 1.5*iqr) or v > (q3 + 1.5*iqr)]
col_info['stats'] = {
'count': n, 'sum': round(sum(numeric_vals), 2), 'mean': round(mean_val, 2), 'std': round(std_val, 2),
'min': round(numeric_vals[0], 2), 'q1': round(q1, 2), 'median': round(statistics.median(numeric_vals), 2),
'q3': round(q3, 2), 'max': round(numeric_vals[-1], 2), 'iqr': round(iqr, 2), 'outlier_count': len(outliers)
}
else:
if col_idx in sensitive_cols:
col_info['top_values'] = [{'value': '[REDACTED]', 'count': len(non_null_values), 'pct': 100.0}]
else:
counts = Counter([str(v) for v in non_null_values]).most_common(5)
col_info['top_values'] = [{'value': k, 'count': c, 'pct': round(c/len(non_null_values)*100, 1) if non_null_values else 0} for k, c in counts]
columns_info[col_key] = col_info
return {'total_rows': total_rows, 'total_cols': total_cols, 'duplicate_rows': dup_count, 'headers': raw_headers, 'sample': redacted_sample, 'columns': columns_info}
def process_file(p):
path = Path(p)
if not path.exists(): return None
sfx = path.suffix.lower()
if sfx in ('.xlsx', '.xlsm', '.xltx'):
sheets = read_xlsx(path); fmt = 'Excel Workbook'
elif sfx in ('.csv', '.tsv', '.txt'):
sheets = read_csv(path); fmt = 'CSV'
else: return None
return {'filename': path.name, 'path': str(path.absolute()), 'size_bytes': path.stat().st_size, 'format': fmt, 'sheets': {k: {'stats': analyze_matrix(v)} for k, v in sheets.items()}}
target_files = sys.argv[1:]
results = []
for f in target_files:
res = process_file(f)
if res is not None:
results.append(res)
print(json.dumps(results, indent=2))
EOF
Run against the target file(s):
python3 /tmp/openrind-excel-parse.py "<path_to_file1>" ["<path_to_file2>" ...] > /tmp/parsed-excel.json
Read /tmp/parsed-excel.json and generate $INBOX/analysis-report.md with:
Tie each finding to a named workbook, sheet, and column. Distinguish observed facts from inference. Ensure secrets, credentials, and unnecessary personal data are never printed in plaintext; redact sensitive columns and values. After writing the file, tell the user its exact path. Present an executive summary and high-level findings in chat; do not require or dump the full report in chat when it contains row-level data or sensitive details.
まだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
Create new OpenCode agents with a gpt-5.2-codex default.
日本語の概要は準備中です。原文の説明を表示しています。
Guide users through browser automation setup using Chrome DevTools MCP only. Use when the user asks to set up browser automation, Chrome DevTools MCP, browser MCP, or runs the browser-setup command.
日本語の概要は準備中です。原文の説明を表示しています。
Manages Cargo.lock file updates and resolves --locked flag issues in CI/CD. Triggers when user mentions: - "cargo test --locked failed" - "cannot update the lock file" - "Cargo.lock is out of date" - "PR failed with --locked error" - "fix Cargo.lock"
日本語の概要は準備中です。原文の説明を表示しています。
Create OpenCode custom commands for repeatable tasks.
日本語の概要は準備中です。原文の説明を表示しています。
Guide users through the get started setup and Chrome DevTools demo.
日本語の概要は準備中です。原文の説明を表示しています。
Bridge between Openrind Desktop UI and OpenCode runtime
日本語の概要は準備中です。原文の説明を表示しています。