agent-sin 0.1.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 (150) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +21 -0
  3. package/README.md +81 -0
  4. package/assets/logo.png +0 -0
  5. package/builtin-skills/_shared/_models_lib.py +227 -0
  6. package/builtin-skills/_shared/_profile_lib.py +98 -0
  7. package/builtin-skills/_shared/_schedules_lib.py +313 -0
  8. package/builtin-skills/_shared/_skill_settings_lib.py +153 -0
  9. package/builtin-skills/_shared/i18n.py +26 -0
  10. package/builtin-skills/memo-delete/main.py +155 -0
  11. package/builtin-skills/memo-delete/skill.yaml +57 -0
  12. package/builtin-skills/memo-index/main.py +178 -0
  13. package/builtin-skills/memo-index/skill.yaml +53 -0
  14. package/builtin-skills/memo-save/README.md +5 -0
  15. package/builtin-skills/memo-save/main.py +74 -0
  16. package/builtin-skills/memo-save/skill.yaml +52 -0
  17. package/builtin-skills/memo-search/README.md +10 -0
  18. package/builtin-skills/memo-search/main.py +97 -0
  19. package/builtin-skills/memo-search/skill.yaml +51 -0
  20. package/builtin-skills/memo-vector-search/main.py +121 -0
  21. package/builtin-skills/memo-vector-search/skill.yaml +53 -0
  22. package/builtin-skills/model-add/main.py +180 -0
  23. package/builtin-skills/model-add/skill.yaml +112 -0
  24. package/builtin-skills/model-list/main.py +93 -0
  25. package/builtin-skills/model-list/skill.yaml +48 -0
  26. package/builtin-skills/model-set/main.py +123 -0
  27. package/builtin-skills/model-set/skill.yaml +69 -0
  28. package/builtin-skills/profile-delete/_profile_lib.py +98 -0
  29. package/builtin-skills/profile-delete/main.py +98 -0
  30. package/builtin-skills/profile-delete/skill.yaml +64 -0
  31. package/builtin-skills/profile-edit/_profile_lib.py +98 -0
  32. package/builtin-skills/profile-edit/main.py +97 -0
  33. package/builtin-skills/profile-edit/skill.yaml +72 -0
  34. package/builtin-skills/profile-save/main.py +52 -0
  35. package/builtin-skills/profile-save/skill.yaml +69 -0
  36. package/builtin-skills/schedule-add/_schedules_lib.py +303 -0
  37. package/builtin-skills/schedule-add/main.py +137 -0
  38. package/builtin-skills/schedule-add/skill.yaml +94 -0
  39. package/builtin-skills/schedule-list/_schedules_lib.py +303 -0
  40. package/builtin-skills/schedule-list/main.py +86 -0
  41. package/builtin-skills/schedule-list/skill.yaml +45 -0
  42. package/builtin-skills/schedule-remove/_schedules_lib.py +303 -0
  43. package/builtin-skills/schedule-remove/main.py +69 -0
  44. package/builtin-skills/schedule-remove/skill.yaml +49 -0
  45. package/builtin-skills/schedule-toggle/_schedules_lib.py +303 -0
  46. package/builtin-skills/schedule-toggle/main.py +78 -0
  47. package/builtin-skills/schedule-toggle/skill.yaml +61 -0
  48. package/builtin-skills/skills-disable/main.py +63 -0
  49. package/builtin-skills/skills-disable/skill.yaml +52 -0
  50. package/builtin-skills/skills-enable/main.py +62 -0
  51. package/builtin-skills/skills-enable/skill.yaml +51 -0
  52. package/builtin-skills/todo-add/main.py +68 -0
  53. package/builtin-skills/todo-add/skill.yaml +53 -0
  54. package/builtin-skills/todo-delete/main.py +65 -0
  55. package/builtin-skills/todo-delete/skill.yaml +47 -0
  56. package/builtin-skills/todo-done/main.py +75 -0
  57. package/builtin-skills/todo-done/skill.yaml +47 -0
  58. package/builtin-skills/todo-list/main.py +91 -0
  59. package/builtin-skills/todo-list/skill.yaml +48 -0
  60. package/builtin-skills/todo-tick/main.py +125 -0
  61. package/builtin-skills/todo-tick/skill.yaml +48 -0
  62. package/dist/builder/build-action-classifier.d.ts +18 -0
  63. package/dist/builder/build-action-classifier.js +142 -0
  64. package/dist/builder/build-commands.d.ts +19 -0
  65. package/dist/builder/build-commands.js +133 -0
  66. package/dist/builder/build-flow.d.ts +72 -0
  67. package/dist/builder/build-flow.js +416 -0
  68. package/dist/builder/builder-session.d.ts +117 -0
  69. package/dist/builder/builder-session.js +1129 -0
  70. package/dist/builder/conversation-router.d.ts +22 -0
  71. package/dist/builder/conversation-router.js +69 -0
  72. package/dist/builder/intent-runtime-store.d.ts +7 -0
  73. package/dist/builder/intent-runtime-store.js +60 -0
  74. package/dist/builder/progress-format.d.ts +7 -0
  75. package/dist/builder/progress-format.js +46 -0
  76. package/dist/cli/index.d.ts +2 -0
  77. package/dist/cli/index.js +2835 -0
  78. package/dist/cli/spinner.d.ts +30 -0
  79. package/dist/cli/spinner.js +164 -0
  80. package/dist/core/ai-provider.d.ts +75 -0
  81. package/dist/core/ai-provider.js +678 -0
  82. package/dist/core/builtin-skills.d.ts +27 -0
  83. package/dist/core/builtin-skills.js +120 -0
  84. package/dist/core/chat-engine.d.ts +70 -0
  85. package/dist/core/chat-engine.js +812 -0
  86. package/dist/core/config.d.ts +127 -0
  87. package/dist/core/config.js +1379 -0
  88. package/dist/core/daily-memory-promotion.d.ts +21 -0
  89. package/dist/core/daily-memory-promotion.js +422 -0
  90. package/dist/core/i18n.d.ts +23 -0
  91. package/dist/core/i18n.js +167 -0
  92. package/dist/core/info-lines.d.ts +5 -0
  93. package/dist/core/info-lines.js +39 -0
  94. package/dist/core/input-schema.d.ts +2 -0
  95. package/dist/core/input-schema.js +156 -0
  96. package/dist/core/intent-router.d.ts +27 -0
  97. package/dist/core/intent-router.js +160 -0
  98. package/dist/core/logger.d.ts +60 -0
  99. package/dist/core/logger.js +240 -0
  100. package/dist/core/memory.d.ts +10 -0
  101. package/dist/core/memory.js +72 -0
  102. package/dist/core/message-utils.d.ts +13 -0
  103. package/dist/core/message-utils.js +104 -0
  104. package/dist/core/notifier.d.ts +17 -0
  105. package/dist/core/notifier.js +424 -0
  106. package/dist/core/output-writer.d.ts +13 -0
  107. package/dist/core/output-writer.js +100 -0
  108. package/dist/core/plan-decision.d.ts +16 -0
  109. package/dist/core/plan-decision.js +88 -0
  110. package/dist/core/profile-memory.d.ts +17 -0
  111. package/dist/core/profile-memory.js +142 -0
  112. package/dist/core/runtime.d.ts +50 -0
  113. package/dist/core/runtime.js +187 -0
  114. package/dist/core/scheduler.d.ts +28 -0
  115. package/dist/core/scheduler.js +155 -0
  116. package/dist/core/secrets.d.ts +31 -0
  117. package/dist/core/secrets.js +214 -0
  118. package/dist/core/service.d.ts +35 -0
  119. package/dist/core/service.js +479 -0
  120. package/dist/core/skill-planner.d.ts +24 -0
  121. package/dist/core/skill-planner.js +100 -0
  122. package/dist/core/skill-registry.d.ts +98 -0
  123. package/dist/core/skill-registry.js +319 -0
  124. package/dist/core/skill-scaffold.d.ts +33 -0
  125. package/dist/core/skill-scaffold.js +256 -0
  126. package/dist/core/skill-settings.d.ts +11 -0
  127. package/dist/core/skill-settings.js +63 -0
  128. package/dist/core/transfer.d.ts +31 -0
  129. package/dist/core/transfer.js +270 -0
  130. package/dist/core/update-notifier.d.ts +2 -0
  131. package/dist/core/update-notifier.js +140 -0
  132. package/dist/discord/bot.d.ts +96 -0
  133. package/dist/discord/bot.js +2424 -0
  134. package/dist/runtimes/codex-app-server.d.ts +53 -0
  135. package/dist/runtimes/codex-app-server.js +305 -0
  136. package/dist/runtimes/python-runner.d.ts +7 -0
  137. package/dist/runtimes/python-runner.js +302 -0
  138. package/dist/runtimes/typescript-runner.d.ts +5 -0
  139. package/dist/runtimes/typescript-runner.js +172 -0
  140. package/dist/skills-sdk/types.d.ts +38 -0
  141. package/dist/skills-sdk/types.js +1 -0
  142. package/dist/telegram/bot.d.ts +94 -0
  143. package/dist/telegram/bot.js +1219 -0
  144. package/install.ps1 +132 -0
  145. package/install.sh +130 -0
  146. package/package.json +60 -0
  147. package/templates/skill-python/main.py +74 -0
  148. package/templates/skill-python/skill.yaml +48 -0
  149. package/templates/skill-typescript/main.ts +87 -0
  150. package/templates/skill-typescript/skill.yaml +42 -0
