sanook-cli 0.4.0 → 0.5.0

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.
Files changed (235) hide show
  1. package/.env.example +19 -0
  2. package/CHANGELOG.md +144 -0
  3. package/README.md +153 -20
  4. package/README.th.md +136 -0
  5. package/dist/agentContext.js +4 -0
  6. package/dist/approval.js +6 -0
  7. package/dist/bin.js +394 -51
  8. package/dist/brain.js +92 -59
  9. package/dist/brand.js +47 -0
  10. package/dist/checkpoint.js +37 -0
  11. package/dist/commands.js +86 -6
  12. package/dist/compaction.js +76 -5
  13. package/dist/config.js +100 -12
  14. package/dist/cost.js +60 -3
  15. package/dist/doctor.js +92 -0
  16. package/dist/gateway/auth.js +2 -2
  17. package/dist/gateway/ledger.js +2 -2
  18. package/dist/gateway/scheduler.js +1 -0
  19. package/dist/gateway/serve.js +6 -4
  20. package/dist/gateway/server.js +10 -2
  21. package/dist/git.js +11 -2
  22. package/dist/hooks.js +43 -17
  23. package/dist/knowledge.js +48 -49
  24. package/dist/loop.js +182 -66
  25. package/dist/lsp/client.js +173 -0
  26. package/dist/lsp/framing.js +56 -0
  27. package/dist/lsp/index.js +138 -0
  28. package/dist/lsp/servers.js +82 -0
  29. package/dist/mcp-server.js +244 -0
  30. package/dist/mcp.js +184 -29
  31. package/dist/memory-store.js +559 -0
  32. package/dist/memory.js +143 -29
  33. package/dist/orchestrate.js +150 -0
  34. package/dist/providers/codex.js +2 -2
  35. package/dist/providers/keys.js +3 -2
  36. package/dist/providers/registry.js +133 -1
  37. package/dist/repomap.js +93 -0
  38. package/dist/search/chunk.js +158 -0
  39. package/dist/search/embed-store.js +187 -0
  40. package/dist/search/engine.js +203 -0
  41. package/dist/search/fuse.js +35 -0
  42. package/dist/search/index-core.js +187 -0
  43. package/dist/search/indexer.js +241 -0
  44. package/dist/search/store.js +77 -0
  45. package/dist/session.js +42 -8
  46. package/dist/skill-install.js +10 -10
  47. package/dist/skills.js +12 -9
  48. package/dist/summarize.js +31 -0
  49. package/dist/tools/bash.js +21 -2
  50. package/dist/tools/diagnostics.js +41 -0
  51. package/dist/tools/edit.js +29 -7
  52. package/dist/tools/index.js +8 -1
  53. package/dist/tools/list.js +7 -2
  54. package/dist/tools/permission.js +90 -9
  55. package/dist/tools/read.js +23 -4
  56. package/dist/tools/remember.js +1 -1
  57. package/dist/tools/sandbox.js +61 -0
  58. package/dist/tools/search.js +105 -4
  59. package/dist/tools/task.js +195 -29
  60. package/dist/tools/timeout.js +35 -0
  61. package/dist/tools/util.js +10 -0
  62. package/dist/tools/write.js +6 -4
  63. package/dist/trust.js +89 -0
  64. package/dist/ui/app.js +218 -27
  65. package/dist/ui/banner.js +4 -9
  66. package/dist/ui/history.js +30 -0
  67. package/dist/ui/mentions.js +44 -0
  68. package/dist/ui/setup.js +6 -5
  69. package/dist/ui/useEditor.js +83 -0
  70. package/dist/update.js +114 -0
  71. package/dist/worktree.js +173 -0
  72. package/package.json +11 -5
  73. package/scripts/postinstall.mjs +33 -0
  74. package/second-brain/.agents/_Index.md +30 -0
  75. package/second-brain/.agents/skills/_Index.md +30 -0
  76. package/second-brain/.agents/workflows/_Index.md +30 -0
  77. package/second-brain/AGENTS.md +4 -4
  78. package/second-brain/Acceptance/_Index.md +30 -0
  79. package/second-brain/Acceptance/golden-case-template.md +39 -0
  80. package/second-brain/Areas/_Index.md +30 -0
  81. package/second-brain/Bugs/System-OS/_Index.md +30 -0
  82. package/second-brain/Bugs/_Index.md +30 -0
  83. package/second-brain/CLAUDE.md +4 -1
  84. package/second-brain/Checklists/_Index.md +30 -0
  85. package/second-brain/Checklists/preflight-postflight-template.md +29 -0
  86. package/second-brain/Distillations/_Index.md +30 -0
  87. package/second-brain/Entities/_Index.md +30 -0
  88. package/second-brain/Entities/entity-template.md +33 -0
  89. package/second-brain/Evals/_Index.md +30 -0
  90. package/second-brain/Evals/correction-pairs.md +24 -0
  91. package/second-brain/Evals/failure-taxonomy.md +24 -0
  92. package/second-brain/Evals/golden-set.md +25 -0
  93. package/second-brain/Evals/quality-ledger.md +23 -0
  94. package/second-brain/Evals/self-eval-rubric.md +23 -0
  95. package/second-brain/GEMINI.md +4 -4
  96. package/second-brain/Goals/_Index.md +30 -0
  97. package/second-brain/Handoffs/_Index.md +30 -0
  98. package/second-brain/Home.md +7 -0
  99. package/second-brain/Intake/Raw Sources/_Index.md +30 -0
  100. package/second-brain/Intake/_Index.md +30 -0
  101. package/second-brain/Intake/_Quarantine/_Index.md +30 -0
  102. package/second-brain/Learning/_Index.md +30 -0
  103. package/second-brain/Playbooks/_Index.md +30 -0
  104. package/second-brain/Playbooks/playbook-template.md +23 -0
  105. package/second-brain/Projects/_Index.md +30 -0
  106. package/second-brain/Prompts/_Index.md +30 -0
  107. package/second-brain/README.md +2 -1
  108. package/second-brain/Research/_Index.md +30 -0
  109. package/second-brain/Retrospectives/_Index.md +30 -0
  110. package/second-brain/Reviews/_Index.md +30 -0
  111. package/second-brain/Runbooks/_Index.md +30 -0
  112. package/second-brain/Runbooks/eval-loop.md +24 -0
  113. package/second-brain/Sessions/_Index.md +30 -0
  114. package/second-brain/Shared/AI-Context-Index.md +20 -0
  115. package/second-brain/Shared/AI-Threads/_Index.md +30 -0
  116. package/second-brain/Shared/Archive/_Index.md +30 -0
  117. package/second-brain/Shared/Assets/_Index.md +30 -0
  118. package/second-brain/Shared/Context-Packs/_Index.md +30 -0
  119. package/second-brain/Shared/Context7-Docs/_Index.md +30 -0
  120. package/second-brain/Shared/Coordination/NOW.md +28 -0
  121. package/second-brain/Shared/Coordination/_Index.md +30 -0
  122. package/second-brain/Shared/Coordination/agent-registry.md +24 -0
  123. package/second-brain/Shared/Coordination/task-board/_Index.md +30 -0
  124. package/second-brain/Shared/Coordination/task-board/task-template.md +43 -0
  125. package/second-brain/Shared/Coordination/task-board.md +32 -0
  126. package/second-brain/Shared/Core-Facts/_Index.md +30 -0
  127. package/second-brain/Shared/Decision-Memory/_Index.md +30 -0
  128. package/second-brain/Shared/Glossary/_Index.md +30 -0
  129. package/second-brain/Shared/Memory-Inbox/_Index.md +30 -0
  130. package/second-brain/Shared/Operating-State/_Index.md +30 -0
  131. package/second-brain/Shared/Prompting/_Index.md +30 -0
  132. package/second-brain/Shared/Provenance/_Index.md +30 -0
  133. package/second-brain/Shared/Rules/_Index.md +30 -0
  134. package/second-brain/Shared/Rules/contextual-note-rule.md +30 -0
  135. package/second-brain/Shared/Rules/frontmatter-standard.md +10 -0
  136. package/second-brain/Shared/Rules/memory-write-protocol.md +28 -0
  137. package/second-brain/Shared/Rules/procedural-runbook-header.md +40 -0
  138. package/second-brain/Shared/Rules/review-and-staleness-policy.md +22 -0
  139. package/second-brain/Shared/Rules/rules-formatting.md +34 -0
  140. package/second-brain/Shared/Scripts/_Index.md +30 -0
  141. package/second-brain/Shared/Scripts-Archive/_Index.md +30 -0
  142. package/second-brain/Shared/Tech-Standards/_Index.md +30 -0
  143. package/second-brain/Shared/Tech-Standards/verification-standard.md +40 -0
  144. package/second-brain/Shared/User-Memory/_Index.md +30 -0
  145. package/second-brain/Shared/User-Persona/_Index.md +30 -0
  146. package/second-brain/Shared/User-Persona/owner-profile.md +25 -0
  147. package/second-brain/Shared/Working-Memory/_Index.md +30 -0
  148. package/second-brain/Shared/_Index.md +30 -0
  149. package/second-brain/Shared/mcp-servers/_Index.md +30 -0
  150. package/second-brain/Skills/_Index.md +30 -0
  151. package/second-brain/Templates/_Index.md +30 -0
  152. package/second-brain/Templates/bug.md +2 -0
  153. package/second-brain/Templates/handoff.md +2 -0
  154. package/second-brain/Templates/session.md +2 -0
  155. package/second-brain/Tools/_Index.md +30 -0
  156. package/second-brain/Traces/_Index.md +30 -0
  157. package/second-brain/Vault Structure Map.md +33 -1
  158. package/second-brain/copilot/_Index.md +30 -0
  159. package/skills/audit-license-compliance/SKILL.md +117 -0
  160. package/skills/author-codemod/SKILL.md +110 -0
  161. package/skills/build-audit-logging/SKILL.md +112 -0
  162. package/skills/build-cdc-streaming-pipeline/SKILL.md +123 -0
  163. package/skills/build-cli-tool/SKILL.md +108 -0
  164. package/skills/build-data-table/SKILL.md +141 -0
  165. package/skills/build-native-mobile-ui/SKILL.md +154 -0
  166. package/skills/build-offline-first-sync/SKILL.md +118 -0
  167. package/skills/build-realtime-channel/SKILL.md +122 -0
  168. package/skills/build-vector-search/SKILL.md +131 -0
  169. package/skills/compose-local-dev-stack/SKILL.md +149 -0
  170. package/skills/configure-bundler-build/SKILL.md +166 -0
  171. package/skills/configure-dns-tls/SKILL.md +142 -0
  172. package/skills/configure-reverse-proxy-lb/SKILL.md +129 -0
  173. package/skills/configure-security-headers-csp/SKILL.md +122 -0
  174. package/skills/contract-testing/SKILL.md +140 -0
  175. package/skills/datetime-timezone-correctness/SKILL.md +125 -0
  176. package/skills/debug-ci-pipeline-failure/SKILL.md +134 -0
  177. package/skills/debug-flaky-tests/SKILL.md +128 -0
  178. package/skills/defend-llm-prompt-injection/SKILL.md +110 -0
  179. package/skills/deliver-webhooks/SKILL.md +116 -0
  180. package/skills/design-api-pagination/SKILL.md +144 -0
  181. package/skills/design-authorization-model/SKILL.md +119 -0
  182. package/skills/design-backup-dr-recovery/SKILL.md +113 -0
  183. package/skills/design-event-sourcing-cqrs/SKILL.md +143 -0
  184. package/skills/design-multi-tenancy/SKILL.md +100 -0
  185. package/skills/design-protobuf-grpc-service/SKILL.md +146 -0
  186. package/skills/design-relational-schema/SKILL.md +129 -0
  187. package/skills/design-search-index-infra/SKILL.md +151 -0
  188. package/skills/design-state-machine/SKILL.md +108 -0
  189. package/skills/design-token-system/SKILL.md +109 -0
  190. package/skills/distributed-locks-leases/SKILL.md +120 -0
  191. package/skills/encrypt-sensitive-data/SKILL.md +148 -0
  192. package/skills/feature-flags-rollout/SKILL.md +130 -0
  193. package/skills/file-upload-object-storage/SKILL.md +107 -0
  194. package/skills/fuzz-dynamic-security-test/SKILL.md +111 -0
  195. package/skills/harden-llm-app-reliability/SKILL.md +126 -0
  196. package/skills/i18n-localization-setup/SKILL.md +113 -0
  197. package/skills/idempotency-keys/SKILL.md +107 -0
  198. package/skills/implement-push-notifications/SKILL.md +142 -0
  199. package/skills/ingest-webhook-secure/SKILL.md +120 -0
  200. package/skills/integrate-oauth-oidc/SKILL.md +126 -0
  201. package/skills/load-stress-test/SKILL.md +129 -0
  202. package/skills/map-privacy-data-gdpr/SKILL.md +146 -0
  203. package/skills/model-nosql-data/SKILL.md +118 -0
  204. package/skills/money-decimal-arithmetic/SKILL.md +123 -0
  205. package/skills/monitor-ml-drift/SKILL.md +109 -0
  206. package/skills/numeric-precision-units/SKILL.md +144 -0
  207. package/skills/optimize-llm-cost-latency/SKILL.md +103 -0
  208. package/skills/optimize-react-rerenders/SKILL.md +124 -0
  209. package/skills/orchestrate-agent-workflow/SKILL.md +100 -0
  210. package/skills/payments-billing-integration/SKILL.md +114 -0
  211. package/skills/pin-toolchain-versions/SKILL.md +116 -0
  212. package/skills/plan-strangler-migration/SKILL.md +95 -0
  213. package/skills/property-based-testing/SKILL.md +108 -0
  214. package/skills/publish-package-registry/SKILL.md +130 -0
  215. package/skills/recover-git-state/SKILL.md +119 -0
  216. package/skills/remediate-web-vulnerabilities/SKILL.md +125 -0
  217. package/skills/resilience-timeouts-retries/SKILL.md +104 -0
  218. package/skills/resolve-merge-rebase-conflict/SKILL.md +97 -0
  219. package/skills/rewrite-git-history/SKILL.md +109 -0
  220. package/skills/scaffold-cross-platform-app/SKILL.md +137 -0
  221. package/skills/schema-evolution-compatibility/SKILL.md +121 -0
  222. package/skills/send-transactional-email/SKILL.md +126 -0
  223. package/skills/serve-deploy-ml-model/SKILL.md +107 -0
  224. package/skills/setup-cdn-edge-waf/SKILL.md +107 -0
  225. package/skills/setup-devcontainer-env/SKILL.md +131 -0
  226. package/skills/setup-lint-format-precommit/SKILL.md +140 -0
  227. package/skills/setup-monorepo-tooling/SKILL.md +125 -0
  228. package/skills/ship-mobile-app-store-release/SKILL.md +137 -0
  229. package/skills/structured-output-llm/SKILL.md +86 -0
  230. package/skills/supply-chain-sbom-provenance/SKILL.md +120 -0
  231. package/skills/test-data-factories/SKILL.md +158 -0
  232. package/skills/threat-model-stride/SKILL.md +123 -0
  233. package/skills/train-evaluate-ml-model/SKILL.md +109 -0
  234. package/skills/unicode-text-correctness/SKILL.md +109 -0
  235. package/skills/visual-regression-testing/SKILL.md +120 -0
