sanook-cli 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +19 -0
- package/CHANGELOG.md +173 -0
- package/README.md +153 -20
- package/README.th.md +136 -0
- package/dist/agentContext.js +4 -0
- package/dist/approval.js +6 -0
- package/dist/bin.js +405 -57
- package/dist/brain.js +92 -59
- package/dist/brand.js +47 -0
- package/dist/checkpoint.js +37 -0
- package/dist/commands.js +86 -6
- package/dist/compaction.js +76 -5
- package/dist/config.js +100 -12
- package/dist/cost.js +60 -3
- package/dist/doctor.js +92 -0
- package/dist/gateway/auth.js +2 -2
- package/dist/gateway/ledger.js +2 -2
- package/dist/gateway/scheduler.js +1 -0
- package/dist/gateway/serve.js +6 -4
- package/dist/gateway/server.js +10 -2
- package/dist/git.js +11 -2
- package/dist/hooks.js +43 -17
- package/dist/knowledge.js +48 -49
- package/dist/loop.js +182 -66
- package/dist/lsp/client.js +173 -0
- package/dist/lsp/framing.js +56 -0
- package/dist/lsp/index.js +138 -0
- package/dist/lsp/servers.js +82 -0
- package/dist/mcp-server.js +244 -0
- package/dist/mcp.js +184 -29
- package/dist/memory-store.js +559 -0
- package/dist/memory.js +143 -29
- package/dist/orchestrate.js +150 -0
- package/dist/providers/codex.js +21 -7
- package/dist/providers/keys.js +3 -2
- package/dist/providers/models.js +22 -6
- package/dist/providers/registry.js +155 -1
- package/dist/repomap.js +93 -0
- package/dist/search/chunk.js +158 -0
- package/dist/search/embed-store.js +187 -0
- package/dist/search/engine.js +203 -0
- package/dist/search/fuse.js +35 -0
- package/dist/search/index-core.js +187 -0
- package/dist/search/indexer.js +241 -0
- package/dist/search/store.js +77 -0
- package/dist/session.js +42 -8
- package/dist/skill-install.js +10 -10
- package/dist/skills.js +12 -9
- package/dist/summarize.js +31 -0
- package/dist/tools/bash.js +21 -2
- package/dist/tools/diagnostics.js +41 -0
- package/dist/tools/edit.js +29 -7
- package/dist/tools/index.js +8 -1
- package/dist/tools/list.js +7 -2
- package/dist/tools/permission.js +90 -9
- package/dist/tools/read.js +23 -4
- package/dist/tools/remember.js +1 -1
- package/dist/tools/sandbox.js +61 -0
- package/dist/tools/search.js +105 -4
- package/dist/tools/task.js +195 -29
- package/dist/tools/timeout.js +35 -0
- package/dist/tools/util.js +10 -0
- package/dist/tools/write.js +6 -4
- package/dist/trust.js +89 -0
- package/dist/ui/app.js +228 -31
- package/dist/ui/banner.js +4 -9
- package/dist/ui/brain-wizard.js +2 -2
- package/dist/ui/history.js +30 -0
- package/dist/ui/mentions.js +44 -0
- package/dist/ui/render.js +55 -15
- package/dist/ui/setup.js +97 -12
- package/dist/ui/useEditor.js +83 -0
- package/dist/update.js +114 -0
- package/dist/worktree.js +173 -0
- package/package.json +11 -5
- package/scripts/postinstall.mjs +33 -0
- package/second-brain/.agents/_Index.md +30 -0
- package/second-brain/.agents/skills/_Index.md +30 -0
- package/second-brain/.agents/workflows/_Index.md +30 -0
- package/second-brain/AGENTS.md +4 -4
- package/second-brain/Acceptance/_Index.md +30 -0
- package/second-brain/Acceptance/golden-case-template.md +39 -0
- package/second-brain/Areas/_Index.md +30 -0
- package/second-brain/Bugs/System-OS/_Index.md +30 -0
- package/second-brain/Bugs/_Index.md +30 -0
- package/second-brain/CLAUDE.md +4 -1
- package/second-brain/Checklists/_Index.md +30 -0
- package/second-brain/Checklists/preflight-postflight-template.md +29 -0
- package/second-brain/Distillations/_Index.md +30 -0
- package/second-brain/Entities/_Index.md +30 -0
- package/second-brain/Entities/entity-template.md +33 -0
- package/second-brain/Evals/_Index.md +30 -0
- package/second-brain/Evals/correction-pairs.md +24 -0
- package/second-brain/Evals/failure-taxonomy.md +24 -0
- package/second-brain/Evals/golden-set.md +25 -0
- package/second-brain/Evals/quality-ledger.md +23 -0
- package/second-brain/Evals/self-eval-rubric.md +23 -0
- package/second-brain/GEMINI.md +4 -4
- package/second-brain/Goals/_Index.md +30 -0
- package/second-brain/Handoffs/_Index.md +30 -0
- package/second-brain/Home.md +7 -0
- package/second-brain/Intake/Raw Sources/_Index.md +30 -0
- package/second-brain/Intake/_Index.md +30 -0
- package/second-brain/Intake/_Quarantine/_Index.md +30 -0
- package/second-brain/Learning/_Index.md +30 -0
- package/second-brain/Playbooks/_Index.md +30 -0
- package/second-brain/Playbooks/playbook-template.md +23 -0
- package/second-brain/Projects/_Index.md +30 -0
- package/second-brain/Prompts/_Index.md +30 -0
- package/second-brain/README.md +2 -1
- package/second-brain/Research/_Index.md +30 -0
- package/second-brain/Retrospectives/_Index.md +30 -0
- package/second-brain/Reviews/_Index.md +30 -0
- package/second-brain/Runbooks/_Index.md +30 -0
- package/second-brain/Runbooks/eval-loop.md +24 -0
- package/second-brain/Sessions/_Index.md +30 -0
- package/second-brain/Shared/AI-Context-Index.md +20 -0
- package/second-brain/Shared/AI-Threads/_Index.md +30 -0
- package/second-brain/Shared/Archive/_Index.md +30 -0
- package/second-brain/Shared/Assets/_Index.md +30 -0
- package/second-brain/Shared/Context-Packs/_Index.md +30 -0
- package/second-brain/Shared/Context7-Docs/_Index.md +30 -0
- package/second-brain/Shared/Coordination/NOW.md +28 -0
- package/second-brain/Shared/Coordination/_Index.md +30 -0
- package/second-brain/Shared/Coordination/agent-registry.md +24 -0
- package/second-brain/Shared/Coordination/task-board/_Index.md +30 -0
- package/second-brain/Shared/Coordination/task-board/task-template.md +43 -0
- package/second-brain/Shared/Coordination/task-board.md +32 -0
- package/second-brain/Shared/Core-Facts/_Index.md +30 -0
- package/second-brain/Shared/Decision-Memory/_Index.md +30 -0
- package/second-brain/Shared/Glossary/_Index.md +30 -0
- package/second-brain/Shared/Memory-Inbox/_Index.md +30 -0
- package/second-brain/Shared/Operating-State/_Index.md +30 -0
- package/second-brain/Shared/Prompting/_Index.md +30 -0
- package/second-brain/Shared/Provenance/_Index.md +30 -0
- package/second-brain/Shared/Rules/_Index.md +30 -0
- package/second-brain/Shared/Rules/contextual-note-rule.md +30 -0
- package/second-brain/Shared/Rules/frontmatter-standard.md +10 -0
- package/second-brain/Shared/Rules/memory-write-protocol.md +28 -0
- package/second-brain/Shared/Rules/procedural-runbook-header.md +40 -0
- package/second-brain/Shared/Rules/review-and-staleness-policy.md +22 -0
- package/second-brain/Shared/Rules/rules-formatting.md +34 -0
- package/second-brain/Shared/Scripts/_Index.md +30 -0
- package/second-brain/Shared/Scripts-Archive/_Index.md +30 -0
- package/second-brain/Shared/Tech-Standards/_Index.md +30 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +40 -0
- package/second-brain/Shared/User-Memory/_Index.md +30 -0
- package/second-brain/Shared/User-Persona/_Index.md +30 -0
- package/second-brain/Shared/User-Persona/owner-profile.md +25 -0
- package/second-brain/Shared/Working-Memory/_Index.md +30 -0
- package/second-brain/Shared/_Index.md +30 -0
- package/second-brain/Shared/mcp-servers/_Index.md +30 -0
- package/second-brain/Skills/_Index.md +30 -0
- package/second-brain/Templates/_Index.md +30 -0
- package/second-brain/Templates/bug.md +2 -0
- package/second-brain/Templates/handoff.md +2 -0
- package/second-brain/Templates/session.md +2 -0
- package/second-brain/Tools/_Index.md +30 -0
- package/second-brain/Traces/_Index.md +30 -0
- package/second-brain/Vault Structure Map.md +33 -1
- package/second-brain/copilot/_Index.md +30 -0
- package/skills/audit-license-compliance/SKILL.md +117 -0
- package/skills/author-codemod/SKILL.md +110 -0
- package/skills/build-audit-logging/SKILL.md +112 -0
- package/skills/build-cdc-streaming-pipeline/SKILL.md +123 -0
- package/skills/build-cli-tool/SKILL.md +108 -0
- package/skills/build-data-table/SKILL.md +141 -0
- package/skills/build-native-mobile-ui/SKILL.md +154 -0
- package/skills/build-offline-first-sync/SKILL.md +118 -0
- package/skills/build-realtime-channel/SKILL.md +122 -0
- package/skills/build-vector-search/SKILL.md +131 -0
- package/skills/compose-local-dev-stack/SKILL.md +149 -0
- package/skills/configure-bundler-build/SKILL.md +166 -0
- package/skills/configure-dns-tls/SKILL.md +142 -0
- package/skills/configure-reverse-proxy-lb/SKILL.md +129 -0
- package/skills/configure-security-headers-csp/SKILL.md +122 -0
- package/skills/contract-testing/SKILL.md +140 -0
- package/skills/datetime-timezone-correctness/SKILL.md +125 -0
- package/skills/debug-ci-pipeline-failure/SKILL.md +134 -0
- package/skills/debug-flaky-tests/SKILL.md +128 -0
- package/skills/defend-llm-prompt-injection/SKILL.md +110 -0
- package/skills/deliver-webhooks/SKILL.md +116 -0
- package/skills/design-api-pagination/SKILL.md +144 -0
- package/skills/design-authorization-model/SKILL.md +119 -0
- package/skills/design-backup-dr-recovery/SKILL.md +113 -0
- package/skills/design-event-sourcing-cqrs/SKILL.md +143 -0
- package/skills/design-multi-tenancy/SKILL.md +100 -0
- package/skills/design-protobuf-grpc-service/SKILL.md +146 -0
- package/skills/design-relational-schema/SKILL.md +129 -0
- package/skills/design-search-index-infra/SKILL.md +151 -0
- package/skills/design-state-machine/SKILL.md +108 -0
- package/skills/design-token-system/SKILL.md +109 -0
- package/skills/distributed-locks-leases/SKILL.md +120 -0
- package/skills/encrypt-sensitive-data/SKILL.md +148 -0
- package/skills/feature-flags-rollout/SKILL.md +130 -0
- package/skills/file-upload-object-storage/SKILL.md +107 -0
- package/skills/fuzz-dynamic-security-test/SKILL.md +111 -0
- package/skills/harden-llm-app-reliability/SKILL.md +126 -0
- package/skills/i18n-localization-setup/SKILL.md +113 -0
- package/skills/idempotency-keys/SKILL.md +107 -0
- package/skills/implement-push-notifications/SKILL.md +142 -0
- package/skills/ingest-webhook-secure/SKILL.md +120 -0
- package/skills/integrate-oauth-oidc/SKILL.md +126 -0
- package/skills/load-stress-test/SKILL.md +129 -0
- package/skills/map-privacy-data-gdpr/SKILL.md +146 -0
- package/skills/model-nosql-data/SKILL.md +118 -0
- package/skills/money-decimal-arithmetic/SKILL.md +123 -0
- package/skills/monitor-ml-drift/SKILL.md +109 -0
- package/skills/numeric-precision-units/SKILL.md +144 -0
- package/skills/optimize-llm-cost-latency/SKILL.md +103 -0
- package/skills/optimize-react-rerenders/SKILL.md +124 -0
- package/skills/orchestrate-agent-workflow/SKILL.md +100 -0
- package/skills/payments-billing-integration/SKILL.md +114 -0
- package/skills/pin-toolchain-versions/SKILL.md +116 -0
- package/skills/plan-strangler-migration/SKILL.md +95 -0
- package/skills/property-based-testing/SKILL.md +108 -0
- package/skills/publish-package-registry/SKILL.md +130 -0
- package/skills/recover-git-state/SKILL.md +119 -0
- package/skills/remediate-web-vulnerabilities/SKILL.md +125 -0
- package/skills/resilience-timeouts-retries/SKILL.md +104 -0
- package/skills/resolve-merge-rebase-conflict/SKILL.md +97 -0
- package/skills/rewrite-git-history/SKILL.md +109 -0
- package/skills/scaffold-cross-platform-app/SKILL.md +137 -0
- package/skills/schema-evolution-compatibility/SKILL.md +121 -0
- package/skills/send-transactional-email/SKILL.md +126 -0
- package/skills/serve-deploy-ml-model/SKILL.md +107 -0
- package/skills/setup-cdn-edge-waf/SKILL.md +107 -0
- package/skills/setup-devcontainer-env/SKILL.md +131 -0
- package/skills/setup-lint-format-precommit/SKILL.md +140 -0
- package/skills/setup-monorepo-tooling/SKILL.md +125 -0
- package/skills/ship-mobile-app-store-release/SKILL.md +137 -0
- package/skills/structured-output-llm/SKILL.md +86 -0
- package/skills/supply-chain-sbom-provenance/SKILL.md +120 -0
- package/skills/test-data-factories/SKILL.md +158 -0
- package/skills/threat-model-stride/SKILL.md +123 -0
- package/skills/train-evaluate-ml-model/SKILL.md +109 -0
- package/skills/unicode-text-correctness/SKILL.md +109 -0
- package/skills/visual-regression-testing/SKILL.md +120 -0
package/dist/memory.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { readFile, writeFile,
|
|
2
|
-
import { homedir } from 'node:os';
|
|
1
|
+
import { readFile, writeFile, stat } from 'node:fs/promises';
|
|
3
2
|
import { join, dirname, resolve } from 'node:path';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
3
|
+
import { appHomePath, BRAND, persistenceEnabled, worklogEnabled } from './brand.js';
|
|
4
|
+
import { redactKey } from './providers/keys.js';
|
|
5
|
+
import { loadStore, saveStore, mergeFact, maybeConsolidate, consolidate, renderPromptBlock } from './memory-store.js';
|
|
6
|
+
const MEMORY_FILE = BRAND.memoryFileName;
|
|
7
|
+
// auto-memory (สิ่งที่ agent จำเองข้าม session) ย้ายไปอยู่ใน ./memory-store.ts —
|
|
8
|
+
// memory.json เป็น source of truth, MEMORY.md เป็น view ที่ render จากมัน
|
|
8
9
|
// เดินขึ้นหยุดที่ project root — ไม่เลยขึ้นไปถึง filesystem root
|
|
9
10
|
// (กัน prompt-injection จาก SANOOK.md ที่ใครก็วางใน parent dir ที่ share กันได้)
|
|
10
11
|
const BOUNDARY_MARKERS = ['.git', 'package.json'];
|
|
@@ -40,7 +41,7 @@ export async function loadMemory(cwd = process.cwd()) {
|
|
|
40
41
|
dir = parent;
|
|
41
42
|
}
|
|
42
43
|
chain.reverse(); // project root ก่อน → cwd ท้าย (local override general)
|
|
43
|
-
const paths = [
|
|
44
|
+
const paths = [appHomePath(MEMORY_FILE), ...chain.map((d) => join(d, MEMORY_FILE))];
|
|
44
45
|
const blocks = [];
|
|
45
46
|
const seen = new Set();
|
|
46
47
|
for (const p of paths) {
|
|
@@ -58,11 +59,14 @@ export async function loadMemory(cwd = process.cwd()) {
|
|
|
58
59
|
}
|
|
59
60
|
return blocks.join('\n\n');
|
|
60
61
|
}
|
|
61
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* โหลด auto-memory เข้า system prompt — render จาก structured store (./memory-store.ts)
|
|
64
|
+
* เป็น block ที่ rank + cap แล้ว (top facts ตาม importance·recency, ≤ ~2k token กัน context-rot)
|
|
65
|
+
* contract เดิม: '' ถ้าว่าง, ไม่งั้นคืน <auto_memory> block เดียวที่ self-contained
|
|
66
|
+
*/
|
|
62
67
|
export async function loadAutoMemory() {
|
|
63
68
|
try {
|
|
64
|
-
|
|
65
|
-
return content ? `<auto_memory note="สิ่งที่จำไว้จาก session ก่อน">\n${content}\n</auto_memory>` : '';
|
|
69
|
+
return renderPromptBlock(await loadStore());
|
|
66
70
|
}
|
|
67
71
|
catch {
|
|
68
72
|
return '';
|
|
@@ -74,35 +78,145 @@ export async function loadAutoMemory() {
|
|
|
74
78
|
* brainPath มาจาก ~/.sanook/config.json · ไม่มี/ไฟล์หาย → คืน '' (เงียบ)
|
|
75
79
|
*/
|
|
76
80
|
export async function loadBrainContext() {
|
|
81
|
+
const brainPath = await getBrainPath();
|
|
82
|
+
return brainPath ? buildBrainContext(brainPath) : '';
|
|
83
|
+
}
|
|
84
|
+
/** ประกอบ brain context จาก vault path (pure → testable) — entry + current-state + remembered facts */
|
|
85
|
+
export async function buildBrainContext(brainPath) {
|
|
86
|
+
const parts = [];
|
|
87
|
+
// 1. entry/pointers (Vault Structure Map ฯลฯ)
|
|
88
|
+
const idx = await readTrimmed(join(brainPath, 'Shared', 'AI-Context-Index.md'), 3000);
|
|
89
|
+
if (idx)
|
|
90
|
+
parts.push(idx);
|
|
91
|
+
// 2. current-state — เนื้อจริง (live focus) ไม่ใช่แค่ pointer
|
|
92
|
+
const cs = await readTrimmed(join(brainPath, 'Shared', 'Operating-State', 'current-state.md'), 1500);
|
|
93
|
+
if (cs)
|
|
94
|
+
parts.push(`## current-state\n${cs}`);
|
|
95
|
+
// 3. ปิด loop: fact ที่ remember ไว้ (Memory-Inbox) กลับเข้า context — ไม่งั้น vault = write-only
|
|
96
|
+
const inbox = await inboxCandidates(join(brainPath, 'Shared', 'Memory-Inbox', 'memory-inbox.md'), 1200);
|
|
97
|
+
if (inbox)
|
|
98
|
+
parts.push(`## remembered (Memory-Inbox)\n${inbox}`);
|
|
99
|
+
if (!parts.length)
|
|
100
|
+
return '';
|
|
101
|
+
return `<brain_vault path="${brainPath}" note="second-brain ของ user — สิ่งที่จำไว้/state ปัจจุบันอยู่ใน block นี้; route โน้ตตาม Vault Structure Map; อ่าน/เขียนไฟล์ใน vault ด้วย absolute path ได้">\n${parts.join('\n\n')}\n</brain_vault>`;
|
|
102
|
+
}
|
|
103
|
+
/** อ่านไฟล์ + trim หัว N ตัว ('' ถ้าไม่มี/ว่าง) */
|
|
104
|
+
async function readTrimmed(p, max) {
|
|
77
105
|
try {
|
|
78
|
-
const
|
|
79
|
-
if (!
|
|
106
|
+
const c = (await readFile(p, 'utf8')).trim();
|
|
107
|
+
if (!c)
|
|
80
108
|
return '';
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
109
|
+
return c.length > max ? `${c.slice(0, max)}\n…` : c;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return '';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** ดึงรายการ "- ..." ใต้ "## New Candidates" จาก memory-inbox (fact ที่ remember ไว้) */
|
|
116
|
+
async function inboxCandidates(p, max) {
|
|
117
|
+
try {
|
|
118
|
+
const after = (await readFile(p, 'utf8')).split('## New Candidates')[1];
|
|
119
|
+
if (!after)
|
|
84
120
|
return '';
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
121
|
+
const lines = after
|
|
122
|
+
.split('\n')
|
|
123
|
+
.filter((l) => l.trim().startsWith('- ') && !l.includes('_('))
|
|
124
|
+
.map((l) => l.trim());
|
|
125
|
+
const text = lines.join('\n').trim();
|
|
126
|
+
return text.length > max ? `${text.slice(0, max)}\n…` : text;
|
|
88
127
|
}
|
|
89
128
|
catch {
|
|
90
129
|
return '';
|
|
91
130
|
}
|
|
92
131
|
}
|
|
93
|
-
/**
|
|
94
|
-
export async function
|
|
95
|
-
const line = `- ${fact.trim().replace(/\s+/g, ' ')}`;
|
|
96
|
-
await mkdir(AUTO_MEMORY_DIR, { recursive: true });
|
|
97
|
-
let existing = '';
|
|
132
|
+
/** path ของ second-brain vault จาก config (undefined = ไม่ได้ตั้ง) */
|
|
133
|
+
export async function getBrainPath() {
|
|
98
134
|
try {
|
|
99
|
-
|
|
135
|
+
const cfg = JSON.parse(await readFile(appHomePath('config.json'), 'utf8'));
|
|
136
|
+
return cfg.brainPath;
|
|
100
137
|
}
|
|
101
138
|
catch {
|
|
102
|
-
|
|
139
|
+
return undefined;
|
|
103
140
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* route fact เข้า vault Memory-Inbox (candidate buffer ตาม §4) — "AI เขียนลง second brain ของคุณ"
|
|
144
|
+
* เขียนเฉพาะถ้า memory-inbox.md มีจริง (กันสร้างไฟล์ใน path ที่ไม่ใช่ vault) · คืน true ถ้าเขียน
|
|
145
|
+
*/
|
|
146
|
+
export async function appendToVaultInbox(brainPath, fact) {
|
|
147
|
+
const p = join(brainPath, 'Shared', 'Memory-Inbox', 'memory-inbox.md');
|
|
148
|
+
let content;
|
|
149
|
+
try {
|
|
150
|
+
content = await readFile(p, 'utf8');
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return false; // ไม่ใช่ vault ที่มีไฟล์นี้ → ไม่ route
|
|
154
|
+
}
|
|
155
|
+
const safeFact = redactKey(fact);
|
|
156
|
+
const line = `- ${safeFact.trim().replace(/\s+/g, ' ')}`;
|
|
157
|
+
if (content.includes(line))
|
|
158
|
+
return false; // dedup
|
|
159
|
+
const marker = '## New Candidates';
|
|
160
|
+
const next = content.includes(marker)
|
|
161
|
+
? content.replace(marker, `${marker}\n${line}`)
|
|
162
|
+
: `${content.trimEnd()}\n${line}\n`;
|
|
163
|
+
await writeFile(p, next);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
/** บันทึก worklog ย่อเข้า vault Sessions/ (รายวัน) — "second brain จำว่าวันนี้ทำอะไร" */
|
|
167
|
+
export async function appendBrainWorklog(brainPath, entry) {
|
|
168
|
+
if (!worklogEnabled())
|
|
169
|
+
return false;
|
|
170
|
+
const dir = join(brainPath, 'Sessions');
|
|
171
|
+
if (!(await exists(dir)))
|
|
172
|
+
return false; // ไม่ใช่ vault → ข้าม
|
|
173
|
+
const topic = entry.prompt.trim().split(/\s+/).slice(0, 6).join(' ').slice(0, 50) || 'work';
|
|
174
|
+
const file = join(dir, `${entry.today}-worklog.md`);
|
|
175
|
+
let content;
|
|
176
|
+
try {
|
|
177
|
+
content = await readFile(file, 'utf8');
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
content = `---\ntags: [session, session-log, worklog]\nnote_type: session-log\ncreated: ${entry.today}\nupdated: ${entry.today}\nparent: "[[Sessions/_Index]]"\nai_surface: history\n---\n\n# ${entry.today} — Worklog (auto by ${BRAND.cliName})\n\nup:: [[Sessions/_Index]]\n`;
|
|
181
|
+
}
|
|
182
|
+
const block = `\n## ${topic}\n- prompt: ${redactKey(entry.prompt).trim().slice(0, 200)}\n- model: ${entry.model}\n- ${redactKey(entry.summary).trim().slice(0, 300)}\n`;
|
|
183
|
+
// แทรกก่อน up:: ท้ายไฟล์ (กัน up:: หลุดไปกลาง)
|
|
184
|
+
const out = content.includes('\nup:: ')
|
|
185
|
+
? content.replace(/\nup:: .*$/s, `\n${block}\nup:: [[Sessions/_Index]]\n`)
|
|
186
|
+
: `${content.trimEnd()}\n${block}`;
|
|
187
|
+
await writeFile(file, out);
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
// in-process write serializer: the AI SDK runs tool calls from one model step concurrently, so two
|
|
191
|
+
// `remember` calls in a turn would otherwise load → mergeFact → save on the SAME baseline and the
|
|
192
|
+
// last save would clobber the first (lost update). Chaining the read-modify-write makes them sequential.
|
|
193
|
+
let memWriteChain = Promise.resolve();
|
|
194
|
+
function withMemLock(fn) {
|
|
195
|
+
const run = memWriteChain.then(fn, fn); // run regardless of the prior task's outcome
|
|
196
|
+
memWriteChain = run.catch(() => { }); // a rejection must not break the chain for the next writer
|
|
197
|
+
return run;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* บันทึก fact ลง auto-memory (remember tool เรียก) — "Merge, Don't Append":
|
|
201
|
+
* โหลด store → mergeFact (ADD/UPDATE/NOOP/SUPERSEDE) → save (ถ้าไม่ใช่ no-write op)
|
|
202
|
+
* → consolidate เป็นระยะ → route เข้า vault inbox (best-effort) เหมือนเดิม
|
|
203
|
+
* read-modify-write ของ store และ vault inbox ถูก serialize ด้วย withMemLock กัน lost-update ตอน parallel remember
|
|
204
|
+
*/
|
|
205
|
+
export async function appendMemory(fact, noteType) {
|
|
206
|
+
if (!persistenceEnabled())
|
|
207
|
+
return;
|
|
208
|
+
const safeFact = redactKey(fact);
|
|
209
|
+
await withMemLock(async () => {
|
|
210
|
+
const store = await loadStore();
|
|
211
|
+
const { store: next, op } = mergeFact(store, { text: safeFact, trust: 'agent', noteType });
|
|
212
|
+
// PROTECTED_HALT = ไม่เขียน (ขัดกับ protected fact); op อื่นเขียนหมด (NOOP ก็เขียนเพราะ touch accessCount)
|
|
213
|
+
if (op !== 'PROTECTED_HALT') {
|
|
214
|
+
const toSave = maybeConsolidate(next) ? consolidate(next).store : next;
|
|
215
|
+
await saveStore(toSave);
|
|
216
|
+
}
|
|
217
|
+
// route เข้า vault second-brain ด้วย (best-effort) — ส่ง plain redacted string เหมือนเดิม
|
|
218
|
+
const brain = await getBrainPath();
|
|
219
|
+
if (brain)
|
|
220
|
+
await appendToVaultInbox(brain, safeFact).catch(() => false);
|
|
221
|
+
});
|
|
108
222
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// src/orchestrate.ts — subagent ORCHESTRATION (parallel fan-out + background).
|
|
3
|
+
//
|
|
4
|
+
// The single Task subagent (src/tools/task.ts) is one-shot and synchronous. This
|
|
5
|
+
// module adds the two missing orchestration primitives a frontier harness needs:
|
|
6
|
+
// 1. runParallel() — fan a list of subagents out concurrently with a real
|
|
7
|
+
// concurrency cap and PER-ITEM error isolation (one failure never sinks the
|
|
8
|
+
// batch), results returned in input order.
|
|
9
|
+
// 2. TaskRegistry — fire-and-forget BACKGROUND subagents: spawn() returns an id
|
|
10
|
+
// immediately, the work runs detached, and collect()/list()/cancel() let the
|
|
11
|
+
// main agent keep working and gather results later in the same session.
|
|
12
|
+
//
|
|
13
|
+
// Everything is PURE w.r.t. the actual agent: the subagent runner is INJECTED
|
|
14
|
+
// (SubagentRunner), and the clock + id generator are injectable, so the whole
|
|
15
|
+
// orchestration layer unit-tests with a fake runner — zero model calls, zero
|
|
16
|
+
// network — exactly like the search subsystem injects its fs.
|
|
17
|
+
// ============================================================================
|
|
18
|
+
const DEFAULT_CONCURRENCY = 5;
|
|
19
|
+
/**
|
|
20
|
+
* Run thunks concurrently, capped at `concurrency`, results in input order.
|
|
21
|
+
* The generic concurrency primitive both runParallel and worktree isolation use.
|
|
22
|
+
*/
|
|
23
|
+
export async function runThunks(thunks, concurrency = DEFAULT_CONCURRENCY) {
|
|
24
|
+
const cap = Math.max(1, Math.floor(concurrency));
|
|
25
|
+
const results = new Array(thunks.length);
|
|
26
|
+
let next = 0;
|
|
27
|
+
const worker = async () => {
|
|
28
|
+
for (;;) {
|
|
29
|
+
const i = next++;
|
|
30
|
+
if (i >= thunks.length)
|
|
31
|
+
return;
|
|
32
|
+
results[i] = await thunks[i]();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
await Promise.all(Array.from({ length: Math.min(cap, thunks.length) }, worker));
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
38
|
+
async function runOne(spec, runner, signal) {
|
|
39
|
+
try {
|
|
40
|
+
const text = await runner(spec, signal);
|
|
41
|
+
return { ok: true, description: spec.description, text };
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
return { ok: false, description: spec.description, text: '', error: e.message };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Run subagents concurrently, capped at `concurrency`, results in input order.
|
|
49
|
+
* Never rejects: a thrown subagent becomes an {ok:false,error} outcome so the
|
|
50
|
+
* caller always gets one outcome per spec.
|
|
51
|
+
*/
|
|
52
|
+
export async function runParallel(specs, runner, opts = {}) {
|
|
53
|
+
return runThunks(specs.map((s) => () => runOne(s, runner, opts.signal)), opts.concurrency ?? DEFAULT_CONCURRENCY);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* In-process registry of BACKGROUND subagents. spawn() launches detached work and
|
|
57
|
+
* returns an id; collect() awaits (optionally with a timeout so the agent can poll
|
|
58
|
+
* instead of block); cancel() aborts via the runner's AbortSignal. Lives for the
|
|
59
|
+
* process — background work dies when the CLI exits, so it is for within-session
|
|
60
|
+
* fan-out ("kick off research, keep coding, gather it later"), not durable jobs
|
|
61
|
+
* (use `schedule_task` / the gateway for those).
|
|
62
|
+
*/
|
|
63
|
+
export class TaskRegistry {
|
|
64
|
+
tasks = new Map();
|
|
65
|
+
settles = new Map();
|
|
66
|
+
controllers = new Map();
|
|
67
|
+
counter = 0;
|
|
68
|
+
now;
|
|
69
|
+
idGen;
|
|
70
|
+
constructor(opts = {}) {
|
|
71
|
+
this.now = opts.now ?? Date.now;
|
|
72
|
+
this.idGen = opts.idGen ?? (() => `t${++this.counter}`);
|
|
73
|
+
}
|
|
74
|
+
/** launch a detached subagent; returns its id immediately. */
|
|
75
|
+
spawn(spec, runner) {
|
|
76
|
+
const id = this.idGen();
|
|
77
|
+
const ac = new AbortController();
|
|
78
|
+
this.controllers.set(id, ac);
|
|
79
|
+
const rec = { id, description: spec.description, state: 'running', startedMs: this.now() };
|
|
80
|
+
this.tasks.set(id, rec);
|
|
81
|
+
const settle = (async () => {
|
|
82
|
+
try {
|
|
83
|
+
const text = await runner(spec, ac.signal);
|
|
84
|
+
const cur = this.tasks.get(id);
|
|
85
|
+
if (cur.state !== 'canceled')
|
|
86
|
+
Object.assign(cur, { state: 'done', text, endedMs: this.now() });
|
|
87
|
+
return cur;
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
const cur = this.tasks.get(id);
|
|
91
|
+
if (cur.state !== 'canceled')
|
|
92
|
+
Object.assign(cur, { state: 'error', error: e.message, endedMs: this.now() });
|
|
93
|
+
return cur;
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
this.controllers.delete(id);
|
|
97
|
+
}
|
|
98
|
+
})();
|
|
99
|
+
// swallow at the source — collect() is the consumer; an uncollected reject must not crash the process
|
|
100
|
+
settle.catch(() => { });
|
|
101
|
+
this.settles.set(id, settle);
|
|
102
|
+
return id;
|
|
103
|
+
}
|
|
104
|
+
get(id) {
|
|
105
|
+
return this.tasks.get(id);
|
|
106
|
+
}
|
|
107
|
+
list() {
|
|
108
|
+
return [...this.tasks.values()];
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Await a background task. With timeoutMs, resolves to the current (possibly
|
|
112
|
+
* still-running) record if it hasn't settled in time, so the caller can poll.
|
|
113
|
+
* Returns undefined for an unknown id.
|
|
114
|
+
*/
|
|
115
|
+
async collect(id, timeoutMs) {
|
|
116
|
+
const settle = this.settles.get(id);
|
|
117
|
+
if (!settle)
|
|
118
|
+
return undefined;
|
|
119
|
+
if (timeoutMs == null)
|
|
120
|
+
return settle;
|
|
121
|
+
let timer;
|
|
122
|
+
const timeout = new Promise((resolve) => {
|
|
123
|
+
timer = setTimeout(() => resolve(this.tasks.get(id)), Math.max(0, timeoutMs));
|
|
124
|
+
});
|
|
125
|
+
try {
|
|
126
|
+
return await Promise.race([settle, timeout]);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
if (timer)
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** abort a running task (best-effort via its AbortSignal). Returns false if not running. */
|
|
134
|
+
cancel(id) {
|
|
135
|
+
const rec = this.tasks.get(id);
|
|
136
|
+
if (!rec || rec.state !== 'running')
|
|
137
|
+
return false;
|
|
138
|
+
this.controllers.get(id)?.abort();
|
|
139
|
+
Object.assign(rec, { state: 'canceled', endedMs: this.now() });
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
/** number of tasks still running — used to gate runaway fan-out. */
|
|
143
|
+
runningCount() {
|
|
144
|
+
let n = 0;
|
|
145
|
+
for (const r of this.tasks.values())
|
|
146
|
+
if (r.state === 'running')
|
|
147
|
+
n++;
|
|
148
|
+
return n;
|
|
149
|
+
}
|
|
150
|
+
}
|
package/dist/providers/codex.js
CHANGED
|
@@ -5,9 +5,20 @@ import { join } from 'node:path';
|
|
|
5
5
|
/** เช็กว่า codex CLI ติดตั้ง + login ChatGPT แล้ว */
|
|
6
6
|
export async function detectCodex() {
|
|
7
7
|
const hasBinary = await new Promise((resolve) => {
|
|
8
|
-
const p = spawn('codex', ['--version']);
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const p = spawn('codex', ['--version'], { shell: process.platform === 'win32' });
|
|
9
|
+
// timeout: binary ค้าง (shim รอ stdin / Gatekeeper stall ตอนรันครั้งแรกบน macOS) → ไม่ให้ wizard ตัน
|
|
10
|
+
const timer = setTimeout(() => {
|
|
11
|
+
p.kill();
|
|
12
|
+
resolve(false);
|
|
13
|
+
}, 5000);
|
|
14
|
+
p.on('error', () => {
|
|
15
|
+
clearTimeout(timer);
|
|
16
|
+
resolve(false);
|
|
17
|
+
});
|
|
18
|
+
p.on('close', (code) => {
|
|
19
|
+
clearTimeout(timer);
|
|
20
|
+
resolve(code === 0);
|
|
21
|
+
});
|
|
11
22
|
});
|
|
12
23
|
if (!hasBinary) {
|
|
13
24
|
return { installed: false, loggedIn: false, reason: 'ไม่พบ codex CLI — ติดตั้ง: npm i -g @openai/codex' };
|
|
@@ -26,16 +37,19 @@ export async function detectCodex() {
|
|
|
26
37
|
* tolerant ต่อ malformed JSONL (codex bug #15451: --json ถูก ignore เมื่อมี tools active)
|
|
27
38
|
*/
|
|
28
39
|
export async function runCodex(opts) {
|
|
29
|
-
|
|
40
|
+
// --ask-for-approval never: รัน non-interactive ไม่ค้างรอ approval (ปลอดภัยเพราะ default sandbox = read-only)
|
|
41
|
+
const args = ['exec', '--skip-git-repo-check', '--sandbox', opts.sandbox ?? 'read-only', '--ask-for-approval', 'never', '--json'];
|
|
30
42
|
if (opts.model)
|
|
31
43
|
args.push('-m', opts.model);
|
|
32
44
|
if (opts.resumeThreadId)
|
|
33
45
|
args.push('resume', opts.resumeThreadId);
|
|
34
46
|
args.push('-'); // prompt via stdin
|
|
35
47
|
return new Promise((resolve, reject) => {
|
|
36
|
-
// OPENAI_API_KEY
|
|
37
|
-
|
|
38
|
-
const
|
|
48
|
+
// ลบ OPENAI_API_KEY ออกจาก env ของ child — กัน BYOK key ของ Sanook ไป override/ชนกับ ChatGPT login
|
|
49
|
+
// (codex bug #2733/#3286: ตั้ง OPENAI_API_KEY ค้าง env ทำให้ ChatGPT-plan auth วน loop sign-in)
|
|
50
|
+
const env = { ...process.env };
|
|
51
|
+
delete env.OPENAI_API_KEY;
|
|
52
|
+
const p = spawn('codex', args, { env, shell: process.platform === 'win32' }); // Windows: codex = JS shim ผ่าน .cmd → ต้อง shell
|
|
39
53
|
let finalText = '';
|
|
40
54
|
let threadId;
|
|
41
55
|
let buf = '';
|
package/dist/providers/keys.js
CHANGED
|
@@ -28,10 +28,11 @@ export function assertDirectApiKey(policy, key) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
if (policy.keyFormat && !policy.keyFormat.test(k)) {
|
|
31
|
-
|
|
31
|
+
const hint = policy.keyExample ? `ขึ้นต้นแบบ ${policy.keyExample}` : `ขึ้นต้นตาม ${policy.keyFormat.source}`;
|
|
32
|
+
throw new Error(`${policy.label}: format ของ API key ไม่ถูกต้อง — เช็ก/วางใหม่ (${hint})`);
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
/** ปิดบัง API key ในข้อความ log/error — เก็บแค่หัว 4 + ท้าย 2 ตัว */
|
|
35
36
|
export function redactKey(s) {
|
|
36
|
-
return s.replace(/\b(sk-[A-Za-z0-9_-]{6,}|AIza[A-Za-z0-9_-]{10,}|xai-[A-Za-z0-9]{10,}|gsk_[A-Za-z0-9]{10,}|[A-Za-z0-9_-]{24,})\b/g, (m) => (m.length > 8 ? `${m.slice(0, 4)}…${m.slice(-2)}` : '…'));
|
|
37
|
+
return s.replace(/\b(AKIA[0-9A-Z]{16}|sk-[A-Za-z0-9_-]{6,}|AIza[A-Za-z0-9_-]{10,}|xai-[A-Za-z0-9]{10,}|gsk_[A-Za-z0-9]{10,}|[A-Za-z0-9_-]{24,})\b/g, (m) => (m.length > 8 ? `${m.slice(0, 4)}…${m.slice(-2)}` : '…'));
|
|
37
38
|
}
|
package/dist/providers/models.js
CHANGED
|
@@ -43,13 +43,29 @@ export async function listRemoteModels(cfg, key, timeoutMs = 6000) {
|
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
45
|
* merge: curated alias (registry — มี label สื่อความหมาย) นำหน้า + remote id ที่เหลือต่อท้าย
|
|
46
|
-
* dedup ด้วย model id
|
|
46
|
+
* dedup ด้วย model id — alias หลายตัวที่ชี้ id เดียวกัน (เช่น haiku/fast → claude-haiku-4-5,
|
|
47
|
+
* smart/gpt → gpt-5.5) ต้องรวมเป็น "haiku / fast — id" บรรทัดเดียว ไม่งั้น value ซ้ำ → React key ชน
|
|
48
|
+
* → ตัวเลือกโผล่ซ้ำ/หาย (bug "มีตัวเลือกสองตัวเลือกเป็น model เดียวกัน"). ใช้ทั้ง setup wizard และ /model picker
|
|
47
49
|
*/
|
|
48
50
|
export function mergeModelOptions(cfg, remote = []) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const
|
|
51
|
+
// group alias ทั้งหมดตาม id (รวม 'default' ด้วย — กัน id ที่มีแต่ alias 'default' เช่น lmstudio:local-model,
|
|
52
|
+
// ollama:qwen3 หายไปจนเลือกไม่ได้/Select ว่าง). ตอนทำ label ค่อยซ่อนคำ "default" ถ้ามีชื่ออื่นอยู่แล้ว
|
|
53
|
+
const aliasesById = new Map();
|
|
54
|
+
const order = []; // คง first-seen order ของ id
|
|
55
|
+
for (const [alias, id] of Object.entries(cfg.models)) {
|
|
56
|
+
if (!aliasesById.has(id)) {
|
|
57
|
+
aliasesById.set(id, []);
|
|
58
|
+
order.push(id);
|
|
59
|
+
}
|
|
60
|
+
aliasesById.get(id)?.push(alias);
|
|
61
|
+
}
|
|
62
|
+
const curated = order.map((id) => {
|
|
63
|
+
const aliases = aliasesById.get(id) ?? [];
|
|
64
|
+
const named = aliases.filter((a) => a !== 'default');
|
|
65
|
+
const shown = named.length ? named : aliases; // มีแต่ 'default' → โชว์ 'default' (ดีกว่าซ่อน id หายไป)
|
|
66
|
+
return { id, label: `${shown.join(' / ')} — ${id}` };
|
|
67
|
+
});
|
|
68
|
+
const seen = new Set(order);
|
|
69
|
+
const extra = [...new Set(remote)].filter((id) => id && !seen.has(id)).map((id) => ({ id, label: id }));
|
|
54
70
|
return [...curated, ...extra].map((o) => ({ label: o.label, value: o.id }));
|
|
55
71
|
}
|