sanook-cli 0.5.0 → 0.5.2

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 (146) hide show
  1. package/.env.example +161 -3
  2. package/CHANGELOG.md +83 -5
  3. package/README.md +240 -23
  4. package/README.th.md +87 -6
  5. package/dist/approval.js +6 -0
  6. package/dist/bin.js +3045 -210
  7. package/dist/brain-context.js +223 -0
  8. package/dist/brain-doctor.js +318 -0
  9. package/dist/brain-eval.js +186 -0
  10. package/dist/brain-final.js +371 -0
  11. package/dist/brain-review.js +382 -0
  12. package/dist/brain.js +12 -1
  13. package/dist/brand.js +1 -1
  14. package/dist/cli-args.js +152 -0
  15. package/dist/cli-option-values.js +16 -0
  16. package/dist/commands.js +172 -13
  17. package/dist/compaction.js +96 -11
  18. package/dist/config.js +118 -28
  19. package/dist/context-compression.js +191 -0
  20. package/dist/cost.js +49 -15
  21. package/dist/first-run.js +21 -0
  22. package/dist/gateway/auth.js +37 -8
  23. package/dist/gateway/bluebubbles.js +205 -0
  24. package/dist/gateway/config.js +929 -0
  25. package/dist/gateway/deliver.js +357 -0
  26. package/dist/gateway/discord.js +124 -0
  27. package/dist/gateway/email.js +472 -0
  28. package/dist/gateway/googlechat.js +207 -0
  29. package/dist/gateway/homeassistant.js +256 -0
  30. package/dist/gateway/ledger.js +18 -0
  31. package/dist/gateway/line.js +171 -0
  32. package/dist/gateway/lock.js +3 -1
  33. package/dist/gateway/matrix.js +366 -0
  34. package/dist/gateway/mattermost.js +322 -0
  35. package/dist/gateway/ntfy.js +218 -0
  36. package/dist/gateway/schedule.js +31 -4
  37. package/dist/gateway/serve.js +267 -7
  38. package/dist/gateway/server.js +253 -19
  39. package/dist/gateway/service.js +224 -0
  40. package/dist/gateway/session.js +343 -0
  41. package/dist/gateway/signal.js +351 -0
  42. package/dist/gateway/slack.js +124 -0
  43. package/dist/gateway/sms.js +169 -0
  44. package/dist/gateway/targets.js +576 -0
  45. package/dist/gateway/teams.js +106 -0
  46. package/dist/gateway/telegram.js +38 -15
  47. package/dist/gateway/webhooks.js +220 -0
  48. package/dist/gateway/whatsapp.js +230 -0
  49. package/dist/hooks.js +13 -2
  50. package/dist/insights-args.js +35 -0
  51. package/dist/insights.js +86 -0
  52. package/dist/loop.js +123 -24
  53. package/dist/lsp/index.js +23 -5
  54. package/dist/mcp-registry.js +350 -0
  55. package/dist/mcp-server.js +1 -1
  56. package/dist/mcp.js +44 -6
  57. package/dist/memory.js +100 -33
  58. package/dist/orchestrate.js +49 -19
  59. package/dist/personality.js +58 -0
  60. package/dist/providers/codex.js +86 -38
  61. package/dist/providers/keys.js +1 -1
  62. package/dist/providers/models.js +22 -6
  63. package/dist/providers/registry.js +38 -49
  64. package/dist/search/chunk.js +7 -8
  65. package/dist/search/cli.js +75 -0
  66. package/dist/search/embed-store.js +3 -0
  67. package/dist/search/indexer.js +44 -1
  68. package/dist/search/store.js +23 -1
  69. package/dist/session.js +93 -7
  70. package/dist/skill-install.js +29 -12
  71. package/dist/support-dump.js +175 -0
  72. package/dist/tools/edit.js +45 -15
  73. package/dist/tools/git.js +10 -5
  74. package/dist/tools/homeassistant.js +106 -0
  75. package/dist/tools/index.js +5 -0
  76. package/dist/tools/list.js +19 -6
  77. package/dist/tools/permission.js +923 -9
  78. package/dist/tools/read.js +16 -4
  79. package/dist/tools/schedule.js +19 -3
  80. package/dist/tools/search.js +217 -13
  81. package/dist/tools/task.js +18 -7
  82. package/dist/tools/timeout.js +21 -3
  83. package/dist/trust.js +11 -1
  84. package/dist/ui/app.js +57 -11
  85. package/dist/ui/brain-wizard.js +2 -2
  86. package/dist/ui/history.js +37 -5
  87. package/dist/ui/mentions.js +3 -2
  88. package/dist/ui/render.js +55 -15
  89. package/dist/ui/setup.js +107 -10
  90. package/dist/update.js +24 -11
  91. package/dist/worktree.js +175 -4
  92. package/package.json +4 -4
  93. package/second-brain/AGENTS.md +6 -4
  94. package/second-brain/CLAUDE.md +7 -1
  95. package/second-brain/Evals/_Index.md +10 -2
  96. package/second-brain/Evals/quality-ledger.md +9 -1
  97. package/second-brain/Evals/second-brain-benchmarks.md +62 -0
  98. package/second-brain/GEMINI.md +5 -4
  99. package/second-brain/Home.md +1 -1
  100. package/second-brain/Projects/_Index.md +3 -1
  101. package/second-brain/Projects/sanook-cli/_Index.md +26 -0
  102. package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +156 -0
  103. package/second-brain/README.md +1 -1
  104. package/second-brain/Research/2026-06-17-ai-second-brain-method-experiment.md +108 -0
  105. package/second-brain/Research/2026-06-18-ai-token-reduction-frameworks.md +55 -0
  106. package/second-brain/Research/2026-06-18-hermes-cli-second-brain-expansion-research.md +160 -0
  107. package/second-brain/Research/2026-06-18-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
  108. package/second-brain/Research/_Index.md +6 -1
  109. package/second-brain/Reviews/2026-06-18-auto-improve-maintenance.md +54 -0
  110. package/second-brain/Reviews/_Index.md +1 -1
  111. package/second-brain/Runbooks/_Index.md +6 -1
  112. package/second-brain/Runbooks/ai-second-brain-operating-sequence.md +108 -0
  113. package/second-brain/SANOOK.md +45 -0
  114. package/second-brain/Sessions/2026-06-17-ai-framework-additional-zones.md +68 -0
  115. package/second-brain/Sessions/2026-06-17-ai-second-brain-sequence-experiment.md +63 -0
  116. package/second-brain/Sessions/2026-06-18-cli-args-release-readiness.md +59 -0
  117. package/second-brain/Sessions/2026-06-18-final-gate-template-final.md +192 -0
  118. package/second-brain/Sessions/2026-06-18-final-gate-template.md +71 -0
  119. package/second-brain/Sessions/2026-06-18-framework-dogfood-permission-and-memory.md +58 -0
  120. package/second-brain/Sessions/2026-06-18-hermes-second-brain-expansion-research.md +52 -0
  121. package/second-brain/Sessions/2026-06-18-mcp-ecosystem-and-sanook-ux-scan.md +81 -0
  122. package/second-brain/Sessions/2026-06-18-sanook-brain-cli-p0-implementation.md +86 -0
  123. package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli-final.md +246 -0
  124. package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli.md +78 -0
  125. package/second-brain/Sessions/2026-06-18-sanook-cli-second-brain-roadmap-correction.md +54 -0
  126. package/second-brain/Sessions/2026-06-18-token-reduction-framework-integration.md +69 -0
  127. package/second-brain/Sessions/_Index.md +15 -1
  128. package/second-brain/Shared/AI-Context-Index.md +22 -0
  129. package/second-brain/Shared/Context-Packs/_Index.md +9 -1
  130. package/second-brain/Shared/Context-Packs/coding-release.md +51 -0
  131. package/second-brain/Shared/Context-Packs/research-to-framework.md +51 -0
  132. package/second-brain/Shared/Context-Packs/second-brain-maintenance.md +41 -0
  133. package/second-brain/Shared/Operating-State/current-state.md +22 -3
  134. package/second-brain/Shared/Scripts/_Index.md +3 -1
  135. package/second-brain/Shared/Scripts/ai-second-brain-method-eval.mjs +198 -0
  136. package/second-brain/Shared/Tech-Standards/_Index.md +4 -1
  137. package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
  138. package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -0
  139. package/second-brain/Shared/User-Memory/_Index.md +4 -1
  140. package/second-brain/Shared/User-Memory/response-examples.md +98 -0
  141. package/second-brain/Shared/User-Memory/user-preferences.md +1 -0
  142. package/second-brain/Templates/_Index.md +9 -0
  143. package/second-brain/Templates/final-lite.md +111 -0
  144. package/second-brain/Templates/final.md +231 -0
  145. package/second-brain/Vault Structure Map.md +2 -1
  146. package/skills/structured-output-llm/SKILL.md +1 -1