package/dist/update.js ADDED
@@ -0,0 +1,114 @@
1
+ import { spawn } from 'node:child_process';
2
+ const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
3
+ export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
4
+ function packageUrl(registry, packageName) {
5
+ const base = registry.replace(/\/+$/, '') || DEFAULT_REGISTRY;
6
+ const encoded = encodeURIComponent(packageName).replace(/^%40/, '@');
7
+ return `${base}/${encoded}`;
8
+ }
9
+ function splitVersion(version) {
10
+ const [withoutBuild] = version.trim().replace(/^v/, '').split('+');
11
+ const [corePart, prereleasePart = ''] = withoutBuild.split('-', 2);
12
+ return {
13
+ core: corePart.split('.').map((part) => Number.parseInt(part, 10)).map((n) => (Number.isFinite(n) ? n : 0)),
14
+ prerelease: prereleasePart ? prereleasePart.split('.') : [],
15
+ };
16
+ }
17
+ function comparePrerelease(a, b) {
18
+ if (!a.length && !b.length)
19
+ return 0;
20
+ if (!a.length)
21
+ return 1;
22
+ if (!b.length)
23
+ return -1;
24
+ const len = Math.max(a.length, b.length);
25
+ for (let i = 0; i < len; i++) {
26
+ const pa = a[i];
27
+ const pb = b[i];
28
+ if (pa === undefined)
29
+ return -1;
30
+ if (pb === undefined)
31
+ return 1;
32
+ const na = /^\d+$/.test(pa) ? Number(pa) : Number.NaN;
33
+ const nb = /^\d+$/.test(pb) ? Number(pb) : Number.NaN;
34
+ if (Number.isFinite(na) && Number.isFinite(nb) && na !== nb)
35
+ return na > nb ? 1 : -1;
36
+ if (Number.isFinite(na) !== Number.isFinite(nb))
37
+ return Number.isFinite(na) ? -1 : 1;
38
+ if (pa !== pb)
39
+ return pa > pb ? 1 : -1;
40
+ }
41
+ return 0;
42
+ }
43
+ export function compareVersions(a, b) {
44
+ const va = splitVersion(a);
45
+ const vb = splitVersion(b);
46
+ const len = Math.max(va.core.length, vb.core.length, 3);
47
+ for (let i = 0; i < len; i++) {
48
+ const na = va.core[i] ?? 0;
49
+ const nb = vb.core[i] ?? 0;
50
+ if (na !== nb)
51
+ return na > nb ? 1 : -1;
52
+ }
53
+ return comparePrerelease(va.prerelease, vb.prerelease);
54
+ }
55
+ export function isNewerVersion(latest, current) {
56
+ return compareVersions(latest, current) > 0;
57
+ }
58
+ export function installCommand(packageName) {
59
+ return `npm install -g ${packageName}@latest`;
60
+ }
61
+ export function shouldCheckForUpdate(cache, nowMs = Date.now(), intervalMs = UPDATE_CHECK_INTERVAL_MS) {
62
+ if (!cache?.checkedAt)
63
+ return true;
64
+ const checkedAt = Date.parse(cache.checkedAt);
65
+ if (!Number.isFinite(checkedAt))
66
+ return true;
67
+ if (checkedAt > nowMs)
68
+ return true;
69
+ return nowMs - checkedAt >= intervalMs;
70
+ }
71
+ export async function fetchLatestVersion(meta, opts = {}) {
72
+ const fetchImpl = opts.fetchImpl ?? fetch;
73
+ const ctrl = new AbortController();
74
+ const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 8000);
75
+ try {
76
+ const res = await fetchImpl(packageUrl(opts.registry ?? process.env.npm_config_registry ?? DEFAULT_REGISTRY, meta.name), {
77
+ headers: { accept: 'application/vnd.npm.install-v1+json' },
78
+ signal: ctrl.signal,
79
+ });
80
+ if (!res.ok) {
81
+ throw new Error(`npm registry ตอบ ${res.status}${res.statusText ? ` ${res.statusText}` : ''}`);
82
+ }
83
+ const body = (await res.json());
84
+ const latest = body['dist-tags']?.latest;
85
+ if (!latest)
86
+ throw new Error('npm registry ไม่มี dist-tag "latest"');
87
+ return latest;
88
+ }
89
+ finally {
90
+ clearTimeout(timer);
91
+ }
92
+ }
93
+ export async function checkForUpdate(meta, opts = {}) {
94
+ const latestVersion = await fetchLatestVersion(meta, opts);
95
+ return {
96
+ packageName: meta.name,
97
+ currentVersion: meta.version,
98
+ latestVersion,
99
+ isOutdated: isNewerVersion(latestVersion, meta.version),
100
+ installCommand: installCommand(meta.name),
101
+ };
102
+ }
103
+ export function installLatest(meta, opts = {}) {
104
+ const spawnImpl = opts.spawnImpl ?? spawn;
105
+ return new Promise((resolve, reject) => {
106
+ const child = spawnImpl('npm', ['install', '-g', `${meta.name}@latest`], {
107
+ stdio: 'inherit',
108
+ env: process.env,
109
+ shell: process.platform === 'win32', // Windows: npm = npm.cmd → spawn ตรงๆ ENOENT
110
+ });
111
+ child.once('error', reject);
112
+ child.once('close', (code) => resolve(code ?? 1));
113
+ });
114
+ }
@@ -0,0 +1,173 @@
1
+ // ============================================================================
2
+ // src/worktree.ts — throwaway git worktrees for ISOLATED parallel write agents.
3
+ //
4
+ // When several sub-agents edit files at once, they would clobber each other in
5
+ // one working tree. This gives each one its own `git worktree` (detached at the
6
+ // current HEAD), so their writes are physically isolated; afterwards each
7
+ // worktree's diff is captured and applied back to the main tree sequentially.
8
+ //
9
+ // Reuses runGit()/isGitRepo() from src/git.ts (execFile, no shell). Everything is
10
+ // best-effort + defensive: not a git repo → returns null (caller falls back to a
11
+ // shared tree); a failed apply is reported, never thrown past the orchestrator.
12
+ // ============================================================================
13
+ import { mkdtemp, rm, writeFile, readFile, realpath } from 'node:fs/promises';
14
+ import { tmpdir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ import { randomUUID } from 'node:crypto';
17
+ import { runGit, isGitRepo } from './git.js';
18
+ /** repo root of `cwd` (the top-level working dir), or null if not a git repo. */
19
+ export async function getRepoRoot(cwd = process.cwd()) {
20
+ if (!(await isGitRepo(cwd)))
21
+ return null;
22
+ try {
23
+ return (await runGit(['rev-parse', '--show-toplevel'], cwd)).trim();
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ /**
30
+ * Create a detached worktree at the current HEAD of the repo containing `cwd`.
31
+ * Returns null if `cwd` is not in a git repo (caller should then run un-isolated).
32
+ */
33
+ export async function createWorktree(cwd = process.cwd()) {
34
+ const repoRoot = await getRepoRoot(cwd);
35
+ if (!repoRoot)
36
+ return null;
37
+ try {
38
+ const baseRef = (await runGit(['rev-parse', 'HEAD'], repoRoot)).trim();
39
+ const tmpParent = await mkdtemp(join(tmpdir(), 'sanook-wt-'));
40
+ const path = join(tmpParent, `t-${randomUUID().slice(0, 8)}`); // must not pre-exist; git creates it
41
+ await runGit(['worktree', 'add', '--detach', path, baseRef], repoRoot);
42
+ const real = await realpath(path).catch(() => path);
43
+ return { path: real, baseRef, repoRoot, tmpParent };
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ /**
50
+ * Capture everything the sub-agent changed in its worktree as a unified diff
51
+ * (vs the base HEAD), including new/untracked files. Empty string = no changes.
52
+ */
53
+ export async function captureDiff(wt) {
54
+ try {
55
+ await runGit(['add', '-A'], wt.path); // stage incl. untracked so they appear in the diff
56
+ return await runGit(['diff', '--cached', '--binary', wt.baseRef], wt.path);
57
+ }
58
+ catch {
59
+ return '';
60
+ }
61
+ }
62
+ /**
63
+ * Apply a captured diff back into the main repo (at its root). Uses --3way so a
64
+ * clean patch lands and a conflicting one is reported rather than silently lost.
65
+ * Empty diff is a no-op success.
66
+ */
67
+ export async function applyDiff(diff, repoRoot) {
68
+ if (!diff.trim())
69
+ return { ok: true };
70
+ const files = diffFiles(diff);
71
+ if (files.length) {
72
+ try {
73
+ await runGit(['diff', '--cached', '--quiet', '--', ...files], repoRoot);
74
+ }
75
+ catch {
76
+ return { ok: false, reason: 'touched files have staged changes; refusing to disturb the index' };
77
+ }
78
+ }
79
+ // Snapshot every touched file's exact pre-apply content (or absence). `git apply --3way`
80
+ // can leave conflict markers + unmerged index entries on failure, and across git versions
81
+ // `--check` doesn't always foresee a 3-way conflict — so on ANY failure we roll the working
82
+ // tree back to precisely this snapshot, preserving uncommitted changes that were already there.
83
+ const before = new Map();
84
+ await Promise.all(files.map(async (f) => {
85
+ before.set(f, await readFile(join(repoRoot, f)).catch(() => null));
86
+ }));
87
+ const patchFile = join(tmpdir(), `sanook-patch-${randomUUID().slice(0, 8)}.diff`);
88
+ try {
89
+ await writeFile(patchFile, diff, 'utf8');
90
+ await runGit(['apply', '--check', '--3way', '--whitespace=nowarn', patchFile], repoRoot); // fast reject
91
+ await runGit(['apply', '--3way', '--whitespace=nowarn', patchFile], repoRoot);
92
+ return { ok: true };
93
+ }
94
+ catch (e) {
95
+ // restore exact pre-apply content + clear any index/unmerged entries --3way may have created
96
+ await Promise.all([...before].map(async ([f, content]) => {
97
+ const abs = join(repoRoot, f);
98
+ if (content == null)
99
+ await rm(abs, { force: true }).catch(() => { });
100
+ else
101
+ await writeFile(abs, content).catch(() => { });
102
+ }));
103
+ if (files.length)
104
+ await runGit(['reset', '-q', '--', ...files], repoRoot).catch(() => { });
105
+ return { ok: false, reason: e.message.split('\n')[0] };
106
+ }
107
+ finally {
108
+ await rm(patchFile, { force: true }).catch(() => { });
109
+ }
110
+ }
111
+ /** Remove the worktree and its temp parent (best-effort; prunes git's bookkeeping). */
112
+ export async function removeWorktree(wt) {
113
+ await runGit(['worktree', 'remove', '--force', wt.path], wt.repoRoot).catch(() => { });
114
+ await rm(wt.tmpParent, { recursive: true, force: true }).catch(() => { });
115
+ await runGit(['worktree', 'prune'], wt.repoRoot).catch(() => { });
116
+ }
117
+ /** changed file paths in a captured diff (for a human-readable summary). */
118
+ export function diffFiles(diff) {
119
+ const files = new Set();
120
+ for (const m of diff.matchAll(/^diff --git a\/(.+?) b\/(.+)$/gm))
121
+ files.add(m[2]);
122
+ return [...files];
123
+ }
124
+ /**
125
+ * Run `work(task, cwd, i)` for each task in ITS OWN throwaway worktree (concurrently,
126
+ * via the injected `runConcurrently`), then capture+apply each worktree's diff back
127
+ * into the main tree sequentially. The work callback is injected so this whole
128
+ * lifecycle (create → isolate → merge → cleanup) unit-tests with no agent/network.
129
+ * Returns null if `root` is not a git repo or worktrees can't be created.
130
+ */
131
+ export async function runInWorktrees(tasks, root, work, runConcurrently) {
132
+ if (!(await getRepoRoot(root)))
133
+ return null;
134
+ const wts = [];
135
+ for (let i = 0; i < tasks.length; i++) {
136
+ const wt = await createWorktree(root);
137
+ if (!wt) {
138
+ for (const w of wts)
139
+ await removeWorktree(w);
140
+ return null;
141
+ }
142
+ wts.push(wt);
143
+ }
144
+ let results;
145
+ try {
146
+ results = await runConcurrently(tasks.map((t, i) => () => work(t, wts[i].path, i)));
147
+ }
148
+ catch (e) {
149
+ for (const w of wts)
150
+ await removeWorktree(w);
151
+ throw e;
152
+ }
153
+ const out = [];
154
+ for (let i = 0; i < wts.length; i++) {
155
+ let merge;
156
+ try {
157
+ const diff = await captureDiff(wts[i]);
158
+ if (!diff.trim()) {
159
+ merge = { description: tasks[i].description, changed: [], applied: true };
160
+ }
161
+ else {
162
+ const changed = diffFiles(diff);
163
+ const res = await applyDiff(diff, root); // sequential → deterministic conflict handling
164
+ merge = { description: tasks[i].description, changed, applied: res.ok, reason: res.reason };
165
+ }
166
+ }
167
+ finally {
168
+ await removeWorktree(wts[i]);
169
+ }
170
+ out.push({ result: results[i], merge });
171
+ }
172
+ return out;
173
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanook-cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "A terminal AI coding agent — BYOK, 12 providers, MCP, cron gateway, skills, and git awareness. Built from scratch in TypeScript.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "dist",
11
11
  "skills",
12
12
  "second-brain",
13
+ "scripts/postinstall.mjs",
13
14
  "README.md",
14
15
  "CHANGELOG.md",
15
16
  "LICENSE",
@@ -21,6 +22,7 @@
21
22
  "typecheck": "tsc --noEmit",
22
23
  "test": "vitest run",
23
24
  "eval": "tsx src/eval/run.ts",
25
+ "postinstall": "node scripts/postinstall.mjs",
24
26
  "prepublishOnly": "npm run build"
25
27
  },
26
28
  "engines": {
@@ -28,15 +30,19 @@
28
30
  },
29
31
  "keywords": [
30
32
  "ai",
31
- "cli",
32
33
  "coding-agent",
33
- "agent",
34
+ "ai-agent",
35
+ "cli",
34
36
  "llm",
35
37
  "terminal",
36
38
  "byok",
37
39
  "mcp",
38
- "gateway",
39
- "cron",
40
+ "second-brain",
41
+ "obsidian",
42
+ "cross-session-memory",
43
+ "claude-code-alternative",
44
+ "ai-coding",
45
+ "agent",
40
46
  "claude",
41
47
  "gemini"
42
48
  ],
@@ -0,0 +1,33 @@
1
+ // postinstall — ชี้ทางให้ผู้ใช้พิมพ์คำสั่งที่ "ใช้ได้จริง" ทันทีหลัง `npm i`
2
+ // ปัญหาที่แก้: `npm i sanook-cli` (ไม่มี -g) = ลง local ไม่เข้า PATH → พิมพ์ `sanook` แล้วไม่เจอ
3
+ // กฎเหล็ก: ห้าม postinstall ทำให้การติดตั้งล้มเหลว (ครอบ try/catch, ออก 0 เสมอ)
4
+ import { resolve, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ try {
8
+ const isGlobal = process.env.npm_config_global === 'true';
9
+ const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
10
+ const initCwd = process.env.INIT_CWD ? resolve(process.env.INIT_CWD) : '';
11
+
12
+ // เงียบเมื่อ: dev ในรีโปตัวเอง (INIT_CWD === pkgRoot) หรือ CI (กัน log รก)
13
+ const selfInstall = initCwd && initCwd === pkgRoot;
14
+ if (selfInstall || (process.env.CI && !isGlobal)) process.exit(0);
15
+
16
+ const tty = process.stdout.isTTY;
17
+ const paint = (code, s) => (tty ? `\x1b[${code}m${s}\x1b[0m` : s);
18
+ const cyan = (s) => paint('36', s);
19
+ const dim = (s) => paint('2', s);
20
+ const bold = (s) => paint('1', s);
21
+
22
+ if (isGlobal) {
23
+ console.log(`\n${bold('✅ sanook-cli พร้อมใช้')} — พิมพ์ ${cyan('sanook')} เพื่อเริ่ม`);
24
+ console.log(dim(' ยังพิมพ์ "sanook" ไม่เจอ? ปิด-เปิด terminal ใหม่ · ตรวจ: ') + cyan('npx sanook doctor') + '\n');
25
+ } else {
26
+ console.log(`\n${bold('sanook-cli ลงแบบ local แล้ว')} — คำสั่ง ${cyan('sanook')} ยัง${bold('ไม่')}อยู่ใน PATH`);
27
+ console.log(` ${dim('• รันเลยตอนนี้:')} ${cyan('npx sanook')}`);
28
+ console.log(` ${dim('• ลงให้พิมพ์ sanook ตรงๆ:')} ${cyan('npm install -g sanook-cli')}`);
29
+ console.log(` ${dim('• ตรวจ/แก้ PATH:')} ${cyan('npx sanook doctor')}\n`);
30
+ }
31
+ } catch {
32
+ // ห้ามทำให้ install ล้ม — เงียบไว้
33
+ }
@@ -0,0 +1,30 @@
1
+ ---
2
+ tags: [index, moc, -agents]
3
+ note_type: moc
4
+ created: {{DATE}}
5
+ updated: {{DATE}}
6
+ parent: "[[Home]]"
7
+ ---
8
+
9
+ # .agents
10
+
11
+ > agent-specific assets (skills/workflows) ของ vault นี้
12
+
13
+ ## ใส่ที่นี่
14
+ skill + workflow guide ที่ agent ใช้
15
+
16
+ ## ไม่ใส่ที่นี่
17
+ โน้ตงาน (→ปลายทางปกติ)
18
+
19
+ ## AI Routing Contract
20
+
21
+ - ก่อนเขียน: เช็กว่าเนื้อหาตรง "ใส่ที่นี่" และไม่เข้า "ไม่ใส่ที่นี่"; ถ้าก้ำกึ่งอ่าน [[Vault Structure Map]] ก่อน
22
+ - ก่อนสร้างไฟล์ใหม่: ค้นหาโน้ตเดิมในโฟลเดอร์นี้และโฟลเดอร์ใกล้เคียงก่อน เพื่อ merge/update แทน append ซ้ำ
23
+ - เมื่อสร้างโน้ตในโฟลเดอร์นี้: ตั้ง `parent: "[[.agents/_Index]]"` และท้ายไฟล์ `up:: [[.agents/_Index]]`
24
+ - หลังเขียน: เชื่อม link ไป source/project/session/decision ที่เกี่ยวข้อง และอัปเดต hub/index ถ้าโน้ตนี้ควรถูกค้นเจอในอนาคต
25
+
26
+ > รายละเอียดทุกโฟลเดอร์ + decision rules → [[Vault Structure Map]]
27
+
28
+ _(ยังว่าง — โน้ตในโฟลเดอร์นี้จะถูกลิงก์ที่นี่)_
29
+
30
+ up:: [[Home]]
@@ -0,0 +1,30 @@
1
+ ---
2
+ tags: [index, moc, skills]
3
+ note_type: moc
4
+ created: {{DATE}}
5
+ updated: {{DATE}}
6
+ parent: "[[.agents/_Index]]"
7
+ ---
8
+
9
+ # skills
10
+
11
+ > skill folders (SKILL.md) ที่ agent โหลด on-demand
12
+
13
+ ## ใส่ที่นี่
14
+ SKILL.md ต่อ skill
15
+
16
+ ## ไม่ใส่ที่นี่
17
+ prose how-to (→Runbooks)
18
+
19
+ ## AI Routing Contract
20
+
21
+ - ก่อนเขียน: เช็กว่าเนื้อหาตรง "ใส่ที่นี่" และไม่เข้า "ไม่ใส่ที่นี่"; ถ้าก้ำกึ่งอ่าน [[Vault Structure Map]] ก่อน
22
+ - ก่อนสร้างไฟล์ใหม่: ค้นหาโน้ตเดิมในโฟลเดอร์นี้และโฟลเดอร์ใกล้เคียงก่อน เพื่อ merge/update แทน append ซ้ำ
23
+ - เมื่อสร้างโน้ตในโฟลเดอร์นี้: ตั้ง `parent: "[[.agents/skills/_Index]]"` และท้ายไฟล์ `up:: [[.agents/skills/_Index]]`
24
+ - หลังเขียน: เชื่อม link ไป source/project/session/decision ที่เกี่ยวข้อง และอัปเดต hub/index ถ้าโน้ตนี้ควรถูกค้นเจอในอนาคต
25
+
26
+ > รายละเอียดทุกโฟลเดอร์ + decision rules → [[Vault Structure Map]]
27
+
28
+ _(ยังว่าง — โน้ตในโฟลเดอร์นี้จะถูกลิงก์ที่นี่)_
29
+
30
+ up:: [[.agents/_Index]]
@@ -0,0 +1,30 @@
1
+ ---
2
+ tags: [index, moc, workflows]
3
+ note_type: moc
4
+ created: {{DATE}}
5
+ updated: {{DATE}}
6
+ parent: "[[.agents/_Index]]"
7
+ ---
8
+
9
+ # workflows
10
+
11
+ > workflow guide (multi-step orchestration)
12
+
13
+ ## ใส่ที่นี่
14
+ workflow ที่ทำซ้ำได้
15
+
16
+ ## ไม่ใส่ที่นี่
17
+ one-off task
18
+
19
+ ## AI Routing Contract
20
+
21
+ - ก่อนเขียน: เช็กว่าเนื้อหาตรง "ใส่ที่นี่" และไม่เข้า "ไม่ใส่ที่นี่"; ถ้าก้ำกึ่งอ่าน [[Vault Structure Map]] ก่อน
22
+ - ก่อนสร้างไฟล์ใหม่: ค้นหาโน้ตเดิมในโฟลเดอร์นี้และโฟลเดอร์ใกล้เคียงก่อน เพื่อ merge/update แทน append ซ้ำ
23
+ - เมื่อสร้างโน้ตในโฟลเดอร์นี้: ตั้ง `parent: "[[.agents/workflows/_Index]]"` และท้ายไฟล์ `up:: [[.agents/workflows/_Index]]`
24
+ - หลังเขียน: เชื่อม link ไป source/project/session/decision ที่เกี่ยวข้อง และอัปเดต hub/index ถ้าโน้ตนี้ควรถูกค้นเจอในอนาคต
25
+
26
+ > รายละเอียดทุกโฟลเดอร์ + decision rules → [[Vault Structure Map]]
27
+
28
+ _(ยังว่าง — โน้ตในโฟลเดอร์นี้จะถูกลิงก์ที่นี่)_
29
+
30
+ up:: [[.agents/_Index]]
@@ -7,10 +7,10 @@
7
7
 
8
8
  ## 🔴 Red Lines
9
9
  1. อ่าน `Shared/AI-Context-Index.md` ก่อนตอบ (vault = source of truth)
10
- 2. verify ก่อนอ้าง ไม่แน่ใจบอกตรงๆ ห้ามแต่ง
11
- 3. ถามก่อนรัน destructive (`rm -rf` / `reset --hard` / `push --force` / drop data)
12
- 4. ห้ามเขียน secret ลงไฟล์ `<secret:VAR>`
13
- 5. ห้ามลบ durable note โดยไม่ถาม
10
+ 2. ก่อนสร้าง/ย้ายโน้ต อ่าน `Vault Structure Map.md` + `_Index.md` ของโฟลเดอร์ปลายทาง แล้วทำตาม AI Routing Contract
11
+ 3. verify ก่อนอ้าง ไม่แน่ใจบอกตรงๆ ห้ามแต่ง
12
+ 4. ถามก่อนรัน destructive (`rm -rf` / `reset --hard` / `push --force` / drop data)
13
+ 5. ห้ามเขียน secret ลงไฟล์ → `<secret:VAR>` · ห้ามลบ durable note โดยไม่ถาม
14
14
 
15
15
  ## Multi-agent
16
16
  หลาย agent ทำงาน vault เดียว → อ่าน `Shared/Coordination/` ก่อนแตะ · เขียน session log หลังทำ (§2 ใน `CLAUDE.md`)
@@ -0,0 +1,30 @@
1
+ ---
2
+ tags: [index, moc, acceptance]
3
+ note_type: moc
4
+ created: {{DATE}}
5
+ updated: {{DATE}}
6
+ parent: "[[Home]]"
7
+ ---
8
+
9
+ # Acceptance
10
+
11
+ > golden input→expected-output fixtures
12
+
13
+ ## ใส่ที่นี่
14
+ case ที่ใช้ตัดสิน done/not-done
15
+
16
+ ## ไม่ใส่ที่นี่
17
+ gate ticklist (→Checklists) · runner (→Evals)
18
+
19
+ ## AI Routing Contract
20
+
21
+ - ก่อนเขียน: เช็กว่าเนื้อหาตรง "ใส่ที่นี่" และไม่เข้า "ไม่ใส่ที่นี่"; ถ้าก้ำกึ่งอ่าน [[Vault Structure Map]] ก่อน
22
+ - ก่อนสร้างไฟล์ใหม่: ค้นหาโน้ตเดิมในโฟลเดอร์นี้และโฟลเดอร์ใกล้เคียงก่อน เพื่อ merge/update แทน append ซ้ำ
23
+ - เมื่อสร้างโน้ตในโฟลเดอร์นี้: ตั้ง `parent: "[[Acceptance/_Index]]"` และท้ายไฟล์ `up:: [[Acceptance/_Index]]`
24
+ - หลังเขียน: เชื่อม link ไป source/project/session/decision ที่เกี่ยวข้อง และอัปเดต hub/index ถ้าโน้ตนี้ควรถูกค้นเจอในอนาคต
25
+
26
+ > รายละเอียดทุกโฟลเดอร์ + decision rules → [[Vault Structure Map]]
27
+
28
+ _(ยังว่าง — โน้ตในโฟลเดอร์นี้จะถูกลิงก์ที่นี่)_
29
+
30
+ up:: [[Home]]
@@ -0,0 +1,39 @@
1
+ ---
2
+ tags: [template, acceptance, golden-case]
3
+ note_type: template
4
+ created: {{DATE}}
5
+ updated: {{DATE}}
6
+ parent: "[[Acceptance/_Index]]"
7
+ ---
8
+
9
+ # Golden Case Template
10
+
11
+ > ใช้สร้าง fixture ที่บอก input→expected output เพื่อพิสูจน์ว่า behavior ใดถือว่า done; ไม่ใช้เป็น checklist หรือ eval report
12
+
13
+ ## Task Family
14
+
15
+ _(What behavior this case evaluates.)_
16
+
17
+ ## Input
18
+
19
+ ```text
20
+ <input>
21
+ ```
22
+
23
+ ## Expected Output
24
+
25
+ ```text
26
+ <expected>
27
+ ```
28
+
29
+ ## Pass Criteria
30
+
31
+ - [ ] Correct result
32
+ - [ ] No forbidden side effects
33
+ - [ ] Matches owner-facing tone/format
34
+
35
+ ## Notes
36
+
37
+ _(Known edge cases, source links, or fixture paths.)_
38
+
39
+ up:: [[Acceptance/_Index]]
@@ -0,0 +1,30 @@
1
+ ---
2
+ tags: [index, moc, areas]
3
+ note_type: moc
4
+ created: {{DATE}}
5
+ updated: {{DATE}}
6
+ parent: "[[Home]]"
7
+ ---
8
+
9
+ # Areas
10
+
11
+ > PARA — โดเมนงานต่อเนื่องที่ไม่มีวันจบ
12
+
13
+ ## ใส่ที่นี่
14
+ brand/trading/content/products ฯลฯ
15
+
16
+ ## ไม่ใส่ที่นี่
17
+ งานที่มีวันจบ (→Projects/Goals)
18
+
19
+ ## AI Routing Contract
20
+
21
+ - ก่อนเขียน: เช็กว่าเนื้อหาตรง "ใส่ที่นี่" และไม่เข้า "ไม่ใส่ที่นี่"; ถ้าก้ำกึ่งอ่าน [[Vault Structure Map]] ก่อน
22
+ - ก่อนสร้างไฟล์ใหม่: ค้นหาโน้ตเดิมในโฟลเดอร์นี้และโฟลเดอร์ใกล้เคียงก่อน เพื่อ merge/update แทน append ซ้ำ
23
+ - เมื่อสร้างโน้ตในโฟลเดอร์นี้: ตั้ง `parent: "[[Areas/_Index]]"` และท้ายไฟล์ `up:: [[Areas/_Index]]`
24
+ - หลังเขียน: เชื่อม link ไป source/project/session/decision ที่เกี่ยวข้อง และอัปเดต hub/index ถ้าโน้ตนี้ควรถูกค้นเจอในอนาคต
25
+
26
+ > รายละเอียดทุกโฟลเดอร์ + decision rules → [[Vault Structure Map]]
27
+
28
+ _(ยังว่าง — โน้ตในโฟลเดอร์นี้จะถูกลิงก์ที่นี่)_
29
+
30
+ up:: [[Home]]
@@ -0,0 +1,30 @@
1
+ ---
2
+ tags: [index, moc, system-os]
3
+ note_type: moc
4
+ created: {{DATE}}
5
+ updated: {{DATE}}
6
+ parent: "[[Bugs/_Index]]"
7
+ ---
8
+
9
+ # System-OS
10
+
11
+ > bug report ระดับระบบ/OS/toolchain ที่ไม่ผูกกับ project เดียว
12
+
13
+ ## ใส่ที่นี่
14
+ OS, shell, package manager, permission, filesystem, or app-runtime bugs
15
+
16
+ ## ไม่ใส่ที่นี่
17
+ bug ของ project เฉพาะ (→Bugs หรือ Projects/<proj>/Bugs)
18
+
19
+ ## AI Routing Contract
20
+
21
+ - ก่อนเขียน: เช็กว่าเนื้อหาตรง "ใส่ที่นี่" และไม่เข้า "ไม่ใส่ที่นี่"; ถ้าก้ำกึ่งอ่าน [[Vault Structure Map]] ก่อน
22
+ - ก่อนสร้างไฟล์ใหม่: ค้นหาโน้ตเดิมในโฟลเดอร์นี้และโฟลเดอร์ใกล้เคียงก่อน เพื่อ merge/update แทน append ซ้ำ
23
+ - เมื่อสร้างโน้ตในโฟลเดอร์นี้: ตั้ง `parent: "[[Bugs/System-OS/_Index]]"` และท้ายไฟล์ `up:: [[Bugs/System-OS/_Index]]`
24
+ - หลังเขียน: เชื่อม link ไป source/project/session/decision ที่เกี่ยวข้อง และอัปเดต hub/index ถ้าโน้ตนี้ควรถูกค้นเจอในอนาคต
25
+
26
+ > รายละเอียดทุกโฟลเดอร์ + decision rules → [[Vault Structure Map]]
27
+
28
+ _(ยังว่าง — โน้ตในโฟลเดอร์นี้จะถูกลิงก์ที่นี่)_
29
+
30
+ up:: [[Bugs/_Index]]
@@ -0,0 +1,30 @@
1
+ ---
2
+ tags: [index, moc, bugs]
3
+ note_type: moc
4
+ created: {{DATE}}
5
+ updated: {{DATE}}
6
+ parent: "[[Home]]"
7
+ ---
8
+
9
+ # Bugs
10
+
11
+ > bug report reproducible ลงวันที่ ไม่ลบ
12
+
13
+ ## ใส่ที่นี่
14
+ bug report (global flat) + link กลับ project · system/OS → Bugs/System-OS/
15
+
16
+ ## ไม่ใส่ที่นี่
17
+ bug ที่ reproduce ไม่ได้
18
+
19
+ ## AI Routing Contract
20
+
21
+ - ก่อนเขียน: เช็กว่าเนื้อหาตรง "ใส่ที่นี่" และไม่เข้า "ไม่ใส่ที่นี่"; ถ้าก้ำกึ่งอ่าน [[Vault Structure Map]] ก่อน
22
+ - ก่อนสร้างไฟล์ใหม่: ค้นหาโน้ตเดิมในโฟลเดอร์นี้และโฟลเดอร์ใกล้เคียงก่อน เพื่อ merge/update แทน append ซ้ำ
23
+ - เมื่อสร้างโน้ตในโฟลเดอร์นี้: ตั้ง `parent: "[[Bugs/_Index]]"` และท้ายไฟล์ `up:: [[Bugs/_Index]]`
24
+ - หลังเขียน: เชื่อม link ไป source/project/session/decision ที่เกี่ยวข้อง และอัปเดต hub/index ถ้าโน้ตนี้ควรถูกค้นเจอในอนาคต
25
+
26
+ > รายละเอียดทุกโฟลเดอร์ + decision rules → [[Vault Structure Map]]
27
+
28
+ _(ยังว่าง — โน้ตในโฟลเดอร์นี้จะถูกลิงก์ที่นี่)_
29
+
30
+ up:: [[Home]]