@hasna/knowledge 0.2.2 → 0.2.4

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.
@@ -0,0 +1,92 @@
1
+ export type SourceRefKind = 'open-files' | 's3' | 'file' | 'web';
2
+
3
+ export interface BaseSourceRef {
4
+ kind: SourceRefKind;
5
+ uri: string;
6
+ }
7
+
8
+ export interface OpenFilesSourceRef extends BaseSourceRef {
9
+ kind: 'open-files';
10
+ entity: 'file' | 'source';
11
+ id: string;
12
+ revision_id?: string;
13
+ path?: string;
14
+ }
15
+
16
+ export interface S3SourceRef extends BaseSourceRef {
17
+ kind: 's3';
18
+ bucket: string;
19
+ key: string;
20
+ }
21
+
22
+ export interface FileSourceRef extends BaseSourceRef {
23
+ kind: 'file';
24
+ path: string;
25
+ }
26
+
27
+ export interface WebSourceRef extends BaseSourceRef {
28
+ kind: 'web';
29
+ url: string;
30
+ }
31
+
32
+ export type SourceRef = OpenFilesSourceRef | S3SourceRef | FileSourceRef | WebSourceRef;
33
+
34
+ function assertNonEmpty(value: string | undefined, message: string): string {
35
+ if (!value) throw new Error(message);
36
+ return value;
37
+ }
38
+
39
+ function parseOpenFilesRef(uri: string): OpenFilesSourceRef {
40
+ const withoutScheme = uri.slice('open-files://'.length);
41
+ const parts = withoutScheme.split('/').filter(Boolean);
42
+ const entity = parts[0];
43
+ if (entity !== 'file' && entity !== 'source') {
44
+ throw new Error("Invalid open-files ref. Expected open-files://file/<id>, open-files://file/<id>/revision/<revision_id>, or open-files://source/<id>/path/<path>.");
45
+ }
46
+ const id = assertNonEmpty(parts[1], 'Invalid open-files ref. Missing id.');
47
+ if (entity === 'file') {
48
+ if (parts.length === 2) return { kind: 'open-files', uri, entity, id };
49
+ if (parts[2] === 'revision' && parts[3] && parts.length === 4) {
50
+ return { kind: 'open-files', uri, entity, id, revision_id: decodeURIComponent(parts[3]) };
51
+ }
52
+ throw new Error('Invalid open-files file ref. Expected open-files://file/<id>/revision/<revision_id>.');
53
+ }
54
+ const pathIndex = parts.indexOf('path');
55
+ const path = pathIndex >= 0 ? decodeURIComponent(parts.slice(pathIndex + 1).join('/')) : undefined;
56
+ return { kind: 'open-files', uri, entity, id, path };
57
+ }
58
+
59
+ function parseS3Ref(uri: string): S3SourceRef {
60
+ const parsed = new URL(uri);
61
+ const bucket = assertNonEmpty(parsed.hostname, 'Invalid s3 ref. Missing bucket.');
62
+ const key = decodeURIComponent(parsed.pathname.replace(/^\/+/, ''));
63
+ if (!key) throw new Error('Invalid s3 ref. Missing object key.');
64
+ return { kind: 's3', uri, bucket, key };
65
+ }
66
+
67
+ function parseFileRef(uri: string): FileSourceRef {
68
+ const parsed = new URL(uri);
69
+ return { kind: 'file', uri, path: decodeURIComponent(parsed.pathname) };
70
+ }
71
+
72
+ function parseWebRef(uri: string): WebSourceRef {
73
+ const parsed = new URL(uri);
74
+ return { kind: 'web', uri, url: parsed.toString() };
75
+ }
76
+
77
+ export function parseSourceRef(uri: string): SourceRef {
78
+ if (uri.startsWith('open-files://')) return parseOpenFilesRef(uri);
79
+ if (uri.startsWith('s3://')) return parseS3Ref(uri);
80
+ if (uri.startsWith('file://')) return parseFileRef(uri);
81
+ if (uri.startsWith('https://') || uri.startsWith('http://')) return parseWebRef(uri);
82
+ throw new Error(`Unsupported source ref scheme: ${uri}`);
83
+ }
84
+
85
+ export function isSupportedSourceRef(uri: string): boolean {
86
+ try {
87
+ parseSourceRef(uri);
88
+ return true;
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
package/src/store.ts CHANGED
@@ -3,17 +3,19 @@
3
3
  * Copyright 2026 Hasna Inc.
4
4
  * Licensed under the Apache License, Version 2.0
5
5
  */
6
- import { mkdirSync, readFileSync, writeFileSync, existsSync, renameSync, unlinkSync } from 'node:fs';
7
- import { dirname } from 'node:path';
8
- import { homedir } from 'node:os';
6
+ import { readFileSync, writeFileSync, existsSync, renameSync, unlinkSync } from 'node:fs';
9
7
  import { randomUUID } from 'node:crypto';
8
+ import { ensureParentDir, globalKnowledgeHome, legacyGlobalStorePath, workspaceForHome } from './workspace';
10
9
 
11
10
  export interface KnowledgeItem {
12
11
  id: string;
12
+ short_id?: string | null;
13
13
  title: string;
14
14
  content: string;
15
15
  url: string | null;
16
16
  tags: string[];
17
+ metadata?: Record<string, unknown>;
18
+ archived?: boolean;
17
19
  created_at: string;
18
20
  updated_at: string;
19
21
  }
@@ -23,13 +25,17 @@ export interface Store {
23
25
  }
24
26
 
25
27
  export function defaultStorePath(): string {
26
- return `${homedir()}/.open-knowledge/db.json`;
28
+ return workspaceForHome(globalKnowledgeHome()).jsonStorePath;
27
29
  }
28
30
 
29
31
  export function ensureStore(path: string): void {
30
32
  if (!existsSync(path)) {
31
- mkdirSync(dirname(path), { recursive: true });
32
- writeFileSync(path, JSON.stringify({ items: [] }, null, 2));
33
+ ensureParentDir(path);
34
+ if (path === defaultStorePath() && existsSync(legacyGlobalStorePath())) {
35
+ writeFileSync(path, readFileSync(legacyGlobalStorePath(), 'utf8'));
36
+ } else {
37
+ writeFileSync(path, JSON.stringify({ items: [] }, null, 2));
38
+ }
33
39
  }
34
40
  }
35
41
 
@@ -101,3 +107,7 @@ export function withLock<T>(path: string, fn: () => T): T {
101
107
  export function makeId(): string {
102
108
  return `k_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
103
109
  }
110
+
111
+ export function makeShortId(id: string): string {
112
+ return id.replace(/^k_/, '').slice(0, 12);
113
+ }
@@ -0,0 +1,104 @@
1
+ import type { ArtifactStore } from './artifact-store';
2
+
3
+ export interface WikiLayoutInitResult {
4
+ schema_key: string;
5
+ root_index_key: string;
6
+ wiki_readme_key: string;
7
+ log_key: string;
8
+ written: string[];
9
+ }
10
+
11
+ function todayParts(now: Date): { year: string; month: string; day: string } {
12
+ const year = String(now.getUTCFullYear());
13
+ const month = String(now.getUTCMonth() + 1).padStart(2, '0');
14
+ const day = String(now.getUTCDate()).padStart(2, '0');
15
+ return { year, month, day };
16
+ }
17
+
18
+ export function agentSchemaTemplate(): string {
19
+ return `# Knowledge Agent Schema v1
20
+
21
+ ## Source Rules
22
+
23
+ - Treat open-files source references as the preferred source of truth.
24
+ - Do not copy raw source files into open-knowledge.
25
+ - Cite every durable fact with a source URI, revision/hash when available, and optional span.
26
+ - Mark uncertainty explicitly when sources disagree or are incomplete.
27
+
28
+ ## Wiki Rules
29
+
30
+ - Write generated knowledge as Markdown pages under wiki/.
31
+ - Keep root indexes small; use topic, team, project, and machine-readable shards for scale.
32
+ - Preserve backlinks between related pages and decisions.
33
+ - Prefer updating existing pages over creating near-duplicates.
34
+
35
+ ## Query Rules
36
+
37
+ - Search wiki pages first, then source chunks, then deeper read-only source refs.
38
+ - Use web search only when requested or when current external context is required.
39
+ - File useful answers back into the wiki only after approval or approved auto-write mode.
40
+
41
+ ## Lint Rules
42
+
43
+ - Flag stale pages, missing citations, contradictions, orphan pages, duplicate pages, and unresolved source refs.
44
+ `;
45
+ }
46
+
47
+ export function rootIndexTemplate(): string {
48
+ return `# Knowledge Index
49
+
50
+ This is a compact orientation index for agents. It is not the full search index.
51
+
52
+ ## Shards
53
+
54
+ - wiki/
55
+ - indexes/
56
+ - schemas/
57
+ - logs/
58
+
59
+ ## Source Ownership
60
+
61
+ Raw source files are resolved through open-files. This app stores source refs,
62
+ citations, chunks, generated wiki artifacts, indexes, and run records.
63
+ `;
64
+ }
65
+
66
+ export function wikiReadmeTemplate(): string {
67
+ return `# Wiki
68
+
69
+ Generated durable knowledge pages live here.
70
+
71
+ Pages should be concise, cited, and organized for both humans and agents.
72
+ `;
73
+ }
74
+
75
+ export async function initializeWikiLayout(store: ArtifactStore, now = new Date()): Promise<WikiLayoutInitResult> {
76
+ const { year, month, day } = todayParts(now);
77
+ const schemaKey = 'schemas/v1.md';
78
+ const rootIndexKey = 'indexes/root.md';
79
+ const wikiReadmeKey = 'wiki/README.md';
80
+ const logKey = `logs/${year}/${month}/${day}.jsonl`;
81
+ const event = {
82
+ ts: now.toISOString(),
83
+ event: 'wiki_layout_initialized',
84
+ schema_key: schemaKey,
85
+ root_index_key: rootIndexKey,
86
+ wiki_readme_key: wikiReadmeKey,
87
+ };
88
+
89
+ const writes = [
90
+ store.put({ key: schemaKey, body: agentSchemaTemplate(), content_type: 'text/markdown' }),
91
+ store.put({ key: rootIndexKey, body: rootIndexTemplate(), content_type: 'text/markdown' }),
92
+ store.put({ key: wikiReadmeKey, body: wikiReadmeTemplate(), content_type: 'text/markdown' }),
93
+ store.put({ key: logKey, body: `${JSON.stringify(event)}\n`, content_type: 'application/x-ndjson' }),
94
+ ];
95
+
96
+ await Promise.all(writes);
97
+ return {
98
+ schema_key: schemaKey,
99
+ root_index_key: rootIndexKey,
100
+ wiki_readme_key: wikiReadmeKey,
101
+ log_key: logKey,
102
+ written: [schemaKey, rootIndexKey, wikiReadmeKey, logKey],
103
+ };
104
+ }
@@ -0,0 +1,123 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join, resolve } from 'node:path';
4
+
5
+ export const HASNA_KNOWLEDGE_APP_PATH = join('.hasna', 'apps', 'knowledge');
6
+
7
+ export interface KnowledgeWorkspace {
8
+ home: string;
9
+ configPath: string;
10
+ jsonStorePath: string;
11
+ knowledgeDbPath: string;
12
+ artifactsDir: string;
13
+ cacheDir: string;
14
+ exportsDir: string;
15
+ indexesDir: string;
16
+ logsDir: string;
17
+ runsDir: string;
18
+ schemasDir: string;
19
+ wikiDir: string;
20
+ }
21
+
22
+ export interface KnowledgeConfig {
23
+ version: 1;
24
+ mode: 'local' | 'hosted';
25
+ storage: {
26
+ type: 'local' | 's3';
27
+ artifacts_root: string;
28
+ s3?: {
29
+ bucket: string;
30
+ prefix?: string;
31
+ region?: string;
32
+ profile?: string;
33
+ max_attempts?: number;
34
+ server_side_encryption?: 'AES256' | 'aws:kms';
35
+ kms_key_id?: string;
36
+ };
37
+ };
38
+ sources: {
39
+ preferred_ref: 'open-files';
40
+ allowed_schemes: string[];
41
+ };
42
+ }
43
+
44
+ export function legacyGlobalStorePath(): string {
45
+ return join(homedir(), '.open-knowledge', 'db.json');
46
+ }
47
+
48
+ export function globalKnowledgeHome(): string {
49
+ return join(homedir(), '.hasna', 'apps', 'knowledge');
50
+ }
51
+
52
+ export function projectKnowledgeHome(cwd = process.cwd()): string {
53
+ return resolve(cwd, HASNA_KNOWLEDGE_APP_PATH);
54
+ }
55
+
56
+ export function workspaceForHome(home: string): KnowledgeWorkspace {
57
+ return {
58
+ home,
59
+ configPath: join(home, 'config.json'),
60
+ jsonStorePath: join(home, 'db.json'),
61
+ knowledgeDbPath: join(home, 'knowledge.db'),
62
+ artifactsDir: join(home, 'artifacts'),
63
+ cacheDir: join(home, 'cache'),
64
+ exportsDir: join(home, 'exports'),
65
+ indexesDir: join(home, 'indexes'),
66
+ logsDir: join(home, 'logs'),
67
+ runsDir: join(home, 'runs'),
68
+ schemasDir: join(home, 'schemas'),
69
+ wikiDir: join(home, 'wiki'),
70
+ };
71
+ }
72
+
73
+ export function defaultKnowledgeConfig(): KnowledgeConfig {
74
+ return {
75
+ version: 1,
76
+ mode: 'local',
77
+ storage: {
78
+ type: 'local',
79
+ artifacts_root: 'artifacts',
80
+ },
81
+ sources: {
82
+ preferred_ref: 'open-files',
83
+ allowed_schemes: ['open-files', 's3', 'file', 'https', 'http'],
84
+ },
85
+ };
86
+ }
87
+
88
+ export function ensureKnowledgeWorkspace(home: string): KnowledgeWorkspace {
89
+ const workspace = workspaceForHome(home);
90
+ mkdirSync(workspace.home, { recursive: true });
91
+ for (const dir of [
92
+ workspace.artifactsDir,
93
+ workspace.cacheDir,
94
+ workspace.exportsDir,
95
+ workspace.indexesDir,
96
+ workspace.logsDir,
97
+ workspace.runsDir,
98
+ workspace.schemasDir,
99
+ workspace.wikiDir,
100
+ ]) {
101
+ mkdirSync(dir, { recursive: true });
102
+ }
103
+ if (!existsSync(workspace.configPath)) {
104
+ writeFileSync(workspace.configPath, `${JSON.stringify(defaultKnowledgeConfig(), null, 2)}\n`);
105
+ }
106
+ return workspace;
107
+ }
108
+
109
+ export function resolveScopedWorkspace(scope: string | undefined, cwd = process.cwd()): KnowledgeWorkspace {
110
+ if (scope === 'project' || scope === 'local') {
111
+ return workspaceForHome(projectKnowledgeHome(cwd));
112
+ }
113
+ return workspaceForHome(globalKnowledgeHome());
114
+ }
115
+
116
+ export function ensureParentDir(path: string): void {
117
+ mkdirSync(dirname(path), { recursive: true });
118
+ }
119
+
120
+ export function readKnowledgeConfig(path: string): KnowledgeConfig {
121
+ const raw = readFileSync(path, 'utf8');
122
+ return JSON.parse(raw) as KnowledgeConfig;
123
+ }
@@ -1,59 +0,0 @@
1
- name: Bug Report
2
- description: Report a bug in open-knowledge
3
- labels: [bug]
4
- body:
5
- - type: markdown
6
- attributes:
7
- value: |
8
- Thanks for reporting a bug!
9
- - type: textarea
10
- id: description
11
- attributes:
12
- label: Bug Description
13
- description: A clear description of the bug
14
- validations:
15
- required: true
16
- - type: textarea
17
- id: steps
18
- attributes:
19
- label: Steps to Reproduce
20
- description: |
21
- 1.
22
- 2.
23
- 3.
24
- validations:
25
- required: true
26
- - type: textarea
27
- id: expected
28
- attributes:
29
- label: Expected Behavior
30
- validations:
31
- required: true
32
- - type: textarea
33
- id: actual
34
- attributes:
35
- label: Actual Behavior
36
- validations:
37
- required: true
38
- - type: input
39
- id: version
40
- attributes:
41
- label: Version
42
- description: Output of `open-knowledge --version`
43
- - type: dropdown
44
- id: os
45
- attributes:
46
- label: Operating System
47
- options:
48
- - macOS
49
- - Linux
50
- - Windows
51
- - Other
52
- - type: dropdown
53
- id: runtime
54
- attributes:
55
- label: Runtime
56
- options:
57
- - Bun
58
- - Node.js
59
- - Other
@@ -1,34 +0,0 @@
1
- name: Feature Request
2
- description: Suggest a new feature or improvement
3
- labels: [enhancement]
4
- body:
5
- - type: markdown
6
- attributes:
7
- value: |
8
- Ideas are welcome! The best features solve real problems for AI agents and CLI users.
9
- - type: textarea
10
- id: problem
11
- attributes:
12
- label: Problem or Motivation
13
- description: What problem does this solve?
14
- validations:
15
- required: true
16
- - type: textarea
17
- id: solution
18
- attributes:
19
- label: Proposed Solution
20
- description: How would you like it to work?
21
- validations:
22
- required: true
23
- - type: textarea
24
- id: alternatives
25
- attributes:
26
- label: Alternatives Considered
27
- description: Any other approaches you considered?
28
- - type: checkboxes
29
- id: willingness
30
- attributes:
31
- label: Willingness to Implement
32
- options:
33
- - label: I am willing to implement this feature
34
- - label: I can help test a PR for this feature
@@ -1,21 +0,0 @@
1
- ## Summary
2
-
3
- <!-- 1-3 sentence description of the change -->
4
-
5
- ## Motivation
6
-
7
- <!-- Why is this change needed? What problem does it solve? -->
8
-
9
- ## Changes
10
-
11
- <!-- Bulleted list of what was changed -->
12
-
13
- ## Testing
14
-
15
- <!-- How was this tested? -->
16
-
17
- ## Checklist
18
-
19
- - [ ] Tests added / updated
20
- - [ ] Documentation updated (if needed)
21
- - [ ] `bun test` passes
@@ -1,49 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- pull_request:
7
- branches: [main]
8
-
9
- jobs:
10
- test:
11
- strategy:
12
- matrix:
13
- os: [ubuntu-latest, macos-latest]
14
- runtime: [bun, node]
15
- runs-on: ${{ matrix.os }}
16
- steps:
17
- - uses: actions/checkout@v4
18
-
19
- - name: Setup Bun
20
- if: matrix.runtime == 'bun'
21
- uses: oven-sh/setup-bun@v2
22
- with:
23
- bun-version: latest
24
-
25
- - name: Setup Node
26
- if: matrix.runtime == 'node'
27
- uses: actions/setup-node@v4
28
- with:
29
- node-version: latest
30
-
31
- - name: Install dependencies
32
- run: bun install
33
-
34
- - name: Run tests
35
- run: bun test
36
-
37
- test-matrix:
38
- strategy:
39
- matrix:
40
- os: [ubuntu-latest, macos-latest, windows-latest]
41
- runtime: [bun]
42
- runs-on: ${{ matrix.os }}
43
- steps:
44
- - uses: actions/checkout@v4
45
- - uses: oven-sh/setup-bun@v2
46
- with:
47
- bun-version: latest
48
- - run: bun install
49
- - run: bun test
@@ -1,7 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(connectors --help)"
5
- ]
6
- }
7
- }
@@ -1,31 +0,0 @@
1
- # Contributor Covenant Code of Conduct
2
-
3
- ## Our Pledge
4
-
5
- We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
-
7
- ## Our Standards
8
-
9
- Examples of behavior that contributes to a positive environment:
10
-
11
- * Using welcoming and inclusive language
12
- * Being respectful of differing viewpoints and experiences
13
- * Gracefully accepting constructive criticism
14
- * Focusing on what is best for the community
15
- * Showing empathy towards other community members
16
-
17
- Examples of unacceptable behavior:
18
-
19
- * The use of sexualized language or imagery and unwelcome sexual attention
20
- * Trolling, insulting/derogatory comments, and personal or political attacks
21
- * Public or private harassment
22
- * Publishing others' private information without explicit permission
23
- * Other conduct which could reasonably be considered inappropriate
24
-
25
- ## Enforcement
26
-
27
- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate.
28
-
29
- ## Attribution
30
-
31
- This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.
package/CONTRIBUTING.md DELETED
@@ -1,83 +0,0 @@
1
- # Contributing to open-knowledge
2
-
3
- Thank you for your interest in contributing!
4
-
5
- ## Development Setup
6
-
7
- ```bash
8
- # Clone the repo
9
- git clone https://github.com/hasna/knowledge.git
10
- cd knowledge
11
-
12
- # Install dependencies (Bun)
13
- bun install
14
-
15
- # Run tests
16
- bun test
17
-
18
- # Run a specific test file
19
- bun test tests/cli.test.ts
20
- ```
21
-
22
- ## Project Structure
23
-
24
- ```
25
- knowledge/
26
- ├── src/
27
- │ ├── cli.js # CLI entry point, argument parsing, commands
28
- │ └── store.js # Persistent store, file locking, ID generation
29
- ├── tests/
30
- │ └── cli.test.ts # Integration tests using Bun.test
31
- ├── package.json
32
- └── LICENSE
33
- ```
34
-
35
- ## Design Principles
36
-
37
- **Agent-friendly first**: every output should be parseable by an LLM. Prefer `--json` for structured data. Keep error messages actionable.
38
-
39
- **Minimal dependencies**: keep the dependency footprint small. The store is a plain JSON file.
40
-
41
- **Safe by default**: destructive operations require explicit confirmation flags (`--yes`).
42
-
43
- **Concurrent-safe**: all store mutations go through `withLock()`. Do not bypass it.
44
-
45
- ## Commit Conventions
46
-
47
- Use [Conventional Commits](https://www.conventionalcommits.org/):
48
-
49
- ```
50
- feat(cli): add --tag filter on list command
51
- fix(store): handle empty store file gracefully
52
- docs(readme): add installation instructions
53
- ```
54
-
55
- Types: `feat`, `fix`, `docs`, `chore`, `refactor`, `test`
56
-
57
- ## Pull Request Process
58
-
59
- 1. Fork the repo and create a branch from `main`.
60
- 2. Add tests for new functionality.
61
- 3. Ensure all tests pass: `bun test`.
62
- 4. Keep commits atomic and well-described.
63
- 5. Open a PR with a clear description of the change and motivation.
64
-
65
- ## Code Style
66
-
67
- - 2-space indentation
68
- - `for` loops over array methods where performance matters
69
- - Descriptive variable names
70
- - No unnecessary dependencies
71
-
72
- ## Reporting Issues
73
-
74
- - Use the [bug report template](.github/ISSUE_TEMPLATE/bug_report.yml)
75
- - Search existing issues first
76
- - Include: Node/Bun version, OS, steps to reproduce, expected vs actual
77
-
78
- ## Suggesting Features
79
-
80
- Open a [feature request issue](.github/ISSUE_TEMPLATE/feature_request.yml) describing:
81
- - The problem you're solving
82
- - How you envision the solution
83
- - Whether you're willing to implement it
package/FUNDING.yml DELETED
@@ -1 +0,0 @@
1
- github: hasna