@@ -0,0 +1,156 @@
1
+ import { l } from "./i18n.js";
2
+ export function validateSkillArgs(manifest, args) {
3
+ try {
4
+ const value = validateValue(args, manifest.input?.schema || { type: "object" }, "input");
5
+ if (!isPlainObject(value)) {
6
+ throw new Error(l("input must be an object", "input は object である必要があります"));
7
+ }
8
+ return value;
9
+ }
10
+ catch (error) {
11
+ const message = error instanceof Error ? error.message : String(error);
12
+ throw new Error(l(`Invalid input for ${manifest.id}: ${message}`, `${manifest.id} の入力が不正です: ${message}`));
13
+ }
14
+ }
15
+ function validateValue(value, schema, label) {
16
+ if (value === undefined) {
17
+ if ("default" in schema) {
18
+ return schema.default;
19
+ }
20
+ return value;
21
+ }
22
+ if (schema.enum && !schema.enum.some((item) => deepEqual(item, value))) {
23
+ throw new Error(l(`${label} must be one of: ${schema.enum.map(String).join(", ")}`, `${label} は次のいずれかである必要があります: ${schema.enum.map(String).join(", ")}`));
24
+ }
25
+ const type = firstType(schema);
26
+ if (!type && (schema.properties || schema.required)) {
27
+ return validateObject(value, schema, label);
28
+ }
29
+ switch (type) {
30
+ case "object":
31
+ return validateObject(value, schema, label);
32
+ case "array":
33
+ return validateArray(value, schema, label);
34
+ case "string":
35
+ return validateString(value, schema, label);
36
+ case "integer":
37
+ return validateNumber(value, schema, label, true);
38
+ case "number":
39
+ return validateNumber(value, schema, label, false);
40
+ case "boolean":
41
+ return validateBoolean(value, label);
42
+ case undefined:
43
+ return value;
44
+ default:
45
+ throw new Error(l(`${label} uses unsupported schema type: ${type}`, `${label} は未対応の schema type です: ${type}`));
46
+ }
47
+ }
48
+ function validateObject(value, schema, label) {
49
+ if (!isPlainObject(value)) {
50
+ throw new Error(l(`${label} must be an object`, `${label} は object である必要があります`));
51
+ }
52
+ const source = value;
53
+ const result = {};
54
+ const properties = schema.properties || {};
55
+ const required = schema.required || [];
56
+ for (const key of required) {
57
+ if (!(key in source) || source[key] === undefined) {
58
+ throw new Error(l(`${label}.${key} is required`, `${label}.${key} は必須です`));
59
+ }
60
+ }
61
+ for (const [key, itemSchema] of Object.entries(properties)) {
62
+ if (key in source) {
63
+ result[key] = validateValue(source[key], itemSchema, `${label}.${key}`);
64
+ }
65
+ else if ("default" in itemSchema) {
66
+ result[key] = itemSchema.default;
67
+ }
68
+ }
69
+ for (const [key, item] of Object.entries(source)) {
70
+ if (key in properties) {
71
+ continue;
72
+ }
73
+ if (schema.additionalProperties === false) {
74
+ throw new Error(l(`${label}.${key} is not allowed`, `${label}.${key} は許可されていません`));
75
+ }
76
+ result[key] = item;
77
+ }
78
+ return result;
79
+ }
80
+ function validateArray(value, schema, label) {
81
+ if (!Array.isArray(value)) {
82
+ throw new Error(l(`${label} must be an array`, `${label} は array である必要があります`));
83
+ }
84
+ if (schema.minItems !== undefined && value.length < schema.minItems) {
85
+ throw new Error(l(`${label} must contain at least ${schema.minItems} item(s)`, `${label} は最低 ${schema.minItems} 件必要です`));
86
+ }
87
+ if (schema.maxItems !== undefined && value.length > schema.maxItems) {
88
+ throw new Error(l(`${label} must contain at most ${schema.maxItems} item(s)`, `${label} は最大 ${schema.maxItems} 件までです`));
89
+ }
90
+ if (!schema.items) {
91
+ return value;
92
+ }
93
+ return value.map((item, index) => validateValue(item, schema.items, `${label}[${index}]`));
94
+ }
95
+ function validateString(value, schema, label) {
96
+ let normalized;
97
+ if (typeof value === "string") {
98
+ normalized = value;
99
+ }
100
+ else if (typeof value === "number" || typeof value === "boolean") {
101
+ normalized = String(value);
102
+ }
103
+ else {
104
+ throw new Error(l(`${label} must be a string`, `${label} は string である必要があります`));
105
+ }
106
+ if (schema.minLength !== undefined && normalized.length < schema.minLength) {
107
+ throw new Error(l(`${label} must be at least ${schema.minLength} character(s)`, `${label} は最低 ${schema.minLength} 文字必要です`));
108
+ }
109
+ if (schema.maxLength !== undefined && normalized.length > schema.maxLength) {
110
+ throw new Error(l(`${label} must be at most ${schema.maxLength} character(s)`, `${label} は最大 ${schema.maxLength} 文字までです`));
111
+ }
112
+ return normalized;
113
+ }
114
+ function validateNumber(value, schema, label, integer) {
115
+ let normalized = value;
116
+ if (typeof normalized === "string" && normalized.trim() !== "") {
117
+ normalized = Number(normalized);
118
+ }
119
+ if (typeof normalized !== "number" || !Number.isFinite(normalized)) {
120
+ throw new Error(l(`${label} must be a ${integer ? "integer" : "number"}`, `${label} は ${integer ? "integer" : "number"} である必要があります`));
121
+ }
122
+ if (integer && !Number.isInteger(normalized)) {
123
+ throw new Error(l(`${label} must be an integer`, `${label} は integer である必要があります`));
124
+ }
125
+ if (schema.minimum !== undefined && normalized < schema.minimum) {
126
+ throw new Error(l(`${label} must be at least ${schema.minimum}`, `${label} は ${schema.minimum} 以上である必要があります`));
127
+ }
128
+ if (schema.maximum !== undefined && normalized > schema.maximum) {
129
+ throw new Error(l(`${label} must be at most ${schema.maximum}`, `${label} は ${schema.maximum} 以下である必要があります`));
130
+ }
131
+ return normalized;
132
+ }
133
+ function validateBoolean(value, label) {
134
+ if (typeof value === "boolean") {
135
+ return value;
136
+ }
137
+ if (value === "true") {
138
+ return true;
139
+ }
140
+ if (value === "false") {
141
+ return false;
142
+ }
143
+ throw new Error(l(`${label} must be a boolean`, `${label} は boolean である必要があります`));
144
+ }
145
+ function firstType(schema) {
146
+ if (Array.isArray(schema.type)) {
147
+ return schema.type[0];
148
+ }
149
+ return schema.type;
150
+ }
151
+ function isPlainObject(value) {
152
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
153
+ }
154
+ function deepEqual(left, right) {
155
+ return JSON.stringify(left) === JSON.stringify(right);
156
+ }
@@ -0,0 +1,27 @@
1
+ import type { AppConfig } from "./config.js";
2
+ import type { SkillManifest } from "./skill-registry.js";
3
+ export type Intent = "chat" | "skill_create" | "skill_edit" | "skill_run" | "unclear";
4
+ export interface IntentResult {
5
+ intent: Intent;
6
+ matched_skill_id?: string;
7
+ matched_draft_id?: string;
8
+ suggested_skill_id?: string;
9
+ confidence: "low" | "med" | "high";
10
+ reason: string;
11
+ }
12
+ export interface IntentRouterTurn {
13
+ role: "user" | "assistant" | "tool";
14
+ content: string;
15
+ }
16
+ export interface ClassifyOptions {
17
+ history?: IntentRouterTurn[];
18
+ modelId?: string;
19
+ drafts?: IntentDraftCandidate[];
20
+ }
21
+ export interface IntentDraftCandidate {
22
+ skill_id: string;
23
+ status?: string;
24
+ summary?: string;
25
+ }
26
+ export declare function classifyIntent(config: AppConfig, userText: string, skills: SkillManifest[], options?: ClassifyOptions): Promise<IntentResult>;
27
+ export declare function buildRouterSystemPrompt(skills: SkillManifest[], drafts?: IntentDraftCandidate[]): string;
@@ -0,0 +1,160 @@
1
+ import { getAiProvider } from "./ai-provider.js";
2
+ const FALLBACK = {
3
+ intent: "chat",
4
+ confidence: "low",
5
+ reason: "router fallback",
6
+ };
7
+ export async function classifyIntent(config, userText, skills, options = {}) {
8
+ const trimmed = userText.trim();
9
+ if (!trimmed) {
10
+ return FALLBACK;
11
+ }
12
+ const system = buildRouterSystemPrompt(skills, options.drafts || []);
13
+ const messages = [{ role: "system", content: system }];
14
+ for (const turn of options.history || []) {
15
+ messages.push({
16
+ role: turn.role === "tool" ? "tool" : turn.role,
17
+ content: turn.content,
18
+ });
19
+ }
20
+ messages.push({
21
+ role: "user",
22
+ content: `Classify the following user input.\n\n<input>${trimmed}</input>\n\nReturn JSON only.`,
23
+ });
24
+ let response;
25
+ try {
26
+ response = await getAiProvider()(config, {
27
+ model_id: options.modelId || config.chat_model_id,
28
+ messages,
29
+ temperature: 0,
30
+ });
31
+ }
32
+ catch {
33
+ return FALLBACK;
34
+ }
35
+ const parsed = parseRouterJson(response.text);
36
+ if (!parsed) {
37
+ return FALLBACK;
38
+ }
39
+ return normalizeIntent(parsed, skills, options.drafts || []);
40
+ }
41
+ export function buildRouterSystemPrompt(skills, drafts = []) {
42
+ const lines = [
43
+ "You are Agent-Sin's intent router.",
44
+ "Classify the user's input as one of these:",
45
+ "- skill_create : intent to create a new reusable skill, automation, or tool",
46
+ "- skill_edit : intent to modify, extend, or change behavior of an existing skill",
47
+ "- skill_run : intent to run an existing skill as-is",
48
+ "- chat : conversation, advice, question, research, or other input not intended as a skill",
49
+ "- unclear : cannot determine",
50
+ "",
51
+ "Rules:",
52
+ "- If the user wants a reusable mechanism, automation, or something they can keep using, classify as skill_create or skill_edit.",
53
+ "- If requirements strongly match an existing skill, prefer skill_run or skill_edit.",
54
+ "- If requirements strongly match an in-progress draft, return skill_create with matched_draft_id and do not create a new suggested_skill_id.",
55
+ "- One-off task requests, opinions, and casual chat are chat.",
56
+ "- If unsure, choose chat rather than unclear.",
57
+ "",
58
+ "<skills>",
59
+ ];
60
+ if (skills.length === 0) {
61
+ lines.push(" <empty/>");
62
+ }
63
+ else {
64
+ for (const skill of skills) {
65
+ const desc = (skill.description || skill.name || "").replaceAll("\n", " ").slice(0, 200);
66
+ lines.push(` <skill id="${escapeXml(skill.id)}">${escapeXml(desc)}</skill>`);
67
+ }
68
+ }
69
+ lines.push("</skills>");
70
+ lines.push("");
71
+ lines.push("<drafts>");
72
+ if (drafts.length === 0) {
73
+ lines.push(" <empty/>");
74
+ }
75
+ else {
76
+ for (const draft of drafts.slice(0, 30)) {
77
+ const summary = (draft.summary || "").replaceAll("\n", " ").slice(0, 200);
78
+ const status = (draft.status || "").replaceAll("\n", " ").slice(0, 40);
79
+ lines.push(` <draft id="${escapeXml(draft.skill_id)}" status="${escapeXml(status)}">${escapeXml(summary)}</draft>`);
80
+ }
81
+ }
82
+ lines.push("</drafts>");
83
+ lines.push("");
84
+ lines.push("Return exactly one JSON object. No explanation and no ``` fences:");
85
+ lines.push('{"intent":"chat|skill_create|skill_edit|skill_run|unclear","matched_skill_id":"...","matched_draft_id":"...","suggested_skill_id":"...","confidence":"low|med|high","reason":"..."}');
86
+ lines.push("Include matched_skill_id only when it exactly matches an id in <skills>, matched_draft_id only when it exactly matches an id in <drafts>, and suggested_skill_id only for new skill creation as one kebab-case suggestion.");
87
+ return lines.join("\n");
88
+ }
89
+ function parseRouterJson(text) {
90
+ const stripped = text
91
+ .replace(/```json\s*/gi, "")
92
+ .replace(/```\s*$/g, "")
93
+ .trim();
94
+ const start = stripped.indexOf("{");
95
+ const end = stripped.lastIndexOf("}");
96
+ if (start < 0 || end <= start) {
97
+ return null;
98
+ }
99
+ const slice = stripped.slice(start, end + 1);
100
+ try {
101
+ const parsed = JSON.parse(slice);
102
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
103
+ return parsed;
104
+ }
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ return null;
110
+ }
111
+ function normalizeIntent(raw, skills, drafts = []) {
112
+ const intentRaw = String(raw.intent || "").trim().toLowerCase();
113
+ const intent = (["chat", "skill_create", "skill_edit", "skill_run", "unclear"].includes(intentRaw)
114
+ ? intentRaw
115
+ : "chat");
116
+ const confidenceRaw = String(raw.confidence || "low").trim().toLowerCase();
117
+ const confidence = (["low", "med", "high"].includes(confidenceRaw) ? confidenceRaw : "low");
118
+ const reason = typeof raw.reason === "string" ? raw.reason.slice(0, 240) : "";
119
+ const matchedRaw = typeof raw.matched_skill_id === "string" ? raw.matched_skill_id.trim() : "";
120
+ const matchedDraftRaw = typeof raw.matched_draft_id === "string" ? raw.matched_draft_id.trim() : "";
121
+ const suggestedRaw = typeof raw.suggested_skill_id === "string" ? raw.suggested_skill_id.trim() : "";
122
+ const matched = matchedRaw && skills.some((s) => s.id === matchedRaw) ? matchedRaw : undefined;
123
+ const matchedDraft = matchedDraftRaw && drafts.some((draft) => draft.skill_id === matchedDraftRaw)
124
+ ? matchedDraftRaw
125
+ : undefined;
126
+ const suggested = suggestedRaw ? sanitizeSkillId(suggestedRaw) : undefined;
127
+ if ((intent === "skill_run" || intent === "skill_edit") && !matched) {
128
+ return {
129
+ intent: intent === "skill_edit" ? "skill_create" : "chat",
130
+ suggested_skill_id: suggested,
131
+ confidence,
132
+ reason: reason || "matched_skill_id missing",
133
+ };
134
+ }
135
+ return {
136
+ intent,
137
+ matched_skill_id: matched,
138
+ matched_draft_id: matchedDraft,
139
+ suggested_skill_id: intent === "skill_create" ? suggested : undefined,
140
+ confidence,
141
+ reason,
142
+ };
143
+ }
144
+ function sanitizeSkillId(raw) {
145
+ const cleaned = raw
146
+ .toLowerCase()
147
+ .replaceAll("_", "-")
148
+ .replace(/[^a-z0-9-]/g, "-")
149
+ .replace(/-+/g, "-")
150
+ .replace(/^-+|-+$/g, "")
151
+ .slice(0, 48);
152
+ return cleaned ? cleaned : undefined;
153
+ }
154
+ function escapeXml(value) {
155
+ return value
156
+ .replaceAll("&", "&amp;")
157
+ .replaceAll("<", "&lt;")
158
+ .replaceAll(">", "&gt;")
159
+ .replaceAll('"', "&quot;");
160
+ }
@@ -0,0 +1,60 @@
1
+ import type { AppConfig } from "./config.js";
2
+ export interface RunLogRecord {
3
+ run_id: string;
4
+ skill_id: string;
5
+ status: string;
6
+ started_at: string;
7
+ finished_at: string;
8
+ attempts?: number;
9
+ input: unknown;
10
+ result?: unknown;
11
+ error?: string;
12
+ saved_outputs?: unknown[];
13
+ memory_path?: string;
14
+ dry_run?: boolean;
15
+ ctx_logs?: Array<{
16
+ level: "info" | "warn" | "error";
17
+ message: string;
18
+ ts?: string;
19
+ }>;
20
+ }
21
+ export type EventLogLevel = "info" | "warn" | "error";
22
+ export type EventLogSource = "cli" | "chat" | "skill" | "setup" | "build" | "schedule" | "discord" | "telegram";
23
+ export interface EventLogEntry {
24
+ ts?: string;
25
+ level: EventLogLevel;
26
+ source: EventLogSource;
27
+ event: string;
28
+ message?: string;
29
+ details?: Record<string, unknown>;
30
+ }
31
+ export interface ReadEventLogOptions {
32
+ tail?: number;
33
+ level?: EventLogLevel;
34
+ source?: EventLogSource;
35
+ }
36
+ export interface ReadEventLogResult extends EventLogEntry {
37
+ ts: string;
38
+ raw: string;
39
+ }
40
+ export declare function createRunId(): string;
41
+ export declare function writeRunLog(config: AppConfig, record: RunLogRecord): Promise<string>;
42
+ export declare function appendEventLog(config: AppConfig, entry: EventLogEntry): Promise<void>;
43
+ export type ConversationLogSource = "chat" | "builder";
44
+ export type ConversationLogRole = "user" | "assistant" | "system" | "tool";
45
+ export interface ConversationLogEntry {
46
+ ts?: string;
47
+ source: ConversationLogSource;
48
+ role: ConversationLogRole;
49
+ content: string;
50
+ model_id?: string;
51
+ skill_id?: string;
52
+ session_id?: string;
53
+ details?: Record<string, unknown>;
54
+ }
55
+ export declare function appendConversationLog(config: AppConfig, entry: ConversationLogEntry): Promise<void>;
56
+ export declare function dailyConversationMemoryFile(config: AppConfig, date?: Date): string;
57
+ export declare function pruneOldLogs(config: AppConfig): Promise<void>;
58
+ export declare function readEventLog(config: AppConfig, options?: ReadEventLogOptions): Promise<ReadEventLogResult[]>;
59
+ export declare function listRunLogs(config: AppConfig, skillId?: string): Promise<RunLogRecord[]>;
60
+ export declare function readRunLog(config: AppConfig, runId: string): Promise<RunLogRecord>;
@@ -0,0 +1,240 @@
1
+ import { appendFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
2
+ import crypto from "node:crypto";
3
+ import path from "node:path";
4
+ import { l } from "./i18n.js";
5
+ export function createRunId() {
6
+ return crypto.randomUUID();
7
+ }
8
+ export async function writeRunLog(config, record) {
9
+ const runsDir = path.join(config.logs_dir, "runs");
10
+ await mkdir(runsDir, { recursive: true });
11
+ const file = path.join(runsDir, `${record.run_id}.json`);
12
+ await writeFile(file, JSON.stringify(record, null, 2), "utf8");
13
+ await appendFile(path.join(config.logs_dir, "app.log"), `${record.finished_at} ${record.status} ${record.skill_id} ${record.run_id}\n`, "utf8");
14
+ await appendEventLog(config, {
15
+ ts: record.finished_at,
16
+ level: record.status === "ok" ? "info" : record.status === "error" ? "error" : "warn",
17
+ source: "skill",
18
+ event: "run",
19
+ message: record.error || extractSummary(record.result),
20
+ details: {
21
+ run_id: record.run_id,
22
+ skill_id: record.skill_id,
23
+ status: record.status,
24
+ attempts: record.attempts,
25
+ saved: Array.isArray(record.saved_outputs) ? record.saved_outputs.length : 0,
26
+ },
27
+ });
28
+ return file;
29
+ }
30
+ export async function appendEventLog(config, entry) {
31
+ await mkdir(config.logs_dir, { recursive: true });
32
+ const ts = entry.ts || new Date().toISOString();
33
+ const line = JSON.stringify({ ...entry, ts });
34
+ await appendFile(path.join(config.logs_dir, "events.jsonl"), `${line}\n`, "utf8");
35
+ }
36
+ let conversationPruneAt = 0;
37
+ export async function appendConversationLog(config, entry) {
38
+ const dir = path.join(config.logs_dir, "conversations");
39
+ await mkdir(dir, { recursive: true });
40
+ const ts = entry.ts || new Date().toISOString();
41
+ const day = ts.slice(0, 10);
42
+ const file = path.join(dir, `${day}.jsonl`);
43
+ await appendFile(file, `${JSON.stringify({ ...entry, ts })}\n`, "utf8");
44
+ await appendDailyConversationMemory(config, { ...entry, ts });
45
+ await maybePruneOldLogs(config);
46
+ }
47
+ export function dailyConversationMemoryFile(config, date = new Date()) {
48
+ const yyyy = String(date.getFullYear());
49
+ const MM = String(date.getMonth() + 1).padStart(2, "0");
50
+ const dd = String(date.getDate()).padStart(2, "0");
51
+ return path.join(config.memory_dir, "daily", yyyy, MM, `${yyyy}-${MM}-${dd}.md`);
52
+ }
53
+ async function appendDailyConversationMemory(config, entry) {
54
+ const date = new Date(entry.ts);
55
+ const file = Number.isNaN(date.getTime())
56
+ ? dailyConversationMemoryFile(config)
57
+ : dailyConversationMemoryFile(config, date);
58
+ const existing = await readTextIfExists(file);
59
+ const titleDate = path.basename(file, ".md");
60
+ const prefix = existing ? "" : `# ${titleDate}\n\n`;
61
+ await mkdir(path.dirname(file), { recursive: true });
62
+ await appendFile(file, `${prefix}${formatConversationMarkdownEntry(entry)}\n`, "utf8");
63
+ }
64
+ function formatConversationMarkdownEntry(entry) {
65
+ const label = [entry.source, entry.role].filter(Boolean).join(" ");
66
+ const content = entry.content.trim() || "(empty)";
67
+ return [`## ${entry.ts} ${label}`, "", content].join("\n");
68
+ }
69
+ async function readTextIfExists(file) {
70
+ try {
71
+ return await readFile(file, "utf8");
72
+ }
73
+ catch {
74
+ return "";
75
+ }
76
+ }
77
+ async function maybePruneOldLogs(config) {
78
+ const now = Date.now();
79
+ if (now - conversationPruneAt < 60 * 60 * 1000) {
80
+ return;
81
+ }
82
+ conversationPruneAt = now;
83
+ try {
84
+ await pruneOldLogs(config);
85
+ }
86
+ catch {
87
+ // best-effort
88
+ }
89
+ }
90
+ export async function pruneOldLogs(config) {
91
+ const days = config.log_retention_days;
92
+ if (days && days > 0) {
93
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
94
+ await pruneFilesByMtime(path.join(config.logs_dir, "conversations"), cutoff);
95
+ await pruneFilesByMtime(path.join(config.logs_dir, "runs"), cutoff);
96
+ }
97
+ const eventDays = config.event_log_retention_days;
98
+ if (eventDays && eventDays > 0) {
99
+ const cutoff = Date.now() - eventDays * 24 * 60 * 60 * 1000;
100
+ await pruneLinesByLeadingTimestamp(path.join(config.logs_dir, "events.jsonl"), cutoff, (line) => {
101
+ try {
102
+ const parsed = JSON.parse(line);
103
+ return parsed.ts ? Date.parse(parsed.ts) : NaN;
104
+ }
105
+ catch {
106
+ return NaN;
107
+ }
108
+ });
109
+ await pruneLinesByLeadingTimestamp(path.join(config.logs_dir, "app.log"), cutoff, (line) => {
110
+ const head = line.slice(0, 32).split(/\s/, 1)[0];
111
+ return head ? Date.parse(head) : NaN;
112
+ });
113
+ }
114
+ }
115
+ async function pruneLinesByLeadingTimestamp(file, cutoff, extractTime) {
116
+ let raw;
117
+ try {
118
+ raw = await readFile(file, "utf8");
119
+ }
120
+ catch {
121
+ return;
122
+ }
123
+ const lines = raw.split(/\r?\n/);
124
+ const kept = [];
125
+ let removed = 0;
126
+ for (const line of lines) {
127
+ if (line.length === 0) {
128
+ continue;
129
+ }
130
+ const ts = extractTime(line);
131
+ if (Number.isFinite(ts) && ts < cutoff) {
132
+ removed += 1;
133
+ continue;
134
+ }
135
+ kept.push(line);
136
+ }
137
+ if (removed === 0) {
138
+ return;
139
+ }
140
+ const next = kept.length > 0 ? `${kept.join("\n")}\n` : "";
141
+ await writeFile(file, next, "utf8");
142
+ }
143
+ async function pruneFilesByMtime(dir, cutoff) {
144
+ let entries;
145
+ try {
146
+ entries = await readdir(dir);
147
+ }
148
+ catch {
149
+ return;
150
+ }
151
+ for (const name of entries) {
152
+ const file = path.join(dir, name);
153
+ try {
154
+ const info = await stat(file);
155
+ if (info.isFile() && info.mtimeMs < cutoff) {
156
+ await rm(file, { force: true });
157
+ }
158
+ }
159
+ catch {
160
+ // ignore
161
+ }
162
+ }
163
+ }
164
+ export async function readEventLog(config, options = {}) {
165
+ const file = path.join(config.logs_dir, "events.jsonl");
166
+ let raw;
167
+ try {
168
+ raw = await readFile(file, "utf8");
169
+ }
170
+ catch {
171
+ return [];
172
+ }
173
+ const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
174
+ const entries = [];
175
+ for (const line of lines) {
176
+ let parsed;
177
+ try {
178
+ parsed = JSON.parse(line);
179
+ }
180
+ catch {
181
+ continue;
182
+ }
183
+ if (!parsed.level || !parsed.source || !parsed.event) {
184
+ continue;
185
+ }
186
+ if (options.level && parsed.level !== options.level) {
187
+ continue;
188
+ }
189
+ if (options.source && parsed.source !== options.source) {
190
+ continue;
191
+ }
192
+ entries.push({ ...parsed, ts: parsed.ts || "", raw: line });
193
+ }
194
+ if (options.tail && options.tail > 0) {
195
+ return entries.slice(-options.tail);
196
+ }
197
+ return entries;
198
+ }
199
+ function extractSummary(result) {
200
+ if (!result || typeof result !== "object") {
201
+ return undefined;
202
+ }
203
+ const summary = result.summary;
204
+ if (typeof summary === "string" && summary.length > 0) {
205
+ return summary;
206
+ }
207
+ return undefined;
208
+ }
209
+ export async function listRunLogs(config, skillId) {
210
+ const runsDir = path.join(config.logs_dir, "runs");
211
+ let files;
212
+ try {
213
+ files = await readdir(runsDir);
214
+ }
215
+ catch {
216
+ return [];
217
+ }
218
+ const records = [];
219
+ for (const file of files.filter((item) => item.endsWith(".json")).sort().reverse()) {
220
+ try {
221
+ const record = JSON.parse(await readFile(path.join(runsDir, file), "utf8"));
222
+ if (!skillId || record.skill_id === skillId) {
223
+ records.push(record);
224
+ }
225
+ }
226
+ catch {
227
+ continue;
228
+ }
229
+ }
230
+ return records;
231
+ }
232
+ export async function readRunLog(config, runId) {
233
+ const file = path.join(config.logs_dir, "runs", `${runId}.json`);
234
+ try {
235
+ return JSON.parse(await readFile(file, "utf8"));
236
+ }
237
+ catch {
238
+ throw new Error(l(`Run log not found: ${runId}`, `実行ログが見つかりません: ${runId}`));
239
+ }
240
+ }
@@ -0,0 +1,10 @@
1
+ import type { AppConfig } from "./config.js";
2
+ import type { SkillManifest } from "./skill-registry.js";
3
+ export interface SkillMemoryAccess {
4
+ namespace: string;
5
+ canRead: boolean;
6
+ canWrite: boolean;
7
+ }
8
+ export declare function skillMemoryAccess(manifest: SkillManifest): SkillMemoryAccess;
9
+ export declare function loadSkillMemory(config: AppConfig, manifest: SkillManifest): Promise<Record<string, unknown>>;
10
+ export declare function saveSkillMemoryUpdates(config: AppConfig, manifest: SkillManifest, updates: Record<string, unknown>): Promise<string | undefined>;