{"record":{"id":"442fb2d2eeff386a","repo":"janhq/jan","slug":"jan-data-folder-is-unavailable-skillstore","errorCode":null,"errorMessage":"Jan data folder is unavailable","messagePattern":"Jan data folder is unavailable","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"web-app/src/lib/skillStore.ts","lineNumber":45,"sourceCode":" * rather than the plugin's. They are not redundant: `agent_skill_write` also runs\n * `ensure_project`, which scaffolds `agent.toml`. That format is\n * owned by `core::agent::project`, and the plugin must not learn to write it --\n * owning no config format is what let the toolset be extracted at all. Routing\n * project writes through guest-js would silently drop the scaffold.\n */\nexport type SkillScope =\n  | { kind: 'store' }\n  | { kind: 'project'; folder: string }\n\nexport const storeScope: SkillScope = { kind: 'store' }\nexport const projectScope = (folder: string): SkillScope => ({\n  kind: 'project',\n  folder,\n})\n\nconst dataFolder = async (): Promise<string> => {\n  const folder = await getServiceHub().app().getJanDataFolder()\n  if (!folder) throw new Error('Jan data folder is unavailable')\n  return folder\n}\n\nexport async function listSkills(scope: SkillScope): Promise<SkillMeta[]> {\n  if (scope.kind === 'project') {\n    return await invoke<SkillMeta[]>('agent_skill_list', {\n      project: scope.folder,\n    })\n  }\n  return await skillList(await dataFolder())\n}\n\n/** Raw SKILL.md text, frontmatter included. */\nexport async function readSkill(\n  scope: SkillScope,\n  name: string\n): Promise<string> {\n  if (scope.kind === 'project') {","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/janhq/jan/blob/7205d770c1e097c3daf35a911176410e93bc5564/web-app/src/lib/skillStore.ts#L27-L63","documentation":"dataFolder resolves the Jan data directory via the service hub; if getJanDataFolder() returns a falsy value it throws. All skill store operations (list/read/write/delete/invoke) depend on this path, so they fail fast when the app data folder cannot be determined.","triggerScenarios":"Any of listSkills/readSkill/writeSkill/deleteSkill/invokeSkill called before the app service hub is ready or when getJanDataFolder() returns null/undefined/empty.","commonSituations":"Calling skill APIs too early during app startup, backend/app service not initialized, or corrupted app state where the data folder was never set.","solutions":["Ensure the app/backend services are fully initialized before using the skill store","Call skill APIs only after the app is ready (await app readiness event)","Check app logs for service hub initialization failures","Verify the Jan data folder setting is present in app configuration"],"exampleFix":"// before\nconst skills = await listSkills({ kind: 'project', folder })\n// after\nconst appService = getServiceHub().app()\nif (!appService) throw new Error('App service not initialized')\nconst skills = await listSkills({ kind: 'project', folder })","handlingStrategy":"try-catch","validationCode":"const folder = await getServiceHub().app().getJanDataFolder()\nconst ready = typeof folder === 'string' && folder.length > 0","typeGuard":"function hasDataFolder(f: unknown): f is string { return typeof f === 'string' && f.length > 0 }","tryCatchPattern":"try { const skills = await listSkills(scope) } catch (e) { if (String(e).includes('data folder is unavailable')) await waitForAppReady(); else throw e }","preventionTips":["Initialize skill operations only after app-ready events","Assert data folder presence during startup","Avoid calling skill APIs from background/preload scripts before hydration"],"tags":["filesystem","initialization"],"backgroundTag":"missing-config-value","analyzedSha":"7205d770c1e097c3daf35a911176410e93bc5564","analyzedAt":"2026-09-17T14:27:30.100Z","contentChangedAt":"2026-09-17T14:27:30.100Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}