@tamsensedev/runner 0.10.0 → 0.18.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.
- package/LICENSE +5 -5
- package/README.md +61 -50
- package/dist/cli.js +1188 -324
- package/dist/cli.js.map +1 -1
- package/dist/recorder.cjs +42 -6
- package/dist/recorder.cjs.map +1 -1
- package/library/.claude-plugin/plugin.json +5 -5
- package/library/THIRD_PARTY_NOTICES.md +123 -123
- package/library/agents/code-architect.md +67 -67
- package/library/agents/code-explorer.md +61 -61
- package/library/agents/code-reviewer.md +73 -73
- package/library/agents/code-simplifier.md +62 -62
- package/library/agents/design-critic.md +52 -52
- package/library/agents/second-opinion.md +64 -64
- package/library/agents/security-auditor.md +86 -86
- package/library/agents/silent-failure-hunter.md +70 -70
- package/library/agents/test-coverage-analyzer.md +78 -78
- package/library/agents/test-runner.md +48 -48
- package/library/agents/ui-tester.md +50 -50
- package/library/licenses/Apache-2.0.txt +202 -202
- package/library/skills/accessibility/SKILL.md +144 -144
- package/library/skills/accessibility/references/wcag-checks.md +168 -168
- package/library/skills/addressing-review-feedback/SKILL.md +109 -109
- package/library/skills/app-screens/SKILL.md +96 -96
- package/library/skills/app-screens/references/shell-kpis-charts.md +108 -108
- package/library/skills/app-screens/references/states-density-phone.md +54 -54
- package/library/skills/app-screens/references/tables-forms-settings.md +104 -104
- package/library/skills/avoiding-generic-ai-design/SKILL.md +92 -92
- package/library/skills/avoiding-generic-ai-design/references/tells.md +112 -112
- package/library/skills/clean-commits/SKILL.md +85 -85
- package/library/skills/design-review/SKILL.md +107 -107
- package/library/skills/design-review/references/critique-passes.md +101 -101
- package/library/skills/design-review/references/screenshot-procedure.md +121 -121
- package/library/skills/differential-security-review/LICENSE +427 -427
- package/library/skills/differential-security-review/SKILL.md +105 -105
- package/library/skills/differential-security-review/references/adversarial.md +115 -115
- package/library/skills/differential-security-review/references/methodology.md +145 -145
- package/library/skills/differential-security-review/references/patterns.md +108 -108
- package/library/skills/false-positive-check/LICENSE +427 -427
- package/library/skills/false-positive-check/SKILL.md +95 -95
- package/library/skills/false-positive-check/references/bug-class-verification.md +128 -128
- package/library/skills/false-positive-check/references/deep-verification.md +113 -113
- package/library/skills/false-positive-check/references/evidence-templates.md +93 -93
- package/library/skills/false-positive-check/references/false-positive-patterns.md +122 -122
- package/library/skills/false-positive-check/references/gate-reviews.md +36 -36
- package/library/skills/false-positive-check/references/standard-verification.md +80 -80
- package/library/skills/focused-implementation/SKILL.md +104 -104
- package/library/skills/implementation-plan/SKILL.md +66 -66
- package/library/skills/interface-design/SKILL.md +144 -144
- package/library/skills/interface-design/references/craft-defaults.md +130 -130
- package/library/skills/interface-design/references/new-direction.md +62 -62
- package/library/skills/interface-design/references/writing.md +52 -52
- package/library/skills/interface-polish/SKILL.md +103 -103
- package/library/skills/interface-polish/references/detail-rules.md +154 -154
- package/library/skills/interface-polish/references/worst-case-data.md +158 -158
- package/library/skills/motion-design/SKILL.md +106 -106
- package/library/skills/motion-design/references/recipes.md +243 -243
- package/library/skills/postgres-performance/SKILL.md +97 -97
- package/library/skills/postgres-performance/references/rules.md +265 -265
- package/library/skills/project-orientation/SKILL.md +77 -77
- package/library/skills/reviewing-changes/SKILL.md +183 -183
- package/library/skills/safe-database-changes/SKILL.md +111 -111
- package/library/skills/safe-database-changes/references/backfills.md +75 -75
- package/library/skills/safe-database-changes/references/migration-tools.md +69 -69
- package/library/skills/safe-database-changes/references/postgres-ddl.md +199 -199
- package/library/skills/security-review/SKILL.md +132 -132
- package/library/skills/security-review/references/checks-by-area.md +107 -107
- package/library/skills/security-review/references/do-not-report.md +86 -86
- package/library/skills/spec-writing/SKILL.md +144 -144
- package/library/skills/systematic-debugging/SKILL.md +130 -130
- package/library/skills/systematic-debugging/references/flaky-tests.md +51 -51
- package/library/skills/systematic-debugging/references/root-cause-tracing.md +55 -55
- package/library/skills/task-interview/SKILL.md +84 -84
- package/library/skills/test-driven-development/SKILL.md +116 -116
- package/library/skills/test-driven-development/references/writing-good-tests.md +85 -85
- package/library/skills/ui-testing/SKILL.md +102 -102
- package/library/skills/ui-testing/references/headless-check.md +295 -280
- package/library/skills/ui-testing/references/local-stand.md +51 -51
- package/library/skills/ui-testing/references/playwright-patterns.md +134 -134
- package/library/skills/ui-testing/references/recording.md +61 -58
- package/library/skills/verification-before-completion/SKILL.md +113 -113
- package/library/skills/web-interface-guidelines/SKILL.md +202 -202
- package/package.json +46 -46
package/dist/recorder.cjs
CHANGED
|
@@ -34,6 +34,7 @@ __export(recorder_exports, {
|
|
|
34
34
|
DEFAULT_TITLE_ENV: () => DEFAULT_TITLE_ENV,
|
|
35
35
|
DEFAULT_TRACKER_ORIGINS: () => DEFAULT_TRACKER_ORIGINS,
|
|
36
36
|
MARK_TAG: () => MARK_TAG,
|
|
37
|
+
MAX_NOTES_LENGTH: () => MAX_NOTES_LENGTH,
|
|
37
38
|
MAX_PURPOSE_LENGTH: () => MAX_PURPOSE_LENGTH,
|
|
38
39
|
MAX_TITLE_LENGTH: () => MAX_TITLE_LENGTH,
|
|
39
40
|
META_SUFFIX: () => META_SUFFIX,
|
|
@@ -43,6 +44,8 @@ __export(recorder_exports, {
|
|
|
43
44
|
SERVER_ENV: () => SERVER_ENV,
|
|
44
45
|
automaticCaption: () => automaticCaption,
|
|
45
46
|
checkCaption: () => checkCaption,
|
|
47
|
+
checkNotes: () => checkNotes,
|
|
48
|
+
clip: () => clip,
|
|
46
49
|
closeRecorded: () => closeRecorded,
|
|
47
50
|
installAutoRecording: () => installAutoRecording,
|
|
48
51
|
isExternalWrite: () => isExternalWrite,
|
|
@@ -74,6 +77,7 @@ var REPLAY_SUFFIX = ".rrweb.ndjson";
|
|
|
74
77
|
var META_SUFFIX = ".json";
|
|
75
78
|
var MAX_TITLE_LENGTH = 60;
|
|
76
79
|
var MAX_PURPOSE_LENGTH = 160;
|
|
80
|
+
var MAX_NOTES_LENGTH = 600;
|
|
77
81
|
var DEFAULT_TRACKER_ORIGINS = ["https://my.tamsense.com"];
|
|
78
82
|
var READ_METHODS = ["GET", "HEAD", "OPTIONS"];
|
|
79
83
|
var BINDING = "__tamsenseRecord";
|
|
@@ -82,7 +86,10 @@ var FLUSH_MS = 100;
|
|
|
82
86
|
var MAX_SESSION_BYTES = 300 * 1024 * 1024;
|
|
83
87
|
var GAP_CHECK_MS = 1500;
|
|
84
88
|
var MAX_LABEL_LENGTH = 120;
|
|
89
|
+
var RRWEB_INCREMENTAL = 3;
|
|
85
90
|
var RRWEB_META = 4;
|
|
91
|
+
var RRWEB_SOURCE_MOUSE_INTERACTION = 2;
|
|
92
|
+
var RRWEB_SOURCE_INPUT = 5;
|
|
86
93
|
function state() {
|
|
87
94
|
const holder = globalThis;
|
|
88
95
|
holder[STATE_KEY] ??= { contexts: /* @__PURE__ */ new WeakMap(), pages: /* @__PURE__ */ new WeakMap(), instrumented: /* @__PURE__ */ new WeakSet(), script: null, hooked: false };
|
|
@@ -133,6 +140,13 @@ function checkCaption(caption) {
|
|
|
133
140
|
}
|
|
134
141
|
return { title, purpose };
|
|
135
142
|
}
|
|
143
|
+
function checkNotes(notes) {
|
|
144
|
+
const text = typeof notes === "string" ? notes.trim() : "";
|
|
145
|
+
if (text.length > MAX_NOTES_LENGTH) {
|
|
146
|
+
throw new Error(`Keep the notes within ${MAX_NOTES_LENGTH} characters: what you opened, clicked and expected, and whether it passed.`);
|
|
147
|
+
}
|
|
148
|
+
return text;
|
|
149
|
+
}
|
|
136
150
|
function clip(text, max) {
|
|
137
151
|
return text.length > max ? `${text.slice(0, max - 1).trimEnd()}\u2026` : text;
|
|
138
152
|
}
|
|
@@ -163,11 +177,22 @@ const send = (extra) => {
|
|
|
163
177
|
buffer = [];
|
|
164
178
|
return window.${BINDING}(JSON.stringify({ events, ...extra })).catch(() => {});
|
|
165
179
|
};
|
|
180
|
+
const urgent = (event) => event.type === ${RRWEB_INCREMENTAL} && (event.data.source === ${RRWEB_SOURCE_MOUSE_INTERACTION} || event.data.source === ${RRWEB_SOURCE_INPUT});
|
|
181
|
+
const placeAtTarget = (interaction) => {
|
|
182
|
+
if (interaction.x !== 0 || interaction.y !== 0) return;
|
|
183
|
+
const node = rrweb.record.mirror.getNode(interaction.id);
|
|
184
|
+
if (!node || typeof node.getBoundingClientRect !== 'function') return;
|
|
185
|
+
const box = node.getBoundingClientRect();
|
|
186
|
+
interaction.x = Math.round(box.left + box.width / 2);
|
|
187
|
+
interaction.y = Math.round(box.top + box.height / 2);
|
|
188
|
+
};
|
|
166
189
|
rrweb.record({
|
|
167
190
|
emit(event) {
|
|
168
191
|
buffer.push(event);
|
|
169
|
-
if (
|
|
192
|
+
if (urgent(event)) send();
|
|
193
|
+
else if (!timer) timer = setTimeout(send, ${FLUSH_MS});
|
|
170
194
|
},
|
|
195
|
+
hooks: { mouseInteraction: placeAtTarget },
|
|
171
196
|
maskInputOptions: { password: true },
|
|
172
197
|
});
|
|
173
198
|
const crossOrigin = (frame) => { try { return new URL(frame.src, location.href).origin !== location.origin; } catch { return false; } };
|
|
@@ -180,6 +205,7 @@ const checkGaps = () => {
|
|
|
180
205
|
};
|
|
181
206
|
window.__tamsenseFlush = () => send();
|
|
182
207
|
window.__tamsenseMark = (label) => { rrweb.record.addCustomEvent('${MARK_TAG}', { label }); return send(); };
|
|
208
|
+
addEventListener('beforeunload', () => { send(); });
|
|
183
209
|
addEventListener('pagehide', () => { send(); });
|
|
184
210
|
addEventListener('load', () => setTimeout(checkGaps, ${GAP_CHECK_MS}));
|
|
185
211
|
})();`;
|
|
@@ -198,7 +224,7 @@ var Session = class {
|
|
|
198
224
|
const id = `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
199
225
|
this.file = (0, import_node_path.join)(dir, `${id}${REPLAY_SUFFIX}`);
|
|
200
226
|
this.metaFile = (0, import_node_path.join)(dir, `${id}${META_SUFFIX}`);
|
|
201
|
-
this.meta = { version: 1, id, ...recording.caption, startedAt: Date.now(), endedAt: null, url, marks: [], video: null, gaps: [], blockedTrackerRequests: 0, blockedExternalWrites: 0, truncated: false, automatic: recording.automatic };
|
|
227
|
+
this.meta = { version: 1, id, ...recording.caption, startedAt: Date.now(), endedAt: null, url, marks: [], video: null, gaps: [], blockedTrackerRequests: 0, blockedExternalWrites: 0, truncated: false, automatic: recording.automatic, notes: "" };
|
|
202
228
|
(0, import_node_fs.writeFileSync)(this.file, "");
|
|
203
229
|
this.save();
|
|
204
230
|
}
|
|
@@ -242,8 +268,8 @@ var Session = class {
|
|
|
242
268
|
}
|
|
243
269
|
}
|
|
244
270
|
summary() {
|
|
245
|
-
const { title, purpose, video, marks, gaps, blockedTrackerRequests, blockedExternalWrites } = this.meta;
|
|
246
|
-
return { title, purpose, replay: this.file, video, marks: marks.length, gaps, blockedTrackerRequests, blockedExternalWrites };
|
|
271
|
+
const { title, purpose, notes, video, marks, gaps, blockedTrackerRequests, blockedExternalWrites } = this.meta;
|
|
272
|
+
return { title, purpose, notes, replay: this.file, video, marks: marks.length, gaps, blockedTrackerRequests, blockedExternalWrites };
|
|
247
273
|
}
|
|
248
274
|
};
|
|
249
275
|
function countBlocked(route, counter) {
|
|
@@ -344,9 +370,16 @@ async function mark(page, label) {
|
|
|
344
370
|
await page.evaluate((value) => window.__tamsenseMark?.(value), text).catch(() => {
|
|
345
371
|
});
|
|
346
372
|
}
|
|
347
|
-
async function closeRecorded(context) {
|
|
348
|
-
await context.close();
|
|
373
|
+
async function closeRecorded(context, options = {}) {
|
|
349
374
|
const sessions = state().contexts.get(context)?.sessions ?? [];
|
|
375
|
+
const notes = clip(String(options.notes ?? "").trim(), MAX_NOTES_LENGTH);
|
|
376
|
+
if (notes) {
|
|
377
|
+
for (const session of sessions) {
|
|
378
|
+
session.meta.notes = notes;
|
|
379
|
+
session.save();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
await context.close();
|
|
350
383
|
const summaries = sessions.map((session) => session.summary());
|
|
351
384
|
for (const summary of summaries) {
|
|
352
385
|
if (summary.gaps.length > 0 && !summary.video) {
|
|
@@ -401,6 +434,7 @@ if (import_node_process.default.env[RECORDINGS_DIR_ENV]) {
|
|
|
401
434
|
DEFAULT_TITLE_ENV,
|
|
402
435
|
DEFAULT_TRACKER_ORIGINS,
|
|
403
436
|
MARK_TAG,
|
|
437
|
+
MAX_NOTES_LENGTH,
|
|
404
438
|
MAX_PURPOSE_LENGTH,
|
|
405
439
|
MAX_TITLE_LENGTH,
|
|
406
440
|
META_SUFFIX,
|
|
@@ -410,6 +444,8 @@ if (import_node_process.default.env[RECORDINGS_DIR_ENV]) {
|
|
|
410
444
|
SERVER_ENV,
|
|
411
445
|
automaticCaption,
|
|
412
446
|
checkCaption,
|
|
447
|
+
checkNotes,
|
|
448
|
+
clip,
|
|
413
449
|
closeRecorded,
|
|
414
450
|
installAutoRecording,
|
|
415
451
|
isExternalWrite,
|
package/dist/recorder.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/recorder.ts","../../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.15_tsx@4.21.0_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js"],"sourcesContent":["import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\r\nimport Module, { createRequire } from 'node:module'\r\nimport { basename, dirname, join } from 'node:path'\r\nimport process from 'node:process'\r\n\r\n// Запись браузера агента: rrweb пишет каждую страницу в свой файл NDJSON в папке запуска, по желанию ещё и видео.\r\n// Модуль самодостаточный: программа подключает его ко всем процессам Node агента через NODE_OPTIONS --require,\r\n// и тогда любой браузер Playwright записывается сам; скрипты агента импортируют его же, чтобы дать записи название и отметки\r\n\r\nexport const RECORDINGS_DIR_ENV = 'TAMSENSE_RECORDINGS_DIR'\r\nexport const SERVER_ENV = 'TAMSENSE_SERVER'\r\nexport const DEFAULT_TITLE_ENV = 'TAMSENSE_RECORDING_TITLE'\r\nexport const DEFAULT_PURPOSE_ENV = 'TAMSENSE_RECORDING_PURPOSE'\r\nexport const MARK_TAG = 'tamsense.mark'\r\nexport const REPLAY_SUFFIX = '.rrweb.ndjson'\r\nexport const META_SUFFIX = '.json'\r\nexport const MAX_TITLE_LENGTH = 60\r\nexport const MAX_PURPOSE_LENGTH = 160\r\n// трекер TAMsense по умолчанию шлёт сюда; адрес сервера из настроек программы добавляется к нему\r\nexport const DEFAULT_TRACKER_ORIGINS = ['https://my.tamsense.com']\r\n\r\n// эти методы только читают; остальные к адресам вне компьютера отменяются\r\nexport const READ_METHODS = ['GET', 'HEAD', 'OPTIONS']\r\n\r\nconst BINDING = '__tamsenseRecord'\r\nconst STATE_KEY = Symbol.for('tamsense.recorder')\r\nconst FLUSH_MS = 100\r\nconst MAX_SESSION_BYTES = 300 * 1024 * 1024\r\nconst GAP_CHECK_MS = 1500\r\nconst MAX_LABEL_LENGTH = 120\r\nconst RRWEB_META = 4\r\n\r\nexport interface RecordingCaption {\r\n title: string\r\n purpose: string\r\n}\r\n\r\nexport interface RecordedContextOptions extends RecordingCaption {\r\n // видео в придачу к записи: для canvas, карт, видео на странице, чужих iframe или когда его попросили\r\n video?: boolean\r\n // обычные настройки browser.newContext: viewport, storageState и другие\r\n context?: Record<string, unknown>\r\n}\r\n\r\nexport interface SessionMark {\r\n ts: number\r\n label: string\r\n}\r\n\r\n// Что знает программа о записи одной страницы; лежит рядом с файлом записи\r\nexport interface SessionMeta {\r\n version: 1\r\n id: string\r\n title: string\r\n purpose: string\r\n startedAt: number\r\n endedAt: number | null\r\n url: string\r\n marks: SessionMark[]\r\n video: string | null\r\n gaps: string[]\r\n blockedTrackerRequests: number\r\n blockedExternalWrites: number\r\n truncated: boolean\r\n // записано само, без названия от агента\r\n automatic: boolean\r\n}\r\n\r\nexport interface RecordingSummary {\r\n title: string\r\n purpose: string\r\n replay: string\r\n video: string | null\r\n marks: number\r\n gaps: string[]\r\n blockedTrackerRequests: number\r\n blockedExternalWrites: number\r\n}\r\n\r\ninterface PlaywrightVideo {\r\n path: () => Promise<string>\r\n}\r\n\r\ninterface PlaywrightPage {\r\n url: () => string\r\n isClosed: () => boolean\r\n on: (event: 'close', listener: () => void) => unknown\r\n evaluate: (fn: string | ((arg: never) => unknown), arg?: unknown) => Promise<unknown>\r\n video: () => PlaywrightVideo | null\r\n}\r\n\r\ninterface PlaywrightRoute {\r\n fulfill: (response: { status: number, contentType: string, body: string }) => Promise<void>\r\n abort: (errorCode?: string) => Promise<void>\r\n continue: () => Promise<void>\r\n request: () => { method: () => string, url: () => string, frame: () => { page: () => PlaywrightPage } }\r\n}\r\n\r\nexport interface PlaywrightContext {\r\n pages: () => PlaywrightPage[]\r\n on: (event: 'page' | 'close', listener: (page: PlaywrightPage) => void) => unknown\r\n exposeBinding: (name: string, callback: (source: { page: PlaywrightPage }, payload: string) => unknown) => Promise<void>\r\n addInitScript: (script: { content: string }) => Promise<void>\r\n route: (url: (url: URL) => boolean, handler: (route: PlaywrightRoute) => Promise<void>) => Promise<void>\r\n close: (options?: unknown) => Promise<void>\r\n}\r\n\r\nexport interface PlaywrightBrowser {\r\n newContext: (options?: Record<string, unknown>) => Promise<PlaywrightContext>\r\n}\r\n\r\ninterface Instrumentation {\r\n addListener: (listener: { runAfterCreateBrowserContext: (context: PlaywrightContext) => Promise<void> }) => void\r\n}\r\n\r\ninterface ContextRecording {\r\n caption: RecordingCaption\r\n automatic: boolean\r\n sessions: Session[]\r\n}\r\n\r\ninterface RecorderState {\r\n contexts: WeakMap<PlaywrightContext, ContextRecording>\r\n pages: WeakMap<PlaywrightPage, Session>\r\n instrumented: WeakSet<object>\r\n script: string | null\r\n hooked: boolean\r\n}\r\n\r\nfunction state(): RecorderState {\r\n const holder = globalThis as { [STATE_KEY]?: RecorderState }\r\n holder[STATE_KEY] ??= { contexts: new WeakMap(), pages: new WeakMap(), instrumented: new WeakSet(), script: null, hooked: false }\r\n return holder[STATE_KEY]\r\n}\r\n\r\nfunction recordingsDir(): string {\r\n const dir = process.env[RECORDINGS_DIR_ENV]\r\n if (!dir) {\r\n throw new Error(`${RECORDINGS_DIR_ENV} is not set: recordings work only inside a TAMsense run`)\r\n }\r\n return dir\r\n}\r\n\r\nfunction warn(text: string): void {\r\n process.stderr.write(`[tamsense recorder] ${text}\\n`)\r\n}\r\n\r\n// Правило блокировки трекера TAMsense: запрос на адрес, который кончается на /api/events, с ключом проекта в ?key=\r\n// или к серверу TAMsense. Так записи агента не попадают в аналитику и недельный разбор\r\nexport function isTrackerIngest(url: URL, tamsenseOrigins: readonly string[]): boolean {\r\n if (!/\\/api\\/events\\/?$/.test(url.pathname)) {\r\n return false\r\n }\r\n return url.searchParams.has('key') || tamsenseOrigins.includes(url.origin)\r\n}\r\n\r\n// Своим считается только сам компьютер: localhost, *.localhost, адреса loopback и 0.0.0.0. Имена контейнеров браузер\r\n// на компьютере не видит, поэтому имя без точки тоже чужое\r\nexport function isLocalHost(hostname: string): boolean {\r\n const host = hostname.toLowerCase().replace(/^\\[|\\]$/g, '').replace(/\\.$/, '')\r\n return host === 'localhost' || host.endsWith('.localhost') || host === '::1' || host === '0.0.0.0' || /^127(?:\\.\\d{1,3}){3}$/.test(host)\r\n}\r\n\r\n// Запрос, который может что-то изменить у чужого сервиса: метод не только читает, а адрес вне этого компьютера\r\nexport function isExternalWrite(method: string, url: URL): boolean {\r\n return !READ_METHODS.includes(method.toUpperCase()) && !isLocalHost(url.hostname)\r\n}\r\n\r\nexport function trackerOrigins(env: Record<string, string | undefined> = process.env): string[] {\r\n const origins = new Set(DEFAULT_TRACKER_ORIGINS)\r\n try {\r\n if (env[SERVER_ENV]) {\r\n origins.add(new URL(env[SERVER_ENV]).origin)\r\n }\r\n }\r\n catch {}\r\n return [...origins]\r\n}\r\n\r\nexport function checkCaption(caption: Partial<RecordingCaption> | undefined): RecordingCaption {\r\n const title = typeof caption?.title === 'string' ? caption.title.trim() : ''\r\n const purpose = typeof caption?.purpose === 'string' ? caption.purpose.trim() : ''\r\n if (!title || !purpose) {\r\n throw new Error('Give it a title (what it shows, for example \"Design option 1\" or \"Feature testing, attempt 2\") and a purpose (one line on why it was made).')\r\n }\r\n if (title.length > MAX_TITLE_LENGTH || purpose.length > MAX_PURPOSE_LENGTH) {\r\n throw new Error(`Keep the title within ${MAX_TITLE_LENGTH} characters and the purpose within ${MAX_PURPOSE_LENGTH}.`)\r\n }\r\n return { title, purpose }\r\n}\r\n\r\nfunction clip(text: string, max: number): string {\r\n return text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text\r\n}\r\n\r\n// Название записи, которую никто не назвал: из настроек запуска и имени скрипта, который открыл браузер\r\nexport function automaticCaption(env: Record<string, string | undefined> = process.env, script: string | undefined = process.argv[1]): RecordingCaption {\r\n const from = script ? `, opened by ${basename(script)}` : ''\r\n return {\r\n title: clip(env[DEFAULT_TITLE_ENV]?.trim() || 'Browser session', MAX_TITLE_LENGTH),\r\n purpose: clip(`${env[DEFAULT_PURPOSE_ENV]?.trim() || 'Recorded automatically while the agent used a browser'}${from}`, MAX_PURPOSE_LENGTH),\r\n }\r\n}\r\n\r\n// Сборка rrweb без модулей: UMD кладёт её в локальный module.exports, страница своего rrweb не видит и не теряет\r\nfunction rrwebSource(): string {\r\n const entry = createRequire(import.meta.url).resolve('rrweb')\r\n return readFileSync(join(dirname(entry), 'rrweb.umd.min.cjs'), 'utf8')\r\n}\r\n\r\n// Скрипт для каждой страницы: пишет только верхний документ, iframe того же сайта rrweb снимает сам.\r\n// События уходят в программу пачками через exposeBinding, пароли скрыты, как в rrweb по умолчанию\r\nexport function pageScript(rrweb: string): string {\r\n return `(() => {\r\nif (window.top !== window || window.__tamsenseRecording) return;\r\nwindow.__tamsenseRecording = true;\r\nconst rrweb = (() => { const module = { exports: {} }; const exports = module.exports; ${rrweb}\r\n; return module.exports; })();\r\nlet buffer = [];\r\nlet timer = null;\r\nconst send = (extra) => {\r\n clearTimeout(timer);\r\n timer = null;\r\n if (!buffer.length && !extra) return Promise.resolve();\r\n const events = buffer;\r\n buffer = [];\r\n return window.${BINDING}(JSON.stringify({ events, ...extra })).catch(() => {});\r\n};\r\nrrweb.record({\r\n emit(event) {\r\n buffer.push(event);\r\n if (!timer) timer = setTimeout(send, ${FLUSH_MS});\r\n },\r\n maskInputOptions: { password: true },\r\n});\r\nconst crossOrigin = (frame) => { try { return new URL(frame.src, location.href).origin !== location.origin; } catch { return false; } };\r\nconst checkGaps = () => {\r\n const gaps = [];\r\n if (document.querySelector('canvas')) gaps.push('canvas');\r\n if (document.querySelector('video')) gaps.push('video');\r\n if ([...document.querySelectorAll('iframe')].some(crossOrigin)) gaps.push('cross-origin iframe');\r\n if (gaps.length) send({ gaps });\r\n};\r\nwindow.__tamsenseFlush = () => send();\r\nwindow.__tamsenseMark = (label) => { rrweb.record.addCustomEvent('${MARK_TAG}', { label }); return send(); };\r\naddEventListener('pagehide', () => { send(); });\r\naddEventListener('load', () => setTimeout(checkGaps, ${GAP_CHECK_MS}));\r\n})();`\r\n}\r\n\r\nfunction script(): string {\r\n const current = state()\r\n current.script ??= pageScript(rrwebSource())\r\n return current.script\r\n}\r\n\r\nclass Session {\r\n readonly file: string\r\n readonly metaFile: string\r\n readonly meta: SessionMeta\r\n private bytes = 0\r\n\r\n constructor(dir: string, recording: ContextRecording, url: string) {\r\n const id = `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`\r\n this.file = join(dir, `${id}${REPLAY_SUFFIX}`)\r\n this.metaFile = join(dir, `${id}${META_SUFFIX}`)\r\n this.meta = { version: 1, id, ...recording.caption, startedAt: Date.now(), endedAt: null, url, marks: [], video: null, gaps: [], blockedTrackerRequests: 0, blockedExternalWrites: 0, truncated: false, automatic: recording.automatic }\r\n writeFileSync(this.file, '')\r\n this.save()\r\n }\r\n\r\n save(): void {\r\n writeFileSync(this.metaFile, `${JSON.stringify(this.meta, null, 2)}\\n`)\r\n }\r\n\r\n receive(payload: string): void {\r\n let batch: { events?: unknown[], gaps?: string[] }\r\n try {\r\n batch = JSON.parse(payload) as typeof batch\r\n }\r\n catch {\r\n return\r\n }\r\n if (batch.gaps?.length) {\r\n this.meta.gaps = [...new Set([...this.meta.gaps, ...batch.gaps])]\r\n this.save()\r\n }\r\n const page = (batch.events as Array<{ type?: unknown, data?: { href?: unknown } }> | undefined)?.find(event => event.type === RRWEB_META && typeof event.data?.href === 'string' && event.data.href !== 'about:blank')\r\n if (page && this.meta.url === 'about:blank') {\r\n this.meta.url = String(page.data!.href)\r\n this.save()\r\n }\r\n const lines = (batch.events ?? []).map(event => JSON.stringify(event)).join('\\n')\r\n if (!lines || this.meta.truncated) {\r\n return\r\n }\r\n if (this.bytes + lines.length > MAX_SESSION_BYTES) {\r\n this.meta.truncated = true\r\n this.save()\r\n return\r\n }\r\n this.bytes += lines.length + 1\r\n appendFileSync(this.file, `${lines}\\n`)\r\n }\r\n\r\n end(): void {\r\n if (this.meta.endedAt === null) {\r\n this.meta.endedAt = Date.now()\r\n this.save()\r\n }\r\n }\r\n\r\n summary(): RecordingSummary {\r\n const { title, purpose, video, marks, gaps, blockedTrackerRequests, blockedExternalWrites } = this.meta\r\n return { title, purpose, replay: this.file, video, marks: marks.length, gaps, blockedTrackerRequests, blockedExternalWrites }\r\n }\r\n}\r\n\r\nfunction countBlocked(route: PlaywrightRoute, counter: 'blockedTrackerRequests' | 'blockedExternalWrites'): void {\r\n try {\r\n const session = state().pages.get(route.request().frame().page())\r\n if (session) {\r\n session.meta[counter] += 1\r\n session.save()\r\n }\r\n }\r\n catch {}\r\n}\r\n\r\nasync function startSession(recording: ContextRecording, page: PlaywrightPage, injectNow: boolean): Promise<void> {\r\n const current = state()\r\n if (current.pages.has(page)) {\r\n return\r\n }\r\n const session = new Session(recordingsDir(), recording, page.url())\r\n current.pages.set(page, session)\r\n recording.sessions.push(session)\r\n page.on('close', () => session.end())\r\n const video = page.video()\r\n if (video) {\r\n session.meta.video = await video.path().catch(() => null)\r\n session.save()\r\n }\r\n // страница уже была открыта до записи: скрипт для будущих страниц её не коснётся\r\n if (injectNow) {\r\n await page.evaluate(script()).catch(() => {})\r\n }\r\n}\r\n\r\nasync function flush(context: PlaywrightContext): Promise<void> {\r\n await Promise.all(context.pages().filter(page => !page.isClosed()).map(page =>\r\n page.evaluate(() => (window as unknown as { __tamsenseFlush?: () => Promise<void> }).__tamsenseFlush?.(), undefined).catch(() => {}),\r\n ))\r\n}\r\n\r\n// Записывает контекст Playwright; второй вызов для того же контекста только меняет название и назначение записи.\r\n// automatic: контекст записан сам, название придумала программа\r\nexport async function recordContext(context: PlaywrightContext, caption: RecordingCaption, automatic = false): Promise<void> {\r\n const checked = checkCaption(caption)\r\n const current = state()\r\n const known = current.contexts.get(context)\r\n if (known) {\r\n Object.assign(known, { caption: checked, automatic })\r\n for (const session of known.sessions) {\r\n Object.assign(session.meta, checked, { automatic })\r\n session.save()\r\n }\r\n return\r\n }\r\n mkdirSync(recordingsDir(), { recursive: true })\r\n const recording: ContextRecording = { caption: checked, automatic, sessions: [] }\r\n current.contexts.set(context, recording)\r\n const origins = trackerOrigins()\r\n // маршруты Playwright идут от последнего к первому: трекер отвечает раньше, запись наружу отменяется после\r\n await context.route(url => !isLocalHost(url.hostname), async (route) => {\r\n const request = route.request()\r\n if (!isExternalWrite(request.method(), new URL(request.url()))) {\r\n // страница могла закрыться, пока запрос шёл\r\n await route.continue().catch(() => {})\r\n return\r\n }\r\n countBlocked(route, 'blockedExternalWrites')\r\n await route.abort('blockedbyclient').catch(() => {})\r\n })\r\n await context.route(url => isTrackerIngest(url, origins), async (route) => {\r\n countBlocked(route, 'blockedTrackerRequests')\r\n await route.fulfill({ status: 200, contentType: 'application/json', body: '{\"ok\":true}' })\r\n })\r\n await context.exposeBinding(BINDING, ({ page }, payload) => current.pages.get(page)?.receive(payload))\r\n await context.addInitScript({ content: script() })\r\n context.on('page', page => void startSession(recording, page, false))\r\n context.on('close', () => recording.sessions.forEach(session => session.end()))\r\n for (const page of context.pages()) {\r\n await startSession(recording, page, true)\r\n }\r\n const close = context.close.bind(context)\r\n context.close = async (options?: unknown) => {\r\n await flush(context)\r\n return close(options)\r\n }\r\n}\r\n\r\n// Новый записанный контекст: название и назначение обязательны, video: true добавляет видео Playwright\r\nexport async function newRecordedContext(browser: PlaywrightBrowser, options: RecordedContextOptions): Promise<PlaywrightContext> {\r\n const caption = checkCaption(options)\r\n const extra = options.video ? { recordVideo: { dir: join(recordingsDir(), 'video') } } : {}\r\n const context = await browser.newContext({ ...options.context, ...extra })\r\n await recordContext(context, caption)\r\n return context\r\n}\r\n\r\n// Отметка шага: видна на шкале плеера и в списке отметок, клик по ней перематывает запись к этому месту\r\nexport async function mark(page: PlaywrightPage, label: string): Promise<void> {\r\n const text = clip(String(label ?? '').trim(), MAX_LABEL_LENGTH)\r\n const session = state().pages.get(page)\r\n if (!text || !session) {\r\n warn(text ? 'mark() was called for a page that is not recorded' : 'mark() needs a label')\r\n return\r\n }\r\n session.meta.marks.push({ ts: Date.now(), label: text })\r\n session.save()\r\n await page.evaluate(value => (window as unknown as { __tamsenseMark?: (label: string) => Promise<void> }).__tamsenseMark?.(value), text).catch(() => {})\r\n}\r\n\r\n// Закрывает записанный контекст и говорит, что записано; подсказывает видео, если записи не хватит\r\nexport async function closeRecorded(context: PlaywrightContext): Promise<RecordingSummary[]> {\r\n await context.close()\r\n const sessions = state().contexts.get(context)?.sessions ?? []\r\n const summaries = sessions.map(session => session.summary())\r\n for (const summary of summaries) {\r\n if (summary.gaps.length > 0 && !summary.video) {\r\n warn(`\"${summary.title}\" has ${summary.gaps.join(', ')}: the replay may not show it. Record it again with video: true.`)\r\n }\r\n }\r\n return summaries\r\n}\r\n\r\nfunction instrument(exports: unknown): void {\r\n const chromium = (exports as { chromium?: { launch?: unknown, _instrumentation?: Instrumentation } } | null)?.chromium\r\n const instrumentation = (exports as { _instrumentation?: Instrumentation } | null)?._instrumentation ?? chromium?._instrumentation\r\n const current = state()\r\n if (typeof chromium?.launch !== 'function' || typeof instrumentation?.addListener !== 'function' || current.instrumented.has(instrumentation)) {\r\n return\r\n }\r\n current.instrumented.add(instrumentation)\r\n instrumentation.addListener({\r\n runAfterCreateBrowserContext: async (context) => {\r\n try {\r\n if (!current.contexts.has(context)) {\r\n await recordContext(context, automaticCaption(), true)\r\n }\r\n }\r\n catch (error) {\r\n warn(`could not record this browser: ${error instanceof Error ? error.message : String(error)}`)\r\n }\r\n },\r\n })\r\n}\r\n\r\n// Сам цепляется к Playwright, как только его загрузят: агент не может забыть включить запись\r\nexport function installAutoRecording(): void {\r\n const current = state()\r\n if (current.hooked) {\r\n return\r\n }\r\n current.hooked = true\r\n const loader = Module as unknown as { _load: (request: string, parent: unknown, isMain: boolean) => unknown }\r\n const load = loader._load\r\n loader._load = function (this: unknown, ...args: [string, unknown, boolean]) {\r\n const exports = load.apply(this, args)\r\n try {\r\n instrument(exports)\r\n }\r\n catch {}\r\n return exports\r\n }\r\n}\r\n\r\nif (process.env[RECORDINGS_DIR_ENV]) {\r\n installAutoRecording()\r\n}\r\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n"],"mappings":"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","names":["process","script","exports","Module"]}
|
|
1
|
+
{"version":3,"sources":["../src/recorder.ts","../../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.15_tsx@4.21.0_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js"],"sourcesContent":["import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport Module, { createRequire } from 'node:module'\nimport { basename, dirname, join } from 'node:path'\nimport process from 'node:process'\n\n// Запись браузера агента: rrweb пишет каждую страницу в свой файл NDJSON в папке запуска, по желанию ещё и видео.\n// Модуль самодостаточный: программа подключает его ко всем процессам Node агента через NODE_OPTIONS --require,\n// и тогда любой браузер Playwright записывается сам; скрипты агента импортируют его же, чтобы дать записи название и отметки\n\nexport const RECORDINGS_DIR_ENV = 'TAMSENSE_RECORDINGS_DIR'\nexport const SERVER_ENV = 'TAMSENSE_SERVER'\nexport const DEFAULT_TITLE_ENV = 'TAMSENSE_RECORDING_TITLE'\nexport const DEFAULT_PURPOSE_ENV = 'TAMSENSE_RECORDING_PURPOSE'\nexport const MARK_TAG = 'tamsense.mark'\nexport const REPLAY_SUFFIX = '.rrweb.ndjson'\nexport const META_SUFFIX = '.json'\nexport const MAX_TITLE_LENGTH = 60\nexport const MAX_PURPOSE_LENGTH = 160\nexport const MAX_NOTES_LENGTH = 600\n// трекер TAMsense по умолчанию шлёт сюда; адрес сервера из настроек программы добавляется к нему\nexport const DEFAULT_TRACKER_ORIGINS = ['https://my.tamsense.com']\n\n// эти методы только читают; остальные к адресам вне компьютера отменяются\nexport const READ_METHODS = ['GET', 'HEAD', 'OPTIONS']\n\nconst BINDING = '__tamsenseRecord'\nconst STATE_KEY = Symbol.for('tamsense.recorder')\nconst FLUSH_MS = 100\nconst MAX_SESSION_BYTES = 300 * 1024 * 1024\nconst GAP_CHECK_MS = 1500\nconst MAX_LABEL_LENGTH = 120\nconst RRWEB_INCREMENTAL = 3\nconst RRWEB_META = 4\nconst RRWEB_SOURCE_MOUSE_INTERACTION = 2\nconst RRWEB_SOURCE_INPUT = 5\n\nexport interface RecordingCaption {\n title: string\n purpose: string\n}\n\nexport interface RecordedContextOptions extends RecordingCaption {\n // видео в придачу к записи: для canvas, карт, видео на странице, чужих iframe или когда его попросили\n video?: boolean\n // обычные настройки browser.newContext: viewport, storageState и другие\n context?: Record<string, unknown>\n}\n\nexport interface RecordingNotes {\n notes?: string\n}\n\nexport interface SessionMark {\n ts: number\n label: string\n}\n\n// Что знает программа о записи одной страницы; лежит рядом с файлом записи\nexport interface SessionMeta {\n version: 1\n id: string\n title: string\n purpose: string\n startedAt: number\n endedAt: number | null\n url: string\n marks: SessionMark[]\n video: string | null\n gaps: string[]\n blockedTrackerRequests: number\n blockedExternalWrites: number\n truncated: boolean\n // записано само, без названия от агента\n automatic: boolean\n // что агент проверял и чем кончилось, его словами; пусто, пока он не закрыл запись с заметкой\n notes: string\n}\n\nexport interface RecordingSummary {\n title: string\n purpose: string\n notes: string\n replay: string\n video: string | null\n marks: number\n gaps: string[]\n blockedTrackerRequests: number\n blockedExternalWrites: number\n}\n\ninterface PlaywrightVideo {\n path: () => Promise<string>\n}\n\ninterface PlaywrightPage {\n url: () => string\n isClosed: () => boolean\n on: (event: 'close', listener: () => void) => unknown\n evaluate: (fn: string | ((arg: never) => unknown), arg?: unknown) => Promise<unknown>\n video: () => PlaywrightVideo | null\n}\n\ninterface PlaywrightRoute {\n fulfill: (response: { status: number, contentType: string, body: string }) => Promise<void>\n abort: (errorCode?: string) => Promise<void>\n continue: () => Promise<void>\n request: () => { method: () => string, url: () => string, frame: () => { page: () => PlaywrightPage } }\n}\n\nexport interface PlaywrightContext {\n pages: () => PlaywrightPage[]\n on: (event: 'page' | 'close', listener: (page: PlaywrightPage) => void) => unknown\n exposeBinding: (name: string, callback: (source: { page: PlaywrightPage }, payload: string) => unknown) => Promise<void>\n addInitScript: (script: { content: string }) => Promise<void>\n route: (url: (url: URL) => boolean, handler: (route: PlaywrightRoute) => Promise<void>) => Promise<void>\n close: (options?: unknown) => Promise<void>\n}\n\nexport interface PlaywrightBrowser {\n newContext: (options?: Record<string, unknown>) => Promise<PlaywrightContext>\n}\n\ninterface Instrumentation {\n addListener: (listener: { runAfterCreateBrowserContext: (context: PlaywrightContext) => Promise<void> }) => void\n}\n\ninterface ContextRecording {\n caption: RecordingCaption\n automatic: boolean\n sessions: Session[]\n}\n\ninterface RecorderState {\n contexts: WeakMap<PlaywrightContext, ContextRecording>\n pages: WeakMap<PlaywrightPage, Session>\n instrumented: WeakSet<object>\n script: string | null\n hooked: boolean\n}\n\nfunction state(): RecorderState {\n const holder = globalThis as { [STATE_KEY]?: RecorderState }\n holder[STATE_KEY] ??= { contexts: new WeakMap(), pages: new WeakMap(), instrumented: new WeakSet(), script: null, hooked: false }\n return holder[STATE_KEY]\n}\n\nfunction recordingsDir(): string {\n const dir = process.env[RECORDINGS_DIR_ENV]\n if (!dir) {\n throw new Error(`${RECORDINGS_DIR_ENV} is not set: recordings work only inside a TAMsense run`)\n }\n return dir\n}\n\nfunction warn(text: string): void {\n process.stderr.write(`[tamsense recorder] ${text}\\n`)\n}\n\n// Правило блокировки трекера TAMsense: запрос на адрес, который кончается на /api/events, с ключом проекта в ?key=\n// или к серверу TAMsense. Так записи агента не попадают в аналитику и недельный разбор\nexport function isTrackerIngest(url: URL, tamsenseOrigins: readonly string[]): boolean {\n if (!/\\/api\\/events\\/?$/.test(url.pathname)) {\n return false\n }\n return url.searchParams.has('key') || tamsenseOrigins.includes(url.origin)\n}\n\n// Своим считается только сам компьютер: localhost, *.localhost, адреса loopback и 0.0.0.0. Имена контейнеров браузер\n// на компьютере не видит, поэтому имя без точки тоже чужое\nexport function isLocalHost(hostname: string): boolean {\n const host = hostname.toLowerCase().replace(/^\\[|\\]$/g, '').replace(/\\.$/, '')\n return host === 'localhost' || host.endsWith('.localhost') || host === '::1' || host === '0.0.0.0' || /^127(?:\\.\\d{1,3}){3}$/.test(host)\n}\n\n// Запрос, который может что-то изменить у чужого сервиса: метод не только читает, а адрес вне этого компьютера\nexport function isExternalWrite(method: string, url: URL): boolean {\n return !READ_METHODS.includes(method.toUpperCase()) && !isLocalHost(url.hostname)\n}\n\nexport function trackerOrigins(env: Record<string, string | undefined> = process.env): string[] {\n const origins = new Set(DEFAULT_TRACKER_ORIGINS)\n try {\n if (env[SERVER_ENV]) {\n origins.add(new URL(env[SERVER_ENV]).origin)\n }\n }\n catch {}\n return [...origins]\n}\n\nexport function checkCaption(caption: Partial<RecordingCaption> | undefined): RecordingCaption {\n const title = typeof caption?.title === 'string' ? caption.title.trim() : ''\n const purpose = typeof caption?.purpose === 'string' ? caption.purpose.trim() : ''\n if (!title || !purpose) {\n throw new Error('Give it a title (what it shows, for example \"Design option 1\" or \"Feature testing, attempt 2\") and a purpose (one line on why it was made).')\n }\n if (title.length > MAX_TITLE_LENGTH || purpose.length > MAX_PURPOSE_LENGTH) {\n throw new Error(`Keep the title within ${MAX_TITLE_LENGTH} characters and the purpose within ${MAX_PURPOSE_LENGTH}.`)\n }\n return { title, purpose }\n}\n\n// Заметка к записи от инструментов attach_replay и attach_video: длиннее предела не принимается, агент её сократит\nexport function checkNotes(notes: unknown): string {\n const text = typeof notes === 'string' ? notes.trim() : ''\n if (text.length > MAX_NOTES_LENGTH) {\n throw new Error(`Keep the notes within ${MAX_NOTES_LENGTH} characters: what you opened, clicked and expected, and whether it passed.`)\n }\n return text\n}\n\nexport function clip(text: string, max: number): string {\n return text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text\n}\n\n// Название записи, которую никто не назвал: из настроек запуска и имени скрипта, который открыл браузер\nexport function automaticCaption(env: Record<string, string | undefined> = process.env, script: string | undefined = process.argv[1]): RecordingCaption {\n const from = script ? `, opened by ${basename(script)}` : ''\n return {\n title: clip(env[DEFAULT_TITLE_ENV]?.trim() || 'Browser session', MAX_TITLE_LENGTH),\n purpose: clip(`${env[DEFAULT_PURPOSE_ENV]?.trim() || 'Recorded automatically while the agent used a browser'}${from}`, MAX_PURPOSE_LENGTH),\n }\n}\n\n// Сборка rrweb без модулей: UMD кладёт её в локальный module.exports, страница своего rrweb не видит и не теряет\nfunction rrwebSource(): string {\n const entry = createRequire(import.meta.url).resolve('rrweb')\n return readFileSync(join(dirname(entry), 'rrweb.umd.min.cjs'), 'utf8')\n}\n\n// Скрипт для каждой страницы: пишет только верхний документ, iframe того же сайта rrweb снимает сам.\n// События уходят в программу пачками через exposeBinding, пароли скрыты, как в rrweb по умолчанию.\n// Клики и ввод уходят сразу: клик по ссылке уводит страницу раньше, чем сработает таймер, и Playwright выбрасывает\n// пачку, пришедшую от уже закрытой страницы. Клик без координат (element.click() из скрипта) ставится в центр элемента\nexport function pageScript(rrweb: string): string {\n return `(() => {\nif (window.top !== window || window.__tamsenseRecording) return;\nwindow.__tamsenseRecording = true;\nconst rrweb = (() => { const module = { exports: {} }; const exports = module.exports; ${rrweb}\n; return module.exports; })();\nlet buffer = [];\nlet timer = null;\nconst send = (extra) => {\n clearTimeout(timer);\n timer = null;\n if (!buffer.length && !extra) return Promise.resolve();\n const events = buffer;\n buffer = [];\n return window.${BINDING}(JSON.stringify({ events, ...extra })).catch(() => {});\n};\nconst urgent = (event) => event.type === ${RRWEB_INCREMENTAL} && (event.data.source === ${RRWEB_SOURCE_MOUSE_INTERACTION} || event.data.source === ${RRWEB_SOURCE_INPUT});\nconst placeAtTarget = (interaction) => {\n if (interaction.x !== 0 || interaction.y !== 0) return;\n const node = rrweb.record.mirror.getNode(interaction.id);\n if (!node || typeof node.getBoundingClientRect !== 'function') return;\n const box = node.getBoundingClientRect();\n interaction.x = Math.round(box.left + box.width / 2);\n interaction.y = Math.round(box.top + box.height / 2);\n};\nrrweb.record({\n emit(event) {\n buffer.push(event);\n if (urgent(event)) send();\n else if (!timer) timer = setTimeout(send, ${FLUSH_MS});\n },\n hooks: { mouseInteraction: placeAtTarget },\n maskInputOptions: { password: true },\n});\nconst crossOrigin = (frame) => { try { return new URL(frame.src, location.href).origin !== location.origin; } catch { return false; } };\nconst checkGaps = () => {\n const gaps = [];\n if (document.querySelector('canvas')) gaps.push('canvas');\n if (document.querySelector('video')) gaps.push('video');\n if ([...document.querySelectorAll('iframe')].some(crossOrigin)) gaps.push('cross-origin iframe');\n if (gaps.length) send({ gaps });\n};\nwindow.__tamsenseFlush = () => send();\nwindow.__tamsenseMark = (label) => { rrweb.record.addCustomEvent('${MARK_TAG}', { label }); return send(); };\naddEventListener('beforeunload', () => { send(); });\naddEventListener('pagehide', () => { send(); });\naddEventListener('load', () => setTimeout(checkGaps, ${GAP_CHECK_MS}));\n})();`\n}\n\nfunction script(): string {\n const current = state()\n current.script ??= pageScript(rrwebSource())\n return current.script\n}\n\nclass Session {\n readonly file: string\n readonly metaFile: string\n readonly meta: SessionMeta\n private bytes = 0\n\n constructor(dir: string, recording: ContextRecording, url: string) {\n const id = `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`\n this.file = join(dir, `${id}${REPLAY_SUFFIX}`)\n this.metaFile = join(dir, `${id}${META_SUFFIX}`)\n this.meta = { version: 1, id, ...recording.caption, startedAt: Date.now(), endedAt: null, url, marks: [], video: null, gaps: [], blockedTrackerRequests: 0, blockedExternalWrites: 0, truncated: false, automatic: recording.automatic, notes: '' }\n writeFileSync(this.file, '')\n this.save()\n }\n\n save(): void {\n writeFileSync(this.metaFile, `${JSON.stringify(this.meta, null, 2)}\\n`)\n }\n\n receive(payload: string): void {\n let batch: { events?: unknown[], gaps?: string[] }\n try {\n batch = JSON.parse(payload) as typeof batch\n }\n catch {\n return\n }\n if (batch.gaps?.length) {\n this.meta.gaps = [...new Set([...this.meta.gaps, ...batch.gaps])]\n this.save()\n }\n const page = (batch.events as Array<{ type?: unknown, data?: { href?: unknown } }> | undefined)?.find(event => event.type === RRWEB_META && typeof event.data?.href === 'string' && event.data.href !== 'about:blank')\n if (page && this.meta.url === 'about:blank') {\n this.meta.url = String(page.data!.href)\n this.save()\n }\n const lines = (batch.events ?? []).map(event => JSON.stringify(event)).join('\\n')\n if (!lines || this.meta.truncated) {\n return\n }\n if (this.bytes + lines.length > MAX_SESSION_BYTES) {\n this.meta.truncated = true\n this.save()\n return\n }\n this.bytes += lines.length + 1\n appendFileSync(this.file, `${lines}\\n`)\n }\n\n end(): void {\n if (this.meta.endedAt === null) {\n this.meta.endedAt = Date.now()\n this.save()\n }\n }\n\n summary(): RecordingSummary {\n const { title, purpose, notes, video, marks, gaps, blockedTrackerRequests, blockedExternalWrites } = this.meta\n return { title, purpose, notes, replay: this.file, video, marks: marks.length, gaps, blockedTrackerRequests, blockedExternalWrites }\n }\n}\n\nfunction countBlocked(route: PlaywrightRoute, counter: 'blockedTrackerRequests' | 'blockedExternalWrites'): void {\n try {\n const session = state().pages.get(route.request().frame().page())\n if (session) {\n session.meta[counter] += 1\n session.save()\n }\n }\n catch {}\n}\n\nasync function startSession(recording: ContextRecording, page: PlaywrightPage, injectNow: boolean): Promise<void> {\n const current = state()\n if (current.pages.has(page)) {\n return\n }\n const session = new Session(recordingsDir(), recording, page.url())\n current.pages.set(page, session)\n recording.sessions.push(session)\n page.on('close', () => session.end())\n const video = page.video()\n if (video) {\n session.meta.video = await video.path().catch(() => null)\n session.save()\n }\n // страница уже была открыта до записи: скрипт для будущих страниц её не коснётся\n if (injectNow) {\n await page.evaluate(script()).catch(() => {})\n }\n}\n\nasync function flush(context: PlaywrightContext): Promise<void> {\n await Promise.all(context.pages().filter(page => !page.isClosed()).map(page =>\n page.evaluate(() => (window as unknown as { __tamsenseFlush?: () => Promise<void> }).__tamsenseFlush?.(), undefined).catch(() => {}),\n ))\n}\n\n// Записывает контекст Playwright; второй вызов для того же контекста только меняет название и назначение записи.\n// automatic: контекст записан сам, название придумала программа\nexport async function recordContext(context: PlaywrightContext, caption: RecordingCaption, automatic = false): Promise<void> {\n const checked = checkCaption(caption)\n const current = state()\n const known = current.contexts.get(context)\n if (known) {\n Object.assign(known, { caption: checked, automatic })\n for (const session of known.sessions) {\n Object.assign(session.meta, checked, { automatic })\n session.save()\n }\n return\n }\n mkdirSync(recordingsDir(), { recursive: true })\n const recording: ContextRecording = { caption: checked, automatic, sessions: [] }\n current.contexts.set(context, recording)\n const origins = trackerOrigins()\n // маршруты Playwright идут от последнего к первому: трекер отвечает раньше, запись наружу отменяется после\n await context.route(url => !isLocalHost(url.hostname), async (route) => {\n const request = route.request()\n if (!isExternalWrite(request.method(), new URL(request.url()))) {\n // страница могла закрыться, пока запрос шёл\n await route.continue().catch(() => {})\n return\n }\n countBlocked(route, 'blockedExternalWrites')\n await route.abort('blockedbyclient').catch(() => {})\n })\n await context.route(url => isTrackerIngest(url, origins), async (route) => {\n countBlocked(route, 'blockedTrackerRequests')\n await route.fulfill({ status: 200, contentType: 'application/json', body: '{\"ok\":true}' })\n })\n await context.exposeBinding(BINDING, ({ page }, payload) => current.pages.get(page)?.receive(payload))\n await context.addInitScript({ content: script() })\n context.on('page', page => void startSession(recording, page, false))\n context.on('close', () => recording.sessions.forEach(session => session.end()))\n for (const page of context.pages()) {\n await startSession(recording, page, true)\n }\n const close = context.close.bind(context)\n context.close = async (options?: unknown) => {\n await flush(context)\n return close(options)\n }\n}\n\n// Новый записанный контекст: название и назначение обязательны, video: true добавляет видео Playwright\nexport async function newRecordedContext(browser: PlaywrightBrowser, options: RecordedContextOptions): Promise<PlaywrightContext> {\n const caption = checkCaption(options)\n const extra = options.video ? { recordVideo: { dir: join(recordingsDir(), 'video') } } : {}\n const context = await browser.newContext({ ...options.context, ...extra })\n await recordContext(context, caption)\n return context\n}\n\n// Отметка шага: видна на шкале плеера и в списке отметок, клик по ней перематывает запись к этому месту\nexport async function mark(page: PlaywrightPage, label: string): Promise<void> {\n const text = clip(String(label ?? '').trim(), MAX_LABEL_LENGTH)\n const session = state().pages.get(page)\n if (!text || !session) {\n warn(text ? 'mark() was called for a page that is not recorded' : 'mark() needs a label')\n return\n }\n session.meta.marks.push({ ts: Date.now(), label: text })\n session.save()\n await page.evaluate(value => (window as unknown as { __tamsenseMark?: (label: string) => Promise<void> }).__tamsenseMark?.(value), text).catch(() => {})\n}\n\n// Закрывает записанный контекст и говорит, что записано; подсказывает видео, если записи не хватит.\n// notes: что агент проверял в этой записи и чем кончилось, его словами; длинная заметка обрезается, а не теряется\nexport async function closeRecorded(context: PlaywrightContext, options: RecordingNotes = {}): Promise<RecordingSummary[]> {\n const sessions = state().contexts.get(context)?.sessions ?? []\n const notes = clip(String(options.notes ?? '').trim(), MAX_NOTES_LENGTH)\n if (notes) {\n for (const session of sessions) {\n session.meta.notes = notes\n session.save()\n }\n }\n await context.close()\n const summaries = sessions.map(session => session.summary())\n for (const summary of summaries) {\n if (summary.gaps.length > 0 && !summary.video) {\n warn(`\"${summary.title}\" has ${summary.gaps.join(', ')}: the replay may not show it. Record it again with video: true.`)\n }\n }\n return summaries\n}\n\nfunction instrument(exports: unknown): void {\n const chromium = (exports as { chromium?: { launch?: unknown, _instrumentation?: Instrumentation } } | null)?.chromium\n const instrumentation = (exports as { _instrumentation?: Instrumentation } | null)?._instrumentation ?? chromium?._instrumentation\n const current = state()\n if (typeof chromium?.launch !== 'function' || typeof instrumentation?.addListener !== 'function' || current.instrumented.has(instrumentation)) {\n return\n }\n current.instrumented.add(instrumentation)\n instrumentation.addListener({\n runAfterCreateBrowserContext: async (context) => {\n try {\n if (!current.contexts.has(context)) {\n await recordContext(context, automaticCaption(), true)\n }\n }\n catch (error) {\n warn(`could not record this browser: ${error instanceof Error ? error.message : String(error)}`)\n }\n },\n })\n}\n\n// Сам цепляется к Playwright, как только его загрузят: агент не может забыть включить запись\nexport function installAutoRecording(): void {\n const current = state()\n if (current.hooked) {\n return\n }\n current.hooked = true\n const loader = Module as unknown as { _load: (request: string, parent: unknown, isMain: boolean) => unknown }\n const load = loader._load\n loader._load = function (this: unknown, ...args: [string, unknown, boolean]) {\n const exports = load.apply(this, args)\n try {\n instrument(exports)\n }\n catch {}\n return exports\n }\n}\n\nif (process.env[RECORDINGS_DIR_ENV]) {\n installAutoRecording()\n}\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n"],"mappings":"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","names":["process","script","exports","Module"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "tamsense",
|
|
3
|
-
"description": "TAMsense skills and helper agents for unattended work on board steps: clarifying questions and specs, implementing, debugging, tests, verification, code and security review, safe database changes, UI testing, accessibility and interface design.",
|
|
4
|
-
"version": "2.0.0"
|
|
5
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "tamsense",
|
|
3
|
+
"description": "TAMsense skills and helper agents for unattended work on board steps: clarifying questions and specs, implementing, debugging, tests, verification, code and security review, safe database changes, UI testing, accessibility and interface design.",
|
|
4
|
+
"version": "2.0.0"
|
|
5
|
+
}
|
|
@@ -1,123 +1,123 @@
|
|
|
1
|
-
# Third-party notices
|
|
2
|
-
|
|
3
|
-
The skills and agents in this folder are written for TAMsense, and many of them adapt open source work. Every adapted file ends with a "Sources:" or "Adapted from" line naming its upstream and what was changed. This file lists the upstream projects and their licenses.
|
|
4
|
-
|
|
5
|
-
## MIT License
|
|
6
|
-
|
|
7
|
-
The following projects are used under the MIT License. Their copyright notices:
|
|
8
|
-
|
|
9
|
-
| Project | Commit | Copyright |
|
|
10
|
-
|---|---|---|
|
|
11
|
-
| https://github.com/obra/superpowers | bb92a77741419a4ab5f06e711a283343f1ada0c3; b739a8af570e10a95898f2bdf6cec97467fa6c12 (writing-plans, for implementation-plan; writing-plans and the brainstorming builder check, for spec-writing) | Copyright (c) 2025 Jesse Vincent |
|
|
12
|
-
| https://github.com/mattpocock/skills | 5beb35b040e3de97e54fa9e386d7e42d15c295c2 | Copyright (c) 2026 Matt Pocock |
|
|
13
|
-
| https://github.com/addyosmani/agent-skills | aeac41d4d005999432bb7312f7ff06383a91d59d | Copyright (c) 2025 Addy Osmani |
|
|
14
|
-
| https://github.com/addyosmani/web-quality-skills | afa8da942115f2961fdbfa80807ea0b232ff6c00 | Copyright (c) 2026 Addy Osmani |
|
|
15
|
-
| https://github.com/affaan-m/ECC | 30b68924ae9729159f44326aa3134b1d9d67c0e2 | Copyright (c) 2026 Affaan Mustafa |
|
|
16
|
-
| https://github.com/anthropics/claude-code-security-review | 0c6a49f1fa56a1d472575da86a94dbc1edb78eda | Copyright (c) 2025 Anthropic |
|
|
17
|
-
| https://github.com/agamm/claude-code-owasp | 8ac7965caa212b4a850aff8d3df0081ab3fa9eed | Copyright (c) 2026 (no holder named) |
|
|
18
|
-
| https://github.com/VoltAgent/awesome-claude-code-subagents (surgeon-reviewer, from https://github.com/LucianFord/agent-pack) | 408d3c9eee2d0c75c12a3abb5de29e53c2ca6b9e | Copyright (c) 2025 VoltAgent |
|
|
19
|
-
| https://github.com/supabase/agent-skills | c9be0e931b7930f7d02126d04774d904c381e7d7 | Copyright (c) 2026 Supabase |
|
|
20
|
-
| https://github.com/prisma/skills | ba3f9d73200bbb62e49733ab93a961fd138a2c18 | Copyright (c) 2025 Prisma |
|
|
21
|
-
| https://github.com/testdino-hq/playwright-skill | 178d1cb537604279d7b54500c4823a6db42b9de6 | Copyright (c) 2026 TestDino |
|
|
22
|
-
| https://github.com/Leonxlnx/taste-skill | 6010f91a9935f1c167dfc500fb1a475681892e6b | Copyright (c) 2026 Leonxlnx |
|
|
23
|
-
| https://github.com/Dammyjay93/interface-design | 2f9be3206855bcb2d1d0af262c8bae25cba6658d | Copyright (c) 2026 Damola Akinleye |
|
|
24
|
-
| https://github.com/jakubkrehel/make-interfaces-feel-better | 35545ea1512ad59fa463e6b1f95ca9c052981fe6 | Copyright (c) 2026 Jakub Krehel |
|
|
25
|
-
| https://github.com/emilkowalski/skills | 2d228630ba1446365f614a108cb8252f1feda202 | Copyright (c) 2026 Emil Kowalski |
|
|
26
|
-
| https://github.com/vercel-labs/web-interface-guidelines | 434b7f91364665f2f733b310ec54809bf8f37937 | Copyright (c) 2025 Vercel Labs |
|
|
27
|
-
| https://github.com/ibelick/ui-skills | 2a7c96670506e3948c3b43dde66d5e16e47985f1 | Copyright (c) 2026 Julien Thibeaut |
|
|
28
|
-
| https://github.com/github/spec-kit | c06e6708122343d54ac5f7173ccd2c537d51e617 | Copyright GitHub, Inc. |
|
|
29
|
-
| https://github.com/bmad-code-org/BMAD-METHOD (text only; the project's name is a trademark and is not used) | 61e01f82644c4bb533750cfa0d6d61ab61d2cc9b | Copyright (c) 2025 BMad Code, LLC |
|
|
30
|
-
| https://github.com/gotalab/cc-sdd (requirements review gate, for spec-writing) | e2a0c671aef37a482404905b409ab5c27092b25b | Copyright (c) 2025 gotalab |
|
|
31
|
-
| https://github.com/Fission-AI/OpenSpec (spec and design templates, for spec-writing) | c04f30e57817422c1a107ea00a682c1d29a423ca | Copyright (c) 2024 OpenSpec Contributors |
|
|
32
|
-
|
|
33
|
-
The MIT License text, which applies to the portions taken from each project above:
|
|
34
|
-
|
|
35
|
-
```
|
|
36
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
37
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
38
|
-
in the Software without restriction, including without limitation the rights
|
|
39
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
40
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
41
|
-
furnished to do so, subject to the following conditions:
|
|
42
|
-
|
|
43
|
-
The above copyright notice and this permission notice shall be included in all
|
|
44
|
-
copies or substantial portions of the Software.
|
|
45
|
-
|
|
46
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
47
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
48
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
49
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
50
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
51
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
52
|
-
SOFTWARE.
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## Apache License 2.0
|
|
56
|
-
|
|
57
|
-
The following projects are used under the Apache License, Version 2.0. The full license text is in [licenses/Apache-2.0.txt](licenses/Apache-2.0.txt). Files adapted from them were modified, and each says so in its last line.
|
|
58
|
-
|
|
59
|
-
| Project | Commit | Copyright |
|
|
60
|
-
|---|---|---|
|
|
61
|
-
| https://github.com/anthropics/claude-plugins-official (feature-dev, code-simplifier, code-review, pr-review-toolkit, code-modernization, frontend-design) | dfffc8c43f48692db02eac092dbe208293fda224 | Anthropic, PBC |
|
|
62
|
-
| https://github.com/anthropics/skills (webapp-testing) | dbd4588f9e1033efb41dad4bef2f7947c8993d44 | Copyright 2026 Anthropic, PBC |
|
|
63
|
-
| https://github.com/getsentry/skills | f57ada2d0b640406ea7e25b11d636c530e77c58b | Sentry (no copyright line in the upstream LICENSE) |
|
|
64
|
-
| https://github.com/timescale/pg-aiguide | 187be00d317a51210c132010d947f4546ffb5eef | Copyright 2025 Timescale, Inc., d/b/a Tiger Data |
|
|
65
|
-
| https://github.com/pbakaus/impeccable | dad8dbb3aac21ad1fa236c110315ef4abece23e8 | Copyright 2025 Paul Bakaus |
|
|
66
|
-
| https://github.com/microsoft/playwright (test agents, ideas only) | | Microsoft Corporation |
|
|
67
|
-
| https://github.com/openai/codex (Plan Mode template and request_user_input tool) | 430a16799d90e296fb27b1e94f206023902b7e56 | Copyright 2025 OpenAI |
|
|
68
|
-
| https://github.com/humanlayer/humanlayer (create_plan command) | 99abe673498cf8bdcd5f989aebe9406a27185b3b | Copyright (c) 2024, humanlayer Authors |
|
|
69
|
-
|
|
70
|
-
### NOTICE of timescale/pg-aiguide
|
|
71
|
-
|
|
72
|
-
```
|
|
73
|
-
Copyright 2025 Timescale, Inc., d/b/a Tiger Data
|
|
74
|
-
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
75
|
-
this file except in compliance with the License. You may obtain a copy of the
|
|
76
|
-
License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by
|
|
77
|
-
applicable law or agreed to in writing, software distributed under the License
|
|
78
|
-
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
79
|
-
KIND, either express or implied. See the License for the specific language
|
|
80
|
-
governing permissions and limitations under the License.
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### NOTICE of pbakaus/impeccable
|
|
84
|
-
|
|
85
|
-
```
|
|
86
|
-
Third-Party Notices
|
|
87
|
-
|
|
88
|
-
This project includes content derived from third-party work, used under the
|
|
89
|
-
terms of its original license.
|
|
90
|
-
|
|
91
|
-
Platform Design Skills
|
|
92
|
-
|
|
93
|
-
The skill/reference/ios.md and skill/reference/android.md platform reference
|
|
94
|
-
files are distilled from ehmo's platform-design-skills (Apple Human Interface
|
|
95
|
-
Guidelines and Material Design 3 rules), rewritten in Impeccable's voice.
|
|
96
|
-
|
|
97
|
-
Original work: https://github.com/ehmo/platform-design-skills
|
|
98
|
-
Original license: MIT
|
|
99
|
-
Author: ehmo
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
The files named in that notice were not used here.
|
|
103
|
-
|
|
104
|
-
### NOTICE of openai/codex
|
|
105
|
-
|
|
106
|
-
```
|
|
107
|
-
OpenAI Codex
|
|
108
|
-
Copyright 2025 OpenAI
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
The rest of that notice is about code derived from Ratatui, which was not used here.
|
|
112
|
-
|
|
113
|
-
## MIT No Attribution
|
|
114
|
-
|
|
115
|
-
`skills/task-interview` adapts the corner checklist and the requirements analysis of https://github.com/awslabs/aidlc-workflows (commit 95d2b44d311365d0c3d8aa3d774e36082f9ac4af), Copyright Amazon.com, Inc. or its affiliates, under the MIT No Attribution license. That license asks for no attribution; it is named here so the source is easy to find.
|
|
116
|
-
|
|
117
|
-
## Creative Commons Attribution-ShareAlike 4.0
|
|
118
|
-
|
|
119
|
-
`skills/differential-security-review` and `skills/false-positive-check` are adapted from https://github.com/trailofbits/skills (commit 5c99c03fb8a9ea568432ecbcba5d86e9f4ac2bf0), made by Trail of Bits (differential-review by Omar Inuwa, fp-check by Maciej Domanski), under CC BY-SA 4.0. Both folders, including our changes, stay under CC BY-SA 4.0; the license text is in the `LICENSE` file inside each folder, and each file names what was changed.
|
|
120
|
-
|
|
121
|
-
## Ideas only
|
|
122
|
-
|
|
123
|
-
Ideas from these sources are restated in our own words, and no text was copied: the Karpathy coding guidelines (https://github.com/multica-ai/andrej-karpathy-skills) and the code-review command of https://github.com/anthropics/claude-code.
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
The skills and agents in this folder are written for TAMsense, and many of them adapt open source work. Every adapted file ends with a "Sources:" or "Adapted from" line naming its upstream and what was changed. This file lists the upstream projects and their licenses.
|
|
4
|
+
|
|
5
|
+
## MIT License
|
|
6
|
+
|
|
7
|
+
The following projects are used under the MIT License. Their copyright notices:
|
|
8
|
+
|
|
9
|
+
| Project | Commit | Copyright |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| https://github.com/obra/superpowers | bb92a77741419a4ab5f06e711a283343f1ada0c3; b739a8af570e10a95898f2bdf6cec97467fa6c12 (writing-plans, for implementation-plan; writing-plans and the brainstorming builder check, for spec-writing) | Copyright (c) 2025 Jesse Vincent |
|
|
12
|
+
| https://github.com/mattpocock/skills | 5beb35b040e3de97e54fa9e386d7e42d15c295c2 | Copyright (c) 2026 Matt Pocock |
|
|
13
|
+
| https://github.com/addyosmani/agent-skills | aeac41d4d005999432bb7312f7ff06383a91d59d | Copyright (c) 2025 Addy Osmani |
|
|
14
|
+
| https://github.com/addyosmani/web-quality-skills | afa8da942115f2961fdbfa80807ea0b232ff6c00 | Copyright (c) 2026 Addy Osmani |
|
|
15
|
+
| https://github.com/affaan-m/ECC | 30b68924ae9729159f44326aa3134b1d9d67c0e2 | Copyright (c) 2026 Affaan Mustafa |
|
|
16
|
+
| https://github.com/anthropics/claude-code-security-review | 0c6a49f1fa56a1d472575da86a94dbc1edb78eda | Copyright (c) 2025 Anthropic |
|
|
17
|
+
| https://github.com/agamm/claude-code-owasp | 8ac7965caa212b4a850aff8d3df0081ab3fa9eed | Copyright (c) 2026 (no holder named) |
|
|
18
|
+
| https://github.com/VoltAgent/awesome-claude-code-subagents (surgeon-reviewer, from https://github.com/LucianFord/agent-pack) | 408d3c9eee2d0c75c12a3abb5de29e53c2ca6b9e | Copyright (c) 2025 VoltAgent |
|
|
19
|
+
| https://github.com/supabase/agent-skills | c9be0e931b7930f7d02126d04774d904c381e7d7 | Copyright (c) 2026 Supabase |
|
|
20
|
+
| https://github.com/prisma/skills | ba3f9d73200bbb62e49733ab93a961fd138a2c18 | Copyright (c) 2025 Prisma |
|
|
21
|
+
| https://github.com/testdino-hq/playwright-skill | 178d1cb537604279d7b54500c4823a6db42b9de6 | Copyright (c) 2026 TestDino |
|
|
22
|
+
| https://github.com/Leonxlnx/taste-skill | 6010f91a9935f1c167dfc500fb1a475681892e6b | Copyright (c) 2026 Leonxlnx |
|
|
23
|
+
| https://github.com/Dammyjay93/interface-design | 2f9be3206855bcb2d1d0af262c8bae25cba6658d | Copyright (c) 2026 Damola Akinleye |
|
|
24
|
+
| https://github.com/jakubkrehel/make-interfaces-feel-better | 35545ea1512ad59fa463e6b1f95ca9c052981fe6 | Copyright (c) 2026 Jakub Krehel |
|
|
25
|
+
| https://github.com/emilkowalski/skills | 2d228630ba1446365f614a108cb8252f1feda202 | Copyright (c) 2026 Emil Kowalski |
|
|
26
|
+
| https://github.com/vercel-labs/web-interface-guidelines | 434b7f91364665f2f733b310ec54809bf8f37937 | Copyright (c) 2025 Vercel Labs |
|
|
27
|
+
| https://github.com/ibelick/ui-skills | 2a7c96670506e3948c3b43dde66d5e16e47985f1 | Copyright (c) 2026 Julien Thibeaut |
|
|
28
|
+
| https://github.com/github/spec-kit | c06e6708122343d54ac5f7173ccd2c537d51e617 | Copyright GitHub, Inc. |
|
|
29
|
+
| https://github.com/bmad-code-org/BMAD-METHOD (text only; the project's name is a trademark and is not used) | 61e01f82644c4bb533750cfa0d6d61ab61d2cc9b | Copyright (c) 2025 BMad Code, LLC |
|
|
30
|
+
| https://github.com/gotalab/cc-sdd (requirements review gate, for spec-writing) | e2a0c671aef37a482404905b409ab5c27092b25b | Copyright (c) 2025 gotalab |
|
|
31
|
+
| https://github.com/Fission-AI/OpenSpec (spec and design templates, for spec-writing) | c04f30e57817422c1a107ea00a682c1d29a423ca | Copyright (c) 2024 OpenSpec Contributors |
|
|
32
|
+
|
|
33
|
+
The MIT License text, which applies to the portions taken from each project above:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
37
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
38
|
+
in the Software without restriction, including without limitation the rights
|
|
39
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
40
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
41
|
+
furnished to do so, subject to the following conditions:
|
|
42
|
+
|
|
43
|
+
The above copyright notice and this permission notice shall be included in all
|
|
44
|
+
copies or substantial portions of the Software.
|
|
45
|
+
|
|
46
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
47
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
48
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
49
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
50
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
51
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
52
|
+
SOFTWARE.
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Apache License 2.0
|
|
56
|
+
|
|
57
|
+
The following projects are used under the Apache License, Version 2.0. The full license text is in [licenses/Apache-2.0.txt](licenses/Apache-2.0.txt). Files adapted from them were modified, and each says so in its last line.
|
|
58
|
+
|
|
59
|
+
| Project | Commit | Copyright |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| https://github.com/anthropics/claude-plugins-official (feature-dev, code-simplifier, code-review, pr-review-toolkit, code-modernization, frontend-design) | dfffc8c43f48692db02eac092dbe208293fda224 | Anthropic, PBC |
|
|
62
|
+
| https://github.com/anthropics/skills (webapp-testing) | dbd4588f9e1033efb41dad4bef2f7947c8993d44 | Copyright 2026 Anthropic, PBC |
|
|
63
|
+
| https://github.com/getsentry/skills | f57ada2d0b640406ea7e25b11d636c530e77c58b | Sentry (no copyright line in the upstream LICENSE) |
|
|
64
|
+
| https://github.com/timescale/pg-aiguide | 187be00d317a51210c132010d947f4546ffb5eef | Copyright 2025 Timescale, Inc., d/b/a Tiger Data |
|
|
65
|
+
| https://github.com/pbakaus/impeccable | dad8dbb3aac21ad1fa236c110315ef4abece23e8 | Copyright 2025 Paul Bakaus |
|
|
66
|
+
| https://github.com/microsoft/playwright (test agents, ideas only) | | Microsoft Corporation |
|
|
67
|
+
| https://github.com/openai/codex (Plan Mode template and request_user_input tool) | 430a16799d90e296fb27b1e94f206023902b7e56 | Copyright 2025 OpenAI |
|
|
68
|
+
| https://github.com/humanlayer/humanlayer (create_plan command) | 99abe673498cf8bdcd5f989aebe9406a27185b3b | Copyright (c) 2024, humanlayer Authors |
|
|
69
|
+
|
|
70
|
+
### NOTICE of timescale/pg-aiguide
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
Copyright 2025 Timescale, Inc., d/b/a Tiger Data
|
|
74
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
75
|
+
this file except in compliance with the License. You may obtain a copy of the
|
|
76
|
+
License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by
|
|
77
|
+
applicable law or agreed to in writing, software distributed under the License
|
|
78
|
+
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
79
|
+
KIND, either express or implied. See the License for the specific language
|
|
80
|
+
governing permissions and limitations under the License.
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### NOTICE of pbakaus/impeccable
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
Third-Party Notices
|
|
87
|
+
|
|
88
|
+
This project includes content derived from third-party work, used under the
|
|
89
|
+
terms of its original license.
|
|
90
|
+
|
|
91
|
+
Platform Design Skills
|
|
92
|
+
|
|
93
|
+
The skill/reference/ios.md and skill/reference/android.md platform reference
|
|
94
|
+
files are distilled from ehmo's platform-design-skills (Apple Human Interface
|
|
95
|
+
Guidelines and Material Design 3 rules), rewritten in Impeccable's voice.
|
|
96
|
+
|
|
97
|
+
Original work: https://github.com/ehmo/platform-design-skills
|
|
98
|
+
Original license: MIT
|
|
99
|
+
Author: ehmo
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The files named in that notice were not used here.
|
|
103
|
+
|
|
104
|
+
### NOTICE of openai/codex
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
OpenAI Codex
|
|
108
|
+
Copyright 2025 OpenAI
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The rest of that notice is about code derived from Ratatui, which was not used here.
|
|
112
|
+
|
|
113
|
+
## MIT No Attribution
|
|
114
|
+
|
|
115
|
+
`skills/task-interview` adapts the corner checklist and the requirements analysis of https://github.com/awslabs/aidlc-workflows (commit 95d2b44d311365d0c3d8aa3d774e36082f9ac4af), Copyright Amazon.com, Inc. or its affiliates, under the MIT No Attribution license. That license asks for no attribution; it is named here so the source is easy to find.
|
|
116
|
+
|
|
117
|
+
## Creative Commons Attribution-ShareAlike 4.0
|
|
118
|
+
|
|
119
|
+
`skills/differential-security-review` and `skills/false-positive-check` are adapted from https://github.com/trailofbits/skills (commit 5c99c03fb8a9ea568432ecbcba5d86e9f4ac2bf0), made by Trail of Bits (differential-review by Omar Inuwa, fp-check by Maciej Domanski), under CC BY-SA 4.0. Both folders, including our changes, stay under CC BY-SA 4.0; the license text is in the `LICENSE` file inside each folder, and each file names what was changed.
|
|
120
|
+
|
|
121
|
+
## Ideas only
|
|
122
|
+
|
|
123
|
+
Ideas from these sources are restated in our own words, and no text was copied: the Karpathy coding guidelines (https://github.com/multica-ai/andrej-karpathy-skills) and the code-review command of https://github.com/anthropics/claude-code.
|