AI Workspace Hub
Vault template
First committed by Benboerba620
Published in Benboerba620/ai-workspace-hub. Publisher page
83 GitHub stars
What it is
All-in-one AI 研究工作台:把 Codex / Claude Code / Cursor 变成有长期记忆的研究系统。wiki 摄入、研究闭环、快速筛选、假设追踪、播客、日报六大能力,零 API key 可跑基座。
Summary from the brain’s metadata
All-in-one AI 研究工作台:把 Codex / Claude Code / Cursor 变成有长期记忆的研究系统。wiki 摄入、研究闭环、快速筛选、假设追踪、播客、日报六大能力,零 API key 可跑基座。 Turn Codex, Claude Code, Cursor, and Cline into a persistent research workspace.
How it works
Memory model: Markdown vault
AutoGineer overview, from the brain’s own files
Memory model: Markdown vault. Folders: .github (agent config, 1 file); docs (docs, 2 files); examples (examples, 22 files); inbox (raw sources, 3 files); tests (tests, 18 files); tools (scripts, 39 files); wiki (wiki, 15 files). The agent reads hypothesis/, wiki/explorations/.
| Folder | Role | Files |
|---|---|---|
| .github | agent-config | 1 |
| docs | docs | 2 |
| examples | examples | 22 |
| inbox | raw-sources | 3 |
| tests | tests | 18 |
| tools | scripts | 39 |
| wiki | wiki | 15 |
| config | folder | 2 |
| evidence | folder | 1 |
| hypothesis | folder | 1 |
| output | folder | 7 |
| portfolio | folder | 2 |
| system | folder | 52 |
| workspace | folder | 11 |
The agent reads
- hypothesis/ evidence: AGENTS.md
- wiki/explorations/ evidence: AGENTS.md
Requirements
Runtimes: Python
Works with: Claude Code, OpenAI Codex, GitHub Copilot, Cursor, Kiro, OpenCode, Amp, Cline, JetBrains Junie, Devin (ex-Windsurf)
Set it up
Clone the repository at the commit AutoGineer scanned:
git clone https://github.com/Benboerba620/ai-workspace-hub.git && cd ai-workspace-hub && git checkout e01a41a06886c6d59f714e6c133004d241ea91acClaude Code reads CLAUDE.md from this folder automatically.
cd ai-workspace-hub && claude
AutoGineer scanned these files at commit e01a41a.
First committed by Benboerba620. Published in Benboerba620/ai-workspace-hub. Licence: MIT License.
Usage
AutoGineer’s guidance for this type of brain
Copy the vault into a folder, open it in your notes app, and point your agent at its instruction file: the agent reads and writes the notes as its memory.
Documentation by Benboerba620
<div align="center">
AI Workspace Hub
Turn Codex, Claude Code, Cursor, and Cline into a persistent research workspace.
把 AI 编程助手变成一套能长期记忆、持续研究、自动沉淀的工作台。
Image: CI Image: Python 3.10+ Image: GitHub stars Image: License
30 秒上手 · 六大能力 · 免费数据源 · 第一周怎么用 · 故障排查
Image: 完整流程演示:一句话安装 → Core Mode READY → 首次股票研究 → 研究偏好 → 主题假设 → 日报跟踪
<sub>演示动图按安装协议与 <a href="START-HERE.md">首次股票研究流程</a>脚本化生成(<a href="docs/gen_demo_gif.py">生成脚本</a>),非实录。</sub>
</div>
最近更新(2026-09-30):当前 Agent 可直接完成材料摘要与标签;日报显示数据来源、日期和缺失状态;安装附带免费数据源指南,daily-watch 与 podcast 均已内置。
对话很聪明,但工作流没有记忆;今天做完,明天又从头解释。
AI Workspace Hub 解决这个问题。六大能力开箱即用——核心工作流不需要 API key。
📖 想先了解设计思路?看作者的公众号长文:《从0构建 AI 协作系统(一):从最小可运行的 MVP 开始》
⚡ 30 秒上手
把下面这句话发给你的 AI agent(Codex / Claude Code / Cursor / Cline):
帮我按这个协议安装 AI Workspace Hub:
https://raw.githubusercontent.com/Benboerba620/ai-workspace-hub/main/INSTALL-FOR-AI.md
抓不到协议全文就先 git clone 本仓库,再读其中的 INSTALL-FOR-AI.md 逐字执行。Agent 会问你 4 个问题(路径、用途、现有 wiki、是否使用 Obsidian),然后自动创建完整工作区。装好后,先用样例确认安装:
把 inbox/first-note.md 整理进 personal wiki。预期:agent 读 AGENTS.md → 按 wiki/_schema.md 整理 → 写入 wiki/sources/ → 在 active-context.md 记录进度。
不需要 API key,不需要联网,不需要写任何代码。
样例通过后,开始第一份真实股票研究:
研究一家公司,同时帮我建立第一版研究偏好。
公司是:____;我现在关注它是因为:____。这次会同时产出研究报告、研究偏好 v0.1、可验证假设和后续跟踪指标。以后研究其他公司会复用并继续校准这套方法,而不是每次从空白开始。
<details> <summary>还没有 AI agent?先花 2 分钟装一个</summary>
任选其一,都有免费或试用档:
Claude Code(推荐):先装 Node.js,然后终端运行
npm install -g @anthropic-ai/claude-code,在任意目录输入claude登录 Claude 账号。Codex CLI:
npm install -g @openai/codex,输入codex登录 ChatGPT 账号。Cursor:去 cursor.com 下载安装,打开一个文件夹后用内置对话。
装好后回到上面,把那句话发给它就行。
</details>
<details> <summary>更习惯手动 clone?</summary>
git clone https://github.com/Benboerba620/ai-workspace-hub.git my-ai-workspace
cd my-ai-workspace用 AI agent 打开这个目录,直接试跑 inbox/first-note.md → wiki。
验证安装状态:
python3 system/scripts/check_workspace.py # Windows: python system/scripts/check_workspace.pyCore Mode 显示 READY 即可开始使用。
</details>
📈 股票研究主流程
首次研究引导 → 研究偏好 v0.1 → 研究结论 → 可验证假设
→ 跟踪指标 → daily-watch 日报 → 独立证据 → 假设复盘 → 长期知识START-HERE.md 是安装后的用户首页。信息摄入只是输入环节;系统的主线是让研究结论进入假设,并通过日报持续验证。
每份研究、假设和证据都有稳定 ID;当前状态只保存在 Markdown frontmatter。需要用户确认的 wiki 沉淀、假设调整、股票池新增和研究偏好变化会进入持久待确认队列。直接说:
看看我的研究系统现在有什么需要处理。Agent 会汇总进行中的研究、待复盘证据、到期假设和待确认动作,并给出下一步。
🧩 六大能力
| 能力 | 你可以怎么说 | 写入哪里 | API key | |
|---|---|---|---|---|
| 📚 | wiki | "把这篇文章整理进知识库" | wiki/ | 推荐(自动标签) |
| 🔬 | research | "帮我研究一下某公司 / 某行业" | output/research/ | 不需要 |
| 🔍 | screen | "帮我筛选 AI 产业链股票" | output/screen/ | 可选 |
| 📊 | daily-watch | "生成今天的盯盘日报" | output/daily-watch/ + evidence/ | 可选 |
| 🧪 | hypothesis | "把这个投资假设建档并追踪" | hypothesis/ | 不需要 |
| 🎙️ | podcast | "扫一下这几个播客并写进 wiki" | wiki/sources/ + output/pod2wiki/ | 当前 Agent 无需独立 Key;后台摘要需 Key |
daily-watch 和 podcast 已经内置在 Hub 中,不需要再单独安装。 后台自动标签、独立运行的播客摘要和部分行情源需要按需填写 API key;交互整理可由当前 Agent 完成。
零 key 可用:wiki 可由当前 Agent 摄入和补齐有依据的标签,不调用独立模型 API;research、hypothesis、screen(websearch 模式)、daily-watch(报告骨架 + 美股降级源)照常可用。
默认建议配置:先按 免费数据源接入指南 选择市场——美股可零 Key 使用 Nasdaq,A 股可从 Tushare 的基础日线档起步,yfinance 可作为个人研究备用;再按需配置 LLM、FMP 或 Longbridge Skill。
🔄 工作流程
flowchart LR
IN["inbox / web"] --> TAG["统一自动标签"]
TAG --> KB["wiki 知识库"]
KB --> R["research R*"]
R --> H["hypothesis H*"]
H --> W["唯一执行股票池"]
W --> D["daily-watch 日报"]
D --> E["evidence E-*"]
E -->|"用户确认后复盘"| H
R --> Q["review-queue Q-*"]
H --> Q
Q -->|"确认后执行"| KB
S["workspace status"] -.-> R
S -.-> H
S -.-> E
S -.-> Q核心不是"文件夹长什么样",而是:agent 进入目录后知道先读什么、做研究时先查本地 wiki、输出时事实与推测分开、证据和判断不混写、待确认动作不会随对话消失、明天继续时能从断点接上。
wiki 变大后也不会每次全量加载。每次研究先运行 Research Preflight:按 rule -> pattern -> exploration 加载相关可复用知识,同时扫描 entities / concepts / sources,只返回少量命中页面、命中理由和过期/冲突提示,再决定哪些问题需要联网。扫描不调用大模型,并生成可检查的回执。知识按 source -> exploration -> pattern -> rule 逐级提炼;每次晋级、降级或退役都保留用户确认、复审日期和失效条件。
材料进入 wiki 时按同一标签契约补齐 domain / ticker / concepts / related / entity_salience / tags。交互模式由当前 Agent 处理,后台模式使用统一打标器;历史补标默认只预览,已有人工字段不会被覆盖。
用真实案例理解 Wiki 晋级
产能扩张周期见顶案例 把一条真实研究判断拆成 4 个 Source、2 个已提炼 Exploration 和 1 张 draft Pattern:光伏与锂电是产生该 Pattern 的母案例,因此不能再次冒充独立确认,primary_confirmations 保持为 0。日常研究默认不会加载这张草稿卡;复盘模式可以找到它,未来经过两次新的独立事前验证并由用户确认后,才允许变成 active Pattern。累计三次独立确认后,再讨论是否形成可执行 Rule。
案例目录可以独立运行 summary 和 load,仓库测试同时守住“母案例不计确认、draft 不进入日常加载、满足门槛后才可晋级”三条边界。
系统不在某个模型里,而在这套文件协议里。谁读懂这套协议,谁就接上你的工作流。
⚙️ Core Mode / Enhanced Mode
| 模式 | API key | 能做什么 | 适合 |
|---|---|---|---|
| Core | 不需要 | wiki 摄入(无脚本自动标签)、研究草稿、快速筛选、假设建档、断点续传 | 临时未配置 API 时 |
| Enhanced | 按市场选填 | wiki 自动标签、播客摘要、行情日报、A 股 / 多市场数据、自动监控 | 默认日常使用 |
推荐路径:安装后运行 check_workspace.py → 按 DATA-SOURCES.md 选择免费或付费数据源 → 只填写实际需要的 key。暂时没有 key 时仍可先跑 Core Mode,美股基础行情还可走 Nasdaq 无 Key 源。
📅 第一周怎么用
| 天数 | 动作 | 目标 |
|---|---|---|
| Day 1 | 用当前 Agent 跑通 inbox/first-note.md → wiki;后台自动化再配置 Key | 确认摘要、自动标签和分类都能工作 |
| Day 2 | 放入 3-5 条真实材料 | 建立第一批知识库 |
| Day 3 | 让 agent 研究一个公司 / 行业 | 生成第一篇结构化报告 |
| Day 4 | check_workspace.py,按免费数据源指南选择 Nasdaq / Tushare / yfinance 等 | 增强行情能力 |
| Day 5 | 做一次主题筛选 | 形成候选池 |
| Day 6 | 扫一次播客 / 博客 | 建立外部信息流 |
| Day 7 | 运行结构体检,删掉没用规则 | 防止系统变胖 |
适合谁?
适合:投资研究员、个人投资者、内容创作者、AI power user、Markdown / Obsidian 用户、想做长期项目而不是一次性对话的人。
不适合:想要 GUI 应用、自动交易系统,或不愿意用 Markdown 管理知识的人。
<details> <summary><b>📂 目录结构</b></summary>
ai-workspace-hub/
├── AGENTS.md # Codex 入口路由
├── CLAUDE.md # Claude Code 入口路由
├── INSTALL-FOR-AI.md # 交给 AI agent 的安装协议
├── SMOKE-TEST.md # 冒烟测试
├── ARCHITECTURE.md # 架构说明
├── TROUBLESHOOTING.md # 故障排查
├── workspace/ # 配置、断点、看板与归档
│ ├── monitoring/ # 用户阅读的监控看板
│ └── archive/ # 退出主流程的历史材料
├── inbox/ # 临时输入材料
├── wiki/ # personal wiki(exploration / pattern / rule)
├── output/ # 研究、筛选、播客和日报输出
│ └── daily-watch/ # 日报归档
├── hypothesis/ # 投资假设和复盘
├── evidence/ # 独立证据账本
├── portfolio/ # 交易记录
├── config/ # 用户配置,不入 git
├── tools/ # podcast / daily-watch 工具
└── system/ # skills / integrations / scripts / templates</details>
<details> <summary><b>🗄️ 数据源与边界</b></summary>
| 名称 | 类型 | 用途 | 是否内置 |
|---|---|---|---|
| Nasdaq | 无 key 降级源 | 美股基础行情 | 是 |
| Finnhub | 免费账户 / 付费 | 美股行情备选;免费端点和额度以官方账户为准 | 是,需 key |
| EODHD | 免费小额度 / 付费 | 港股、韩国、芬兰等最近 EOD 备用;免费账户当前 20 次/日,不是实时价 | 是,需 key |
| yfinance | 无 Key | 多市场个人研究备用;非官方 Yahoo SDK,需额外安装 | 是,需开启 |
| tushare | 积分 / 单独权限 | A 股行情;当前 120 积分档可取非复权日线 | 是,需 token |
| FMP | 免费 Basic / 付费 | 免费档适合 EOD/profile 测试;全球覆盖和高级数据取决于套餐 | 是,需 key |
| Longbridge Skill | 外部 Agent 扩展 | 多市场查询、筛选、研究 | 否,独立安装授权 |
| websearch | Agent 能力 | 补充新闻、资料、公司信息 | 取决于 agent |
重要边界:本项目不做自动交易,不承诺数据源永远免费,不把 AI 输出包装成投资建议。缺少 API key 时优雅降级,不会让用户误以为工作区坏了。
</details>
<details> <summary><b>💻 常用命令</b></summary>
python3 system/scripts/check_workspace.py # 总检查
python3 system/scripts/workspace_status.py # 当前研究、证据和待办
python3 system/scripts/research_preflight.py --root . --context "你的研究问题和关键词" --research-id R-... --research-file output/research/报告.md --record # 扫描 Wiki 并更新研究报告
python3 system/scripts/knowledge_lifecycle.py --root . summary # 知识状态、到期复审和晋级候选
python3 system/scripts/knowledge_lifecycle.py --root . rebuild-index --apply # 重建三个短索引
python3 system/scripts/review_queue.py list # 查看持久待确认队列
python3 system/scripts/wiki_tagger.py --root . backfill wiki # 预览历史 wiki 补标
python3 -m unittest discover -s tests -v # 运行测试
python3 tools/daily-watch/scripts/check_setup.py --init # 初始化日报配置
python3 tools/podcast/scripts/fetch_podcasts.py --help # 播客工具帮助Python 工具需要 3.10+。Windows 用户用 python 代替 python3,或 py -3 指定版本。
</details>
<details> <summary><b>❓ 新手常见问题</b></summary>
Q:我不会写代码,能用这个项目吗?
可以。Core Mode 只需要把文本放进 inbox/,然后用自然语言告诉 AI agent 做什么。
Q:一定要用 Codex / Claude Code 吗? 不一定。任何能读写文件的 AI agent 都可以。Codex 和 Claude Code 效果最好,Cursor 和 Cline 也可以。
Q:安装 Hub 后,还要单独安装 daily-watchlist 或 pod2wiki 吗?
不需要。两套工具及其配置模板已经包含在 tools/daily-watch/ 和 tools/podcast/;你只需要按用途安装可选依赖,并按 免费数据源接入指南 选择是否配置 key。
Q:wiki 越来越大怎么办?
用 Obsidian 打开 wiki/ 目录做可视化管理,或让 AI agent 帮你整理归档。
Q:API key 会不会泄露?
config/ 已被 .gitignore 排除,不会被 git 提交。
Q:自动标签会把什么发给大模型?
会把当前 wiki 页面的 frontmatter 和正文发送给你在 config/pod2wiki.env 选择的模型服务商,只用于返回结构化标签。敏感材料应先确认所选服务商的数据政策,或跳过自动标签。
Q:可以和 Obsidian 一起用吗?
可以。安装时选择 Obsidian(或在已有 vault 中安装),Hub 会增加 reading-hub.md 和 reading-hub.base,用 9 个动态视图汇总待读内容;可直接在表格中填写 已读 / 精读 / 跳过,并优先按资料自身日期而不是本机文件时间排序。默认不安装,也不影响非 Obsidian 用户。需要 Obsidian 1.9+ 的 Bases 功能。
更多问题见 TROUBLESHOOTING.md。
</details>
Roadmap
更多真实研究工作流样例
更强的 screen 预设
更完善的 daily-watch 降级策略
证据来源质量分级和跨来源事件去重
知识索引的自动健康检查与过期提示
Contributing
欢迎提 issue / PR,尤其是新 agent 适配、研究报告模板、数据源接入、真实使用摩擦点、README / 教程 / 安装流程改进。
Star History
如果这个项目对你有帮助,欢迎点一下 ⭐ Star。它会帮助更多需要"长期 AI 工作流"的人发现这个项目,也会让我知道哪些方向值得继续做。
Disclaimer
AI Workspace Hub 是个人研究与知识管理工具,不构成投资建议。所有市场数据、公司信息、财务数字、新闻和监管信息都应以原始来源为准。
<!-- 文件说明:项目首页、价值介绍和快速上手。 -->
为什么已经在 Agent 中使用,还可能需要模型 Key?
交互整理可以由当前 Agent 完成:播客工具使用 --agent-handoff 准备原文与待处理队列,Agent 阅读原文后完成摘要和标签,不要求另配模型 Key。脚本在后台独立生成摘要才需要模型 API。两种方式的步骤与完成标准见 Podcast 流程。
system/scripts/research_preflight.py
Shown as plain text.
#!/usr/bin/env python3
"""Build a compact, verifiable local-knowledge package before research."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import sys
import time
from datetime import date, datetime, timezone
from pathlib import Path
from typing import Any
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
import knowledge_lifecycle as lifecycle # noqa: E402
for _stream in (sys.stdout, sys.stderr):
if hasattr(_stream, "reconfigure"):
_stream.reconfigure(encoding="utf-8", errors="replace")
WIKI_SECTIONS = ("entities", "concepts", "sources")
SECTION_TYPES = {"entities": "entity", "concepts": "concept", "sources": "source", "raw": "raw"}
KNOWLEDGE_SECTIONS = {
"exploration": "explorations",
"pattern": "patterns",
"rule": "rules",
}
REVIEW_STATUSES = {
"weakened",
"invalidated",
"retired",
"deprecated",
"archived",
"stale",
"superseded",
}
STOP_TERMS = {
"研究",
"分析",
"公司",
"行业",
"相关",
"问题",
"判断",
"research",
"analysis",
"company",
}
def find_workspace_root(start: Path) -> Path:
return lifecycle.find_workspace_root(start)
def resolve_wiki_root(root: Path, requested: Path | None = None) -> Path:
if requested is not None:
candidate = requested.expanduser()
else:
candidate = Path("wiki")
config = root / "workspace" / "workspace-config.md"
if config.is_file():
content = config.read_text(encoding="utf-8-sig")
match = re.search(
r"(?m)^\s*-\s*wiki_root:\s*`?([^`\r\n]+?)`?\s*$", content
)
if match:
candidate = Path(match.group(1).strip().strip('"\''))
if not candidate.is_absolute():
candidate = root / candidate
return candidate.resolve()
def display_path(path: Path, root: Path) -> str:
try:
return lifecycle.relative(path, root)
except ValueError:
return str(path)
def as_list(value: Any) -> list[str]:
return lifecycle.as_list(value)
def normalize(value: str) -> str:
return lifecycle.normalized(value)
def query_terms(context: str, explicit: list[str]) -> list[str]:
terms = [item.strip() for item in explicit if item.strip()]
terms.extend(
item.strip()
for item in re.split(r"[\s,,。;;::/|、()()]+", context)
if item.strip()
)
terms.extend(re.findall(r"\b[A-Za-z]{1,8}(?:\.[A-Za-z]{1,4})?\b", context))
result: list[str] = []
seen: set[str] = set()
for term in terms:
key = normalize(term)
if len(key) < 2 or key in STOP_TERMS or key in seen:
continue
seen.add(key)
result.append(term)
return result
def extract_title(meta: dict[str, Any], content: str, path: Path) -> str:
if meta.get("title"):
return str(meta["title"])
match = re.search(r"(?m)^#\s+(.+?)\s*$", content)
return match.group(1).strip() if match else path.stem
def strip_frontmatter(content: str) -> str:
return re.sub(r"^---\s*\n.*?\n---\s*(?:\n|$)", "", content, count=1, flags=re.DOTALL)
def make_snippet(content: str, terms: list[str], limit: int = 260) -> str:
body = strip_frontmatter(content)
lines = [line.strip(" #>-\t") for line in body.splitlines() if line.strip()]
normalized_terms = [normalize(term) for term in terms]
for line in lines:
normalized_line = normalize(line)
if any(term in normalized_line for term in normalized_terms if term):
return line[:limit] + ("…" if len(line) > limit else "")
if not lines:
return ""
text = " ".join(lines[:3])
return text[:limit] + ("…" if len(text) > limit else "")
def substantive_counter_section(content: str) -> bool:
pattern = re.compile(
r"(?ims)^##\s*(?:反方证据|反例|矛盾|争议|counter[- ]?evidence|counterexamples?)\s*$"
r"(.*?)(?=^##\s|\Z)"
)
for match in pattern.finditer(content):
body = re.sub(r"<!--[\s\S]*?-->", "", match.group(1))
body = re.sub(r"[\s#>*_`-]", "", body)
if body:
return True
return False
def page_flags(section: str, meta: dict[str, Any], content: str) -> list[str]:
flags: list[str] = []
status = str(meta.get("status") or "").lower()
if status in REVIEW_STATUSES:
flags.append(f"状态需复审: {status}")
if lifecycle.is_overdue(meta.get("review_due")):
flags.append(f"复审已到期: {meta.get('review_due')}")
if substantive_counter_section(content):
flags.append("存在已填写的反方/冲突章节")
if section == "sources" and not (meta.get("source_url") or meta.get("source_path")):
flags.append("来源字段不完整")
if section in KNOWLEDGE_SECTIONS:
if not meta.get("id"):
flags.append("缺少稳定知识 ID")
if not meta.get("review_due"):
flags.append("缺少复审日期")
if not as_list(meta.get("recall_signals")):
flags.append("缺少召回信号")
return flags
def score_wiki_page(
*,
meta: dict[str, Any],
title: str,
path: Path,
content: str,
context: str,
terms: list[str],
) -> tuple[int, list[str]]:
score = 0
reasons: list[str] = []
context_norm = normalize(context)
title_norm = normalize(title)
filename_norm = normalize(path.stem)
body_norm = normalize(strip_frontmatter(content))
weighted_fields = (
("ticker", 14, "标的"),
("concepts", 9, "概念"),
("tags", 7, "标签"),
("related", 6, "关联页"),
("domain", 3, "领域"),
)
for field, weight, label in weighted_fields:
hits = []
for value in as_list(meta.get(field)):
value_norm = normalize(value)
if value_norm and value_norm in context_norm:
hits.append(value)
if hits:
score += weight * len(hits)
reasons.append(f"{label}: {'、'.join(hits[:2])}")
title_hits = [term for term in terms if normalize(term) in title_norm or normalize(term) in filename_norm]
if title_hits:
score += 10 * len(title_hits)
reasons.append("标题: " + "、".join(title_hits[:3]))
elif len(title_norm) >= 3 and title_norm in context_norm:
score += 12
reasons.append(f"标题直接命中: {title}")
body_hits = [term for term in terms if normalize(term) in body_norm]
if body_hits:
score += 2 * min(len(body_hits), 5)
reasons.append("正文: " + "、".join(body_hits[:3]))
summary_norm = normalize(str(meta.get("summary") or meta.get("description") or ""))
summary_hits = [term for term in terms if normalize(term) in summary_norm]
if summary_hits:
score += 5 * len(summary_hits)
reasons.append("摘要: " + "、".join(summary_hits[:3]))
return score, reasons
def iter_knowledge_pages(root: Path, wiki_root: Path) -> list[dict[str, Any]]:
pages: list[dict[str, Any]] = []
locations: list[tuple[str, Path]] = [
(kind, wiki_root / section) for kind, section in KNOWLEDGE_SECTIONS.items()
]
locations.append(("pattern", root / "workspace/patterns"))
for knowledge_type, folder in locations:
if not folder.is_dir():
continue
for path in sorted(folder.glob("*.md")):
if path.name.startswith("_") or path.name.lower() == "readme.md":
continue
meta, content, error = lifecycle.read_page(path)
pages.append(
{
"type": knowledge_type,
"path": path,
"relative_path": display_path(path, root),
"meta": meta,
"content": content,
"error": error,
}
)
return pages
def load_reusable_knowledge(
root: Path,
wiki_root: Path,
context: str,
*,
limit: int,
include_review: bool,
pages: list[dict[str, Any]] | None = None,
) -> list[dict[str, Any]]:
matches: list[dict[str, Any]] = []
for page in pages if pages is not None else iter_knowledge_pages(root, wiki_root):
meta = page["meta"]
status = str(meta.get("status") or "")
if not include_review and status not in lifecycle.DEFAULT_LOAD_STATUSES[page["type"]]:
continue
score, reasons = lifecycle.score_page(page, context)
if score <= 0:
continue
matches.append(
{
"id": str(meta.get("id") or page["path"].stem),
"type": page["type"],
"title": extract_title(meta, page["content"], page["path"]),
"status": status,
"summary": str(meta.get("summary") or ""),
"scope": str(meta.get("scope") or ""),
"review_due": str(meta.get("review_due") or ""),
"failure_signals": as_list(
meta.get("failure_signals") or meta.get("invalidation_signals")
),
"path": page["relative_path"],
"score": score,
"matched_by": reasons,
"flags": page_flags(page["type"], meta, page["content"]),
"content": page["content"],
}
)
type_order = {"rule": 0, "pattern": 1, "exploration": 2}
matches.sort(key=lambda item: (type_order[item["type"]], -item["score"], item["id"]))
return matches[:limit]
def scan_wiki(
root: Path,
wiki_root: Path,
context: str,
terms: list[str],
*,
sections: tuple[str, ...],
limit: int,
) -> tuple[list[dict[str, Any]], dict[str, int]]:
matches: list[dict[str, Any]] = []
counts = {section: 0 for section in sections}
for section in sections:
folder = wiki_root / section
if not folder.is_dir():
continue
for path in sorted(folder.rglob("*.md")):
if path.name.startswith("_") or path.name.lower() == "readme.md":
continue
counts[section] += 1
meta, content, error = lifecycle.read_page(path)
if error and not content:
continue
title = extract_title(meta, content, path)
score, reasons = score_wiki_page(
meta=meta,
title=title,
path=path,
content=content,
context=context,
terms=terms,
)
if score <= 0:
continue
matches.append(
{
"type": SECTION_TYPES[section],
"title": title,
"status": str(meta.get("status") or ""),
"summary": str(meta.get("summary") or meta.get("description") or ""),
"path": display_path(path, root),
"score": score,
"matched_by": reasons,
"snippet": make_snippet(content, terms),
"flags": page_flags(section, meta, content),
"content": content,
}
)
section_order = {"entity": 0, "concept": 1, "source": 2, "raw": 3}
matches.sort(key=lambda item: (-item["score"], section_order.get(item["type"], 9), item["path"]))
return matches[:limit], counts
def build_preflight(
root: Path,
*,
context: str,
research_id: str = "",
wiki_root: Path | None = None,
explicit_terms: list[str] | None = None,
tickers: list[str] | None = None,
domains: list[str] | None = None,
knowledge_limit: int = 8,
wiki_limit: int = 12,
include_review: bool = False,
include_raw: bool = False,
) -> dict[str, Any]:
started = time.perf_counter()
root = find_workspace_root(root)
resolved_wiki = resolve_wiki_root(root, wiki_root)
additions = [*(explicit_terms or []), *(tickers or []), *(domains or [])]
enriched_context = " ".join([context, *additions]).strip()
terms = query_terms(enriched_context, additions)
knowledge_pages = iter_knowledge_pages(root, resolved_wiki)
knowledge = load_reusable_knowledge(
root,
resolved_wiki,
enriched_context,
limit=max(1, knowledge_limit),
include_review=include_review,
pages=knowledge_pages,
)
sections = WIKI_SECTIONS + (("raw",) if include_raw else ())
wiki_matches, scanned = scan_wiki(
root,
resolved_wiki,
enriched_context,
terms,
sections=sections,
limit=max(1, wiki_limit),
)
digest = hashlib.sha256(
(research_id + enriched_context + str(resolved_wiki)).encode("utf-8")
).hexdigest()[:8]
prefix = re.sub(r"[^A-Za-z0-9-]+", "-", research_id).strip("-") or date.today().strftime("%Y%m%d")
flags = [
{"path": item["path"], "flags": item["flags"]}
for item in [*knowledge, *wiki_matches]
if item["flags"]
]
return {
"id": f"PF-{prefix}-{digest}",
"research_id": research_id or None,
"created_at": datetime.now(timezone.utc).isoformat(),
"context": enriched_context,
"terms": terms,
"wiki_root": str(resolved_wiki),
"scanned": {
"knowledge": len(knowledge_pages),
**scanned,
},
"knowledge": knowledge,
"wiki_matches": wiki_matches,
"review_flags": flags,
"elapsed_ms": round((time.perf_counter() - started) * 1000, 2),
}
def yaml_list(values: list[str]) -> str:
return "[" + ", ".join(json.dumps(value, ensure_ascii=False) for value in values) + "]"
def render_receipt(result: dict[str, Any]) -> str:
knowledge_ids = [item["id"] for item in result["knowledge"]]
wiki_paths = [item["path"] for item in result["wiki_matches"]]
lines = [
"---",
f"id: {result['id']}",
"type: research-preflight",
f"research_id: {result['research_id'] or ''}",
f"created_at: {result['created_at']}",
f"context: {json.dumps(result['context'], ensure_ascii=False)}",
f"knowledge_used: {yaml_list(knowledge_ids)}",
f"wiki_pages_loaded: {yaml_list(wiki_paths)}",
"---",
"",
f"# Research Preflight: {result['research_id'] or result['id']}",
"",
f"> 本地扫描耗时 {result['elapsed_ms']} ms;只记录命中和风险提示,不代表结论已验证。",
"",
"## 扫描范围",
"",
f"- Wiki:`{result['wiki_root']}`",
"- 扫描:" + " / ".join(f"{key} {value}" for key, value in result["scanned"].items()),
"- 检索词:" + ("、".join(result["terms"]) or "无"),
"",
"## 可复用知识",
"",
]
if result["knowledge"]:
for item in result["knowledge"]:
lines.append(
f"- `{item['id']}` [{item['type']}] {item['title']} | {item['path']} | "
f"命中:{';'.join(item['matched_by'])}"
)
else:
lines.append("- 未命中可复用 Rule / Pattern / Exploration。")
lines.extend(["", "## 相关 Wiki 页面", ""])
if result["wiki_matches"]:
for item in result["wiki_matches"]:
lines.append(
f"- [{item['type']}] {item['title']} | {item['path']} | "
f"命中:{';'.join(item['matched_by'])} | 摘要:{item['summary'] or item['snippet'] or '-'}"
)
else:
lines.append("- 未命中 Entity / Concept / Source 页面。")
lines.extend(["", "## 复审提示", ""])
if result["review_flags"]:
for item in result["review_flags"]:
lines.append(f"- {item['path']}:{';'.join(item['flags'])}")
else:
lines.append("- 未发现显式过期、弱化或反方章节提示。")
lines.extend(
[
"",
"## 加载回执",
"",
f"Wiki check: 扫描 {sum(result['scanned'].values())} 篇,命中知识卡 "
f"{len(result['knowledge'])} 张、普通 Wiki {len(result['wiki_matches'])} 篇;"
f"复审提示 {len(result['review_flags'])} 条。",
"",
]
)
return "\n".join(lines)
def save_receipt(root: Path, result: dict[str, Any], requested: Path | None = None) -> Path:
if requested is None:
filename = re.sub(r"[^A-Za-z0-9._-]+", "-", str(result["research_id"] or result["id"]))
path = root / "output" / "research" / "preflight" / f"{filename}.md"
else:
path = requested if requested.is_absolute() else root / requested
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(render_receipt(result), encoding="utf-8")
return path
def update_research_file(root: Path, requested: Path, result: dict[str, Any]) -> Path:
path = requested if requested.is_absolute() else root / requested
if not path.is_file():
raise ValueError(f"research file does not exist: {display_path(path, root)}")
meta, content, error = lifecycle.read_page(path)
if error:
raise ValueError(f"invalid research file: {error}")
research_id = str(result.get("research_id") or "")
file_id = str(meta.get("id") or "")
if file_id and research_id and file_id != research_id:
raise ValueError(f"research id mismatch: file={file_id}, preflight={research_id}")
content = lifecycle.replace_frontmatter_scalar(content, "preflight_id", str(result["id"]))
content = lifecycle.replace_frontmatter_scalar(
content, "knowledge_used", yaml_list([item["id"] for item in result["knowledge"]])
)
content = lifecycle.replace_frontmatter_scalar(
content,
"wiki_pages_loaded",
yaml_list([item["path"] for item in result["wiki_matches"]]),
)
path.write_text(content, encoding="utf-8")
return path
def serializable_result(result: dict[str, Any], full: bool) -> dict[str, Any]:
payload = json.loads(json.dumps(result, ensure_ascii=False))
if not full:
for item in [*payload["knowledge"], *payload["wiki_matches"]]:
item.pop("content", None)
return payload
def render_text(result: dict[str, Any], full: bool) -> str:
lines = [render_receipt(result).rstrip()]
if full:
for item in [*result["knowledge"], *result["wiki_matches"]]:
lines.extend(["", f"---\n\n## 全文:{item['title']}\n", item["content"].rstrip()])
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=Path.cwd())
parser.add_argument("--wiki-root", type=Path)
parser.add_argument("--context", required=True)
parser.add_argument("--research-id", default="")
parser.add_argument("--term", action="append", default=[])
parser.add_argument("--ticker", action="append", default=[])
parser.add_argument("--domain", action="append", default=[])
parser.add_argument("--knowledge-limit", type=int, default=8)
parser.add_argument("--wiki-limit", type=int, default=12)
parser.add_argument("--include-review", action="store_true")
parser.add_argument("--include-raw", action="store_true")
parser.add_argument("--record", action="store_true")
parser.add_argument("--save", type=Path)
parser.add_argument("--research-file", type=Path)
parser.add_argument("--full", action="store_true")
parser.add_argument("--json", action="store_true")
args = parser.parse_args()
if args.record and (not args.research_id or not args.research_file):
print(
"ERROR: --record 需要同时提供 --research-id 和 --research-file,"
"以便生成可追踪回执并更新研究报告",
file=sys.stderr,
)
return 2
root = find_workspace_root(args.root)
result = build_preflight(
root,
context=args.context,
research_id=args.research_id,
wiki_root=args.wiki_root,
explicit_terms=args.term,
tickers=args.ticker,
domains=args.domain,
knowledge_limit=args.knowledge_limit,
wiki_limit=args.wiki_limit,
include_review=args.include_review,
include_raw=args.include_raw,
)
if args.research_file:
try:
research_path = update_research_file(root, args.research_file, result)
except ValueError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
result["research_file"] = display_path(research_path, root)
if args.record:
result["knowledge_invocation_id"] = lifecycle.record_load(
root, result["context"], result["knowledge"], args.research_id
)
saved = save_receipt(root, result, args.save) if args.record or args.save else None
if saved:
result["receipt_path"] = display_path(saved, root)
if args.json:
print(json.dumps(serializable_result(result, args.full), ensure_ascii=False, indent=2))
else:
print(render_text(result, args.full))
if saved:
print(f"回执:{display_path(saved, root)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
193 files · 8 not served here
- .gitattributes
- .github/workflows/ci.yml
- .gitignore
- AGENTS.md
- ARCHITECTURE.md
- ATTRIBUTION.md
- CHANGELOG.md
- CLAUDE.md
- DATA-SOURCES.md (not served here)
- INSTALL-FOR-AI.md
- LICENSE
- README.md
- SMOKE-TEST.md
- START-HERE.md
- TROUBLESHOOTING.md
- config/.gitkeep
- config/README.md
- docs/demo.gif (not served here)
- docs/gen_demo_gif.py
- evidence/README.md
- examples/README.md
- examples/knowledge-lifecycle-cycle-top/README.md
- examples/knowledge-lifecycle-cycle-top/wiki/explorations/lithium-cycle-review.md
- examples/knowledge-lifecycle-cycle-top/wiki/explorations/solar-cycle-review.md
- examples/knowledge-lifecycle-cycle-top/wiki/patterns/capacity-expansion-cycle-top.md
- examples/knowledge-lifecycle-cycle-top/wiki/sources/lithium-capacity-expansion.md
- examples/knowledge-lifecycle-cycle-top/wiki/sources/lithium-participant-pressure.md
- examples/knowledge-lifecycle-cycle-top/wiki/sources/solar-equipment-orders.md
- examples/knowledge-lifecycle-cycle-top/wiki/sources/solar-leader-slowdown.md
- examples/knowledge-lifecycle-cycle-top/workspace/workspace-config.md
- examples/smoke-test/output/first-ingest/2026-06-05-smoke-test.md
- examples/smoke-test/output/pdf-ingest/2026-06-05-smoke-test.md
- examples/smoke-test/output/wiki-read-priority/2026-06-05-answer-from-wiki.md
- examples/smoke-test/wiki/concepts/ai-workspace-file-protocol.md
- examples/smoke-test/wiki/concepts/pdf-to-md-ingest.md
- examples/smoke-test/wiki/explorations/2026-06-05-first-ingest-smoke-test.md
- examples/smoke-test/wiki/explorations/2026-06-05-pdf-ingest-smoke-test.md
- examples/smoke-test/wiki/raw/2026-06-05-first-note.md
- examples/smoke-test/wiki/raw/2026-06-05-sample-ai-workspace.md
- examples/smoke-test/wiki/raw/2026-06-05-sample-ai-workspace.pdf (not served here)
- examples/smoke-test/wiki/sources/2026-06-05-first-note.md
- examples/smoke-test/wiki/sources/2026-06-05-sample-ai-workspace.md
- hypothesis/README.md
- inbox/README.md
- inbox/first-note.md
- inbox/sample-ai-workspace.pdf (not served here)
- output/.gitkeep
- output/README.md
- output/daily-watch/README.md
- output/pod2wiki/.gitkeep
- output/research/.gitkeep
- output/research/preflight/.gitkeep
- output/screen/.gitkeep
- portfolio/README.md
- portfolio/journal/.gitkeep
- requirements-pdf.txt
- requirements.lock
- requirements.txt
- system/README.md
- system/integrations/_template.md
- system/integrations/daily-watchlist.md
- system/integrations/hypothesis-tracker.md
- system/integrations/object-model.md
- system/integrations/personal-wiki.md
- system/integrations/pod2wiki.md
- system/interfaces/README.md
- system/lib/__init__.py
- system/lib/llm_client.py
- system/managed-files.json
- system/scripts/check_workspace.py
- system/scripts/install_workspace.py
- system/scripts/knowledge_lifecycle.py
- system/scripts/pdf_to_md.py
- system/scripts/prune_active_context.py (not served here)
- system/scripts/research_preflight.py
- system/scripts/review_queue.py
- system/scripts/upgrade_workspace.py
- system/scripts/wiki_tagger.py
- system/scripts/workspace_doctor.py
- system/scripts/workspace_status.py
- system/skills/_template.md
- system/skills/daily-watch-ht.md
- system/skills/daily-watch-import.md
- system/skills/daily-watch.md
- system/skills/first-ingest.md
- system/skills/first-research.md
- system/skills/hypothesis-review.md
- system/skills/knowledge-lifecycle.md
- system/skills/pdf-ingest.md
- system/skills/podcast.md
- system/skills/post-install-cleanup.md
- system/skills/research-closeout.md
- system/skills/research.md
- system/skills/screen.md
- system/skills/structure-health.md
- system/skills/upgrade-workspace.md
- system/skills/workspace-doctor.md
- system/skills/workspace-status.md
- system/templates/AGENTS.md
- system/templates/CLAUDE.md
- system/templates/active-context.md
- system/templates/evidence-entry.md
- system/templates/friction-log.md
- system/templates/interfaces-README.md
- system/templates/reading-hub.base
- system/templates/reading-hub.md
- system/templates/research-profile.md
- system/templates/research-report.md
- system/templates/review-queue.md
- system/templates/workspace-config.md
- tests/README.md
- tests/_test_paths.py
- tests/conftest.py
- tests/test_agent_handoff.py
- tests/test_daily_watch_fixes.py
- tests/test_data_coverage.py
- tests/test_first_research_e2e.py
- tests/test_install_workspace.py
- tests/test_knowledge_lifecycle.py
- tests/test_llm_config.py
- tests/test_no_key_degradation.py
- tests/test_podcast_fixes.py
- tests/test_prune_active_context.py
- tests/test_repository_contract.py
- tests/test_research_preflight.py
- tests/test_wiki_tagger.py (not served here)
- tests/test_workspace_paths.py
- tests/test_workspace_state.py
- tools/README.md
- tools/__init__.py
- tools/daily-watch/README.md
- tools/daily-watch/__init__.py
- tools/daily-watch/config-examples/daily-watchlist.env.example
- tools/daily-watch/config-examples/daily-watchlist.example.yaml
- tools/daily-watch/config-examples/daily-watchlist.watchlist.empty.md
- tools/daily-watch/config-examples/daily-watchlist.watchlist.example.md
- tools/daily-watch/config-examples/hypothesis-tracker.example.yaml
- tools/daily-watch/config-examples/hypothesis-tracker.rules.example.md
- tools/daily-watch/pyproject.toml
- tools/daily-watch/requirements-tushare.txt
- tools/daily-watch/requirements.txt
- tools/daily-watch/scripts/__init__.py
- tools/daily-watch/scripts/check_setup.py (not served here)
- tools/daily-watch/scripts/fetch_macro_data.py
- tools/daily-watch/scripts/fetch_market_data.py (not served here)
- tools/daily-watch/scripts/generate_daily_report.py
- tools/daily-watch/scripts/sync_hypothesis.py
- tools/daily-watch/scripts/trade_stats.py
- tools/daily-watch/scripts/workspace_paths.py
- tools/daily-watch/templates/daily-watchlist-report-template.md
- tools/daily-watch/templates/hypothesis-tracker-hypothesis-template.md
- tools/daily-watch/templates/hypothesis-tracker-journal-template.md
- tools/daily-watch/templates/hypothesis-tracker-report-template.md
- tools/podcast/.env.example
- tools/podcast/README.md
- tools/podcast/__init__.py
- tools/podcast/examples/config.ai-investing.yaml
- tools/podcast/pyproject.toml
- tools/podcast/requirements-transcribe.txt
- tools/podcast/requirements.txt
- tools/podcast/scripts/__init__.py
- tools/podcast/scripts/fetch_podcasts.py
- tools/podcast/scripts/llm_client.py
- tools/podcast/scripts/podcast_batch_summarize.py
- tools/podcast/scripts/podcast_feed_registry.py
- tools/podcast/scripts/podcast_rss_transcribe.py
- tools/podcast/scripts/proxy_config.py
- wiki/README.md
- wiki/_schema.md
- wiki/concepts/.gitkeep
- wiki/entities/.gitkeep
- wiki/explorations/.gitkeep
- wiki/explorations/_index.md
- wiki/explorations/_template.md
- wiki/false-beliefs.md
- wiki/patterns/_index.md
- wiki/patterns/_template.md
- wiki/raw/.gitkeep
- wiki/rules.md
- wiki/rules/_index.md
- wiki/rules/_template.md
- wiki/sources/.gitkeep
- workspace/README.md
- workspace/archive/README.md
- workspace/meta/active-context.md
- workspace/meta/friction-log.md
- workspace/monitoring/README.md
- workspace/monitoring/watchlist.md
- workspace/patterns/_index.md
- workspace/patterns/_template.md
- workspace/research-profile.md
- workspace/review-queue.md
- workspace/workspace-config.md
Advantages
- Local-first: the memory stays in files or a database on your own machine (a rule-derived fact from the brain’s files, evidence: README.md)
- Plain markdown memory that you can read and edit yourself (a rule-derived fact from the brain’s files, evidence: wiki)
- No API key is named in its files (a rule-derived fact from the brain’s files, evidence: README.md)
- Open licence (MIT) (a rule-derived fact from the brain’s files)
Limitations
- No ZIP download: install it from its source (a rule-derived fact from the brain’s files)
Security
Risk level: Low risk
This brain runs
Agent instructions that run or fetch something (24)
AGENTS.md:工具代码在 `tools/` 目录下。首次使用时 agent 检查依赖,缺什么用 `python3 -m pip install ...` 安装;如果环境只有 `python`,再替换成 `python -m pip ...`。AGENTS.md:用户说“看看现在该做什么”时,运行 `python system/scripts/workspace_status.py`(macOS/Linux 可用 `python3`)。AGENTS.md:python3 system/scripts/prune_active_context.py # dry-run,先看会剪什么AGENTS.md:python3 system/scripts/prune_active_context.py --apply # 确认后执行AGENTS.md:装好上手后一次性瘦身:`system/skills/post-install-cleanup.md`(清安装脚手架 + 精简必读文件)。AGENTS.md:active-context 三道闸门:`python3 system/scripts/prune_active_context.py --apply`。CLAUDE.md:工具代码在 `tools/` 目录下。首次使用时 agent 检查依赖,缺什么用 `python3 -m pip install ...` 安装;如果环境只有 `python`,再替换成 `python -m pip ...`。CLAUDE.md:用户说“看看现在该做什么”时,运行 `python system/scripts/workspace_status.py`(macOS/Linux 可用 `python3`)。CLAUDE.md:python3 system/scripts/prune_active_context.py # dry-run,先看会剪什么CLAUDE.md:python3 system/scripts/prune_active_context.py --apply # 确认后执行CLAUDE.md:装好上手后一次性瘦身:`system/skills/post-install-cleanup.md`(清安装脚手架 + 精简必读文件)。CLAUDE.md:active-context 三道闸门:`python3 system/scripts/prune_active_context.py --apply`。system/templates/AGENTS.md:工具代码在 `tools/` 目录下。首次使用时 agent 检查依赖,缺什么用 `python3 -m pip install ...` 安装;如果环境只有 `python`,再替换成 `python -m pip ...`。system/templates/AGENTS.md:用户说“看看现在该做什么”时,运行 `python system/scripts/workspace_status.py`(macOS/Linux 可用 `python3`)。system/templates/AGENTS.md:python3 system/scripts/prune_active_context.py # dry-run,先看会剪什么system/templates/AGENTS.md:python3 system/scripts/prune_active_context.py --apply # 确认后执行system/templates/AGENTS.md:装好上手后一次性瘦身:`system/skills/post-install-cleanup.md`(清安装脚手架 + 精简必读文件)。system/templates/AGENTS.md:active-context 三道闸门:`python3 system/scripts/prune_active_context.py --apply`。system/templates/CLAUDE.md:工具代码在 `tools/` 目录下。首次使用时 agent 检查依赖,缺什么用 `python3 -m pip install ...` 安装;如果环境只有 `python`,再替换成 `python -m pip ...`。system/templates/CLAUDE.md:用户说“看看现在该做什么”时,运行 `python system/scripts/workspace_status.py`(macOS/Linux 可用 `python3`)。system/templates/CLAUDE.md:python3 system/scripts/prune_active_context.py # dry-run,先看会剪什么system/templates/CLAUDE.md:python3 system/scripts/prune_active_context.py --apply # 确认后执行system/templates/CLAUDE.md:装好上手后一次性瘦身:`system/skills/post-install-cleanup.md`(清安装脚手架 + 精简必读文件)。system/templates/CLAUDE.md:active-context 三道闸门:`python3 system/scripts/prune_active_context.py --apply`。
- Low risk: network-instructions (AGENTS.md:52)
- Low risk: network-instructions (CLAUDE.md:52)
- Low risk: scripts-present (system/scripts/prune_active_context.py)
- Low risk: network-instructions (system/templates/AGENTS.md:52)
- Low risk: network-instructions (system/templates/CLAUDE.md:52)
Attribution and licence
- First committed by
Benboerba620
- Publisher
- Benbor (Community)
- Source
- Benboerba620/ai-workspace-hub · commit e01a41a
- Licence
- MIT License (files served here)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Contributors
- Benboerba620
AutoGineer is not affiliated with or endorsed by Benboerba620.
Install from source
8 of its files are not served here, so there is no ZIP. Benboerba620/ai-workspace-hub at e01a41a