package/dist/tools/git.js CHANGED
@@ -1,7 +1,11 @@
1
1
  import { tool } from 'ai';
2
2
  import { z } from 'zod';
3
3
  import { runGit } from '../git.js';
4
+ import { agentCwd } from '../agentContext.js';
4
5
  const gitErr = (e) => `git error: ${e.message}`;
6
+ // รัน git ใน cwd ของ agent (worktree ของ sub-agent ถ้ามี) — ไม่งั้น git_commit/status/diff ไปโดน MAIN repo
7
+ // แทนที่ของ sub-agent ที่ isolate ไว้ (worktree isolation พัง / commit ผิด tree)
8
+ const gitCwd = () => agentCwd();
5
9
  export const gitStatusTool = tool({
6
10
  description: 'ดู git status — ไฟล์ที่เปลี่ยน/staged/untracked + branch',
7
11
  inputSchema: z.object({
@@ -10,7 +14,7 @@ export const gitStatusTool = tool({
10
14
  execute: async ({ path }) => {
11
15
  try {
12
16
  const args = ['status', '--short', '--branch', ...(path ? ['--', path] : [])];
13
- return (await runGit(args)).trim() || '(clean)';
17
+ return (await runGit(args, gitCwd())).trim() || '(clean)';
14
18
  }
15
19
  catch (e) {
16
20
  return gitErr(e);
@@ -26,7 +30,7 @@ export const gitDiffTool = tool({
26
30
  execute: async ({ staged, path }) => {
27
31
  try {
28
32
  const args = ['diff', ...(staged ? ['--staged'] : []), ...(path ? ['--', path] : [])];
29
- const out = await runGit(args);
33
+ const out = await runGit(args, gitCwd());
30
34
  return out.length > 20000 ? `${out.slice(0, 20000)}\n... [diff ยาว, ตัด]` : out || '(no changes)';
31
35
  }
32
36
  catch (e) {
@@ -41,7 +45,7 @@ export const gitLogTool = tool({
41
45
  }),
42
46
  execute: async ({ count = 10 }) => {
43
47
  try {
44
- return (await runGit(['log', '--oneline', '-n', String(Math.min(Math.max(count, 1), 50))])) || '(no commits)';
48
+ return (await runGit(['log', '--oneline', '-n', String(Math.min(Math.max(count, 1), 50))], gitCwd())) || '(no commits)';
45
49
  }
46
50
  catch (e) {
47
51
  return gitErr(e);
@@ -57,9 +61,10 @@ export const gitCommitTool = tool({
57
61
  }),
58
62
  execute: async ({ message, addAll }) => {
59
63
  try {
64
+ const cwd = gitCwd();
60
65
  if (addAll)
61
- await runGit(['add', '-A']);
62
- return (await runGit(['commit', '-m', message])).trim();
66
+ await runGit(['add', '-A'], cwd);
67
+ return (await runGit(['commit', '-m', message], cwd)).trim();
63
68
  }
64
69
  catch (e) {
65
70
  return gitErr(e);
@@ -0,0 +1,106 @@
1
+ import { tool } from 'ai';
2
+ import { z } from 'zod';
3
+ import { readGatewayConfig, resolveHomeAssistantConfig } from '../gateway/config.js';
4
+ import { homeAssistantApiUrl, homeAssistantAuthHeaders, readHomeAssistantJsonResponse } from '../gateway/homeassistant.js';
5
+ const BLOCKED_DOMAINS = new Set(['shell_command', 'command_line', 'python_script', 'pyscript', 'hassio', 'rest_command']);
6
+ const ENTITY_ID_RE = /^[a-z_][a-z0-9_]*\.[a-z0-9_]+$/;
7
+ async function loadHaConfig() {
8
+ const config = resolveHomeAssistantConfig(await readGatewayConfig());
9
+ if (!config.token)
10
+ throw new Error('ยังไม่ได้ตั้ง Home Assistant — รัน: sanook gateway setup homeassistant หรือ set HASS_TOKEN');
11
+ return config;
12
+ }
13
+ async function haFetch(config, path, init = {}) {
14
+ const r = await fetch(homeAssistantApiUrl(config, path), {
15
+ ...init,
16
+ headers: {
17
+ ...homeAssistantAuthHeaders(config.token, init.method ? { 'content-type': 'application/json' } : {}),
18
+ ...(init.headers ?? {}),
19
+ },
20
+ });
21
+ return readHomeAssistantJsonResponse(r, 'Home Assistant API');
22
+ }
23
+ function validateEntityId(entityId) {
24
+ const clean = entityId.trim();
25
+ if (!ENTITY_ID_RE.test(clean))
26
+ throw new Error(`entity_id ไม่ปลอดภัย/ไม่ถูกต้อง: ${entityId}`);
27
+ return clean;
28
+ }
29
+ function stateSummary(state) {
30
+ const name = typeof state.attributes?.friendly_name === 'string' ? state.attributes.friendly_name : state.entity_id;
31
+ return `${state.entity_id}: ${state.state ?? '(unknown)'}${name && name !== state.entity_id ? ` (${name})` : ''}`;
32
+ }
33
+ export const haListEntitiesTool = tool({
34
+ description: 'Home Assistant: list entities/states. Requires HASS_TOKEN or gateway setup homeassistant.',
35
+ inputSchema: z.object({
36
+ domain: z.string().optional().describe('Filter by entity domain เช่น light, switch, climate, sensor'),
37
+ area: z.string().optional().describe('Simple friendly-name substring filter เช่น living room, kitchen'),
38
+ limit: z.number().int().positive().max(200).optional().describe('Maximum rows to return (default 80)'),
39
+ }),
40
+ execute: async ({ domain, area, limit }) => {
41
+ const config = await loadHaConfig();
42
+ const states = await haFetch(config, '/states');
43
+ const cleanDomain = domain?.trim();
44
+ const areaNeedle = area?.trim().toLowerCase();
45
+ const rows = states
46
+ .filter((s) => !cleanDomain || s.entity_id?.startsWith(`${cleanDomain}.`))
47
+ .filter((s) => !areaNeedle || String(s.attributes?.friendly_name ?? s.entity_id ?? '').toLowerCase().includes(areaNeedle))
48
+ .slice(0, limit ?? 80)
49
+ .map(stateSummary);
50
+ return rows.length ? rows.join('\n') : 'ไม่พบ Home Assistant entity ที่ตรงเงื่อนไข';
51
+ },
52
+ });
53
+ export const haGetStateTool = tool({
54
+ description: 'Home Assistant: get detailed state and attributes for one entity.',
55
+ inputSchema: z.object({
56
+ entity_id: z.string().describe('Entity id เช่น light.living_room'),
57
+ }),
58
+ execute: async ({ entity_id }) => {
59
+ const config = await loadHaConfig();
60
+ const entityId = validateEntityId(entity_id);
61
+ const state = await haFetch(config, `/states/${encodeURIComponent(entityId)}`);
62
+ return JSON.stringify(state, null, 2);
63
+ },
64
+ });
65
+ export const haListServicesTool = tool({
66
+ description: 'Home Assistant: list available service domains/actions, optionally filtered by domain.',
67
+ inputSchema: z.object({
68
+ domain: z.string().optional().describe('Filter by service domain เช่น light, climate, switch'),
69
+ }),
70
+ execute: async ({ domain }) => {
71
+ const config = await loadHaConfig();
72
+ const services = await haFetch(config, '/services');
73
+ const cleanDomain = domain?.trim();
74
+ const rows = services
75
+ .filter((entry) => !cleanDomain || entry.domain === cleanDomain)
76
+ .map((entry) => `${entry.domain}: ${Object.keys(entry.services ?? {}).sort().join(', ') || '(none)'}`);
77
+ return rows.length ? rows.join('\n') : 'ไม่พบ Home Assistant service ที่ตรงเงื่อนไข';
78
+ },
79
+ });
80
+ export const haCallServiceTool = tool({
81
+ description: 'Home Assistant: call a service to control a device. Blocks unsafe domains such as shell_command, command_line, python_script, pyscript, hassio, and rest_command.',
82
+ inputSchema: z.object({
83
+ domain: z.string().describe('Service domain เช่น light, switch, climate, cover, media_player, scene, script'),
84
+ service: z.string().describe('Service name เช่น turn_on, turn_off, toggle, set_temperature'),
85
+ entity_id: z.string().optional().describe('Optional target entity id เช่น light.living_room'),
86
+ data: z.record(z.unknown()).optional().describe('Additional JSON service data'),
87
+ }),
88
+ execute: async ({ domain, service, entity_id, data }) => {
89
+ const config = await loadHaConfig();
90
+ const cleanDomain = domain.trim();
91
+ const cleanService = service.trim();
92
+ if (!/^[a-z_][a-z0-9_]*$/.test(cleanDomain))
93
+ throw new Error(`domain ไม่ถูกต้อง: ${domain}`);
94
+ if (!/^[a-z_][a-z0-9_]*$/.test(cleanService))
95
+ throw new Error(`service ไม่ถูกต้อง: ${service}`);
96
+ if (BLOCKED_DOMAINS.has(cleanDomain))
97
+ return `⛔ blocked Home Assistant domain: ${cleanDomain}`;
98
+ const entityId = entity_id ? validateEntityId(entity_id) : undefined;
99
+ const body = { ...(data ?? {}), ...(entityId ? { entity_id: entityId } : {}) };
100
+ const result = await haFetch(config, `/services/${encodeURIComponent(cleanDomain)}/${encodeURIComponent(cleanService)}`, {
101
+ method: 'POST',
102
+ body: JSON.stringify(body),
103
+ });
104
+ return `OK Home Assistant ${cleanDomain}.${cleanService}${entityId ? ` ${entityId}` : ''} (${Array.isArray(result) ? result.length : 0} state update(s))`;
105
+ },
106
+ });
@@ -11,6 +11,7 @@ import { scheduleTaskTool, listScheduledTool, cancelScheduledTool } from './sche
11
11
  import { taskTool, taskParallelTool, taskSpawnTool, taskCollectTool, taskCancelTool, taskStatusTool } from './task.js';
12
12
  import { diagnosticsTool } from './diagnostics.js';
13
13
  import { gitStatusTool, gitDiffTool, gitLogTool, gitCommitTool } from './git.js';
14
+ import { haCallServiceTool, haGetStateTool, haListEntitiesTool, haListServicesTool } from './homeassistant.js';
14
15
  /** tool registry ที่ส่งให้ agent loop */
15
16
  export const tools = {
16
17
  read_file: readFileTool,
@@ -39,5 +40,9 @@ export const tools = {
39
40
  git_diff: gitDiffTool,
40
41
  git_log: gitLogTool,
41
42
  git_commit: gitCommitTool,
43
+ ha_list_entities: haListEntitiesTool,
44
+ ha_get_state: haGetStateTool,
45
+ ha_list_services: haListServicesTool,
46
+ ha_call_service: haCallServiceTool,
42
47
  };
43
48
  export { readFileTool, writeFileTool, editFileTool, listDirTool, globTool, grepTool, bashTool };
@@ -1,6 +1,7 @@
1
1
  import { tool } from 'ai';
2
2
  import { z } from 'zod';
3
- import { readdir } from 'node:fs/promises';
3
+ import { readdir, stat } from 'node:fs/promises';
4
+ import { join } from 'node:path';
4
5
  import { clamp, resolveAgentPath } from './util.js';
5
6
  import { checkReadPath } from './permission.js';
6
7
  export const listDirTool = tool({
@@ -15,11 +16,23 @@ export const listDirTool = tool({
15
16
  return `BLOCKED: ${guard.reason}`;
16
17
  try {
17
18
  const entries = await readdir(full, { withFileTypes: true });
18
- const out = entries
19
- .filter((e) => !e.name.startsWith('.') || e.name === '.env.example' || e.name === '.gitignore')
20
- .map((e) => (e.isDirectory() ? `${e.name}/` : e.name))
21
- .sort()
22
- .join('\n');
19
+ const visible = [];
20
+ for (const e of entries) {
21
+ if (e.name.startsWith('.') && e.name !== '.env.example' && e.name !== '.gitignore')
22
+ continue;
23
+ const entryPath = join(full, e.name);
24
+ const entryGuard = await checkReadPath(entryPath);
25
+ if (!entryGuard.ok)
26
+ continue;
27
+ let isDirectory = e.isDirectory();
28
+ if (!isDirectory && e.isSymbolicLink()) {
29
+ isDirectory = await stat(entryPath)
30
+ .then((s) => s.isDirectory())
31
+ .catch(() => false);
32
+ }
33
+ visible.push(isDirectory ? `${e.name}/` : e.name);
34
+ }
35
+ const out = visible.sort().join('\n');
23
36
  return clamp(out) || '(empty)';
24
37
  }
25
38
  catch (err